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.NewAdobeX509RSASHA1CustomWithOpts, 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.SHA256)
}

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

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. Algorithm decides which hash NewDigest returns, and the crypto.SHA256 passed to the signer has to name the same one. Change one and the other has to change with it.

sighandler.NewAdobeX509RSASHA1Custom(certificate, signFunc) takes no options and hashes with SHA-1, so a signing function used with it passes crypto.SHA1.

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, and the example keeps the default. Setting EstimateSize: true in AdobeX509RSASHA1Opts computes the size from the modulus of the certificate’s public key instead and skips the call.

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. It loads the library from the SOFTHSM2_LIB environment variable, or from /usr/local/lib/softhsm/libsofthsm2.so (the PathSoftHSM constant) when the variable is unset. Homebrew installs it at /opt/homebrew/lib/softhsm/libsofthsm2.so and Debian/Ubuntu packages at /usr/lib/softhsm/libsofthsm2.so.

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 with SHA-256. initPKCS11Session is the place to change how the library path or the pin is supplied. The external signing README has the full SoftHSM setup, including creating the test token used below.

Point the example at your SoftHSM library if it is not in the default location:

export SOFTHSM2_LIB=/opt/homebrew/lib/softhsm/libsofthsm2.so

Then get the example and create the key pair:

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/external
go run pdf_sign_hsm_pkcs11_cgo.go add test PIN keypair_label

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

And sign:

go run pdf_sign_hsm_pkcs11_cgo.go sign test PIN keypair_label input.pdf input_signed.pdf
View the full source
Last updated on