How to sign a PDF with an external signing service
Getting the green checkmark in Acrobat requires an AATL approved certificate, and those are normally held in hardware: a USB token, a smart card, or an HSM reached over PKCS#11. Operating that hardware yourself is often not practical, so the key stays with a third party and you send them something to sign.
The pattern is a placeholder pass followed by a byte splice. Sign with
NewEmptyAdobePKCS7Detached, which reserves space without producing a real
signature, then overwrite the reserved bytes with what the service returns:
const sigLen = 8192
handler, err := sighandler.NewEmptyAdobePKCS7Detached(sigLen)
if err != nil {
return err
}
// Sign as usual with this handler and write to a buffer.
pdfData, signature, err := generateSignedFile(inputPath, handler)
if err != nil {
return err
}
byteRange, err := parseByteRange(signature.ByteRange)
if err != nil {
return err
}
// The signed content is everything except the reserved /Contents gap.
content := append(append([]byte{}, pdfData[byteRange[0]:byteRange[1]]...), pdfData[byteRange[2]:byteRange[3]]...)
// Send content, or its SHA-256 digest, to the signing device or service and
// get a detached PKCS#7 signature back.
signatureData, err := getExternalSignature(content)
if err != nil {
return err
}
if len(signatureData) > sigLen {
return fmt.Errorf("signature is %d bytes, only %d reserved", len(signatureData), sigLen)
}
sigBytes := make([]byte, sigLen)
copy(sigBytes, signatureData)
copy(pdfData[byteRange[1]:byteRange[2]], core.MakeHexString(string(sigBytes)).Write())generateSignedFile, parseByteRange and getExternalSignature are helpers
defined in the example itself, not library calls. Only
signature.ByteRange, a *core.PdfObjectArray on model.PdfSignature, comes
from UniPDF.
What goes to the service is the signed content, not the whole file. The
ByteRange names two spans, the bytes before the reserved /Contents gap and the
bytes after it, and the signature has to cover exactly those. Sending the whole
buffer would sign the placeholder too, and no validator would accept the result.
The same sigLen sizes the reserved region and the buffer written into it. The
splice writes a full-length padded buffer, so a shorter one would leave stale
bytes behind and a longer one would run past the gap and corrupt the file. Make
it comfortably larger than any signature the service will return, and reject a
signature that does not fit rather than truncating it.
Splice into byteRange[1]:byteRange[2], the gap the ByteRange skips over. Those
are exactly the bytes excluded from the digest, which is why writing there does
not invalidate the signature you just computed. Nothing else in the file may
change after the digest is taken, so this is a byte-level patch on the buffer
rather than a re-write through the appender.
If your service exposes a signing callback instead of a detached blob, the other
route is sighandler.NewAdobeX509RSASHA1Custom, which takes a SignFunc that
UniPDF calls with the digest. That handler invokes your function twice by
default, once to size the signature; see
why the handler is called twice for the
option that avoids the extra call, which matters when every call is a billable
API request.
You can check out the user guide to learn how to use such a 3rd party service to work with UniPDF to send the signed content, or its digest, to the 3rd party server, and then applying the signature to the final PDF.
There are worked guides for the common providers: AWS KMS, Google Cloud KMS, GlobalSign DSS and an HSM via PKCS11.