Skip to content
Sign with an HSM via PKCS11

Sign with an HSM via PKCS11

A PKCS11 token exposes a crypto.Signer and nothing else: the private key never leaves the device, and signing only works while a session is open. That fits sighandler.NewAdobeX509RSASHA1Custom, which takes a certificate and a signing function instead of a key.

The signing function

signFunc := func(sig *model.PdfSignature, digest model.Hasher) ([]byte, error) {
    h, ok := digest.(hash.Hash)
    if !ok {
        return nil, errors.New("hash type error")
    }

    return signer.Sign(rand.Reader, h.Sum(nil), crypto.SHA1)
}

handler, err := sighandler.NewAdobeX509RSASHA1Custom(certificate, signFunc)

The model.Hasher handed to the function is a hash.Hash for this handler, not a *bytes.Buffer as it is for the PKCS7 handlers, so the byte range has already been hashed by the time you see it. The algorithm is SHA-1 by default, which is why crypto.SHA1 is passed to the signer. Those two have to agree.

To use something stronger, or to avoid the sizing round trip described below, build the handler with options instead:

handler, err := sighandler.NewAdobeX509RSASHA1CustomWithOpts(certificate, signFunc,
    &sighandler.AdobeX509RSASHA1Opts{
        Algorithm:    crypto.SHA256,
        EstimateSize: true,
    })

Changing Algorithm changes what NewDigest returns, so the crypto.SHA1 argument inside signFunc has to change with it.

Session lifetime

crypto11.Configure opens the session and the signer is only usable until it is closed. The handler’s signing function runs during appender.Write, not during appender.Sign, so the session has to still be open at write time. The example defers ctx.Close() in main and does everything inside that scope.

Limitations

By default the handler makes a mock Sign call while initializing the signature, purely to find out how many bytes to reserve for the contents. That is a second operation on the token for every signature. EstimateSize: true computes the size from the modulus of the certificate’s public key instead and skips it.

adbe.x509.rsa_sha1 carries a single certificate in the signature’s Cert entry, not an array, so this handler cannot publish a chain the way the PKCS7 handlers do. Validation software has to build the path itself, or you have to add the chain through the DSS.

The example generates a self-signed certificate for the token key each time it runs, which is only useful for testing. Use a certificate issued for the key instead.

The example needs cgo and a PKCS11 library at the path in the PathSoftHSM constant, which is set for a Linux SoftHSM install and differs elsewhere.

Run the example

The example has two subcommands. add generates a 2048-bit RSA key pair in the token under a label, sign looks that pair up, builds a certificate for it and signs. initPKCS11Session is the place to change the library path or how the pin is supplied. The signatures/README.md in the examples repository has the full SoftHSM setup, including creating the test token used below.

Create the key pair first:

go run pdf_sign_hsm_pkcs11_cgo.go add test PIN keypair_label

Then sign:

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures
go run pdf_sign_hsm_pkcs11_cgo.go sign test PIN keypair_label input.pdf input_signed.pdf

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source
Last updated on