Skip to content
Sign with an External Service

Sign with an External Service

When the private key lives somewhere UniPDF cannot reach, the signature does not exist yet at the point the file has to be written. The way around that is to write the revision once with a fixed-size placeholder for the signature contents, let the external service sign the bytes the placeholder does not cover, and then overwrite the placeholder in those same bytes. The document is built only once.

ApproachUse it when
Placeholder and patch, described hereThe service takes bytes and hands back a complete PKCS7 package.
A custom model.SignatureHandlerThe service exposes something crypto.Signer-shaped, so it can be called during appender.Write. See the AWS KMS and Google Cloud KMS guides.

Reserving space

const sigLen = 8192

handler, err := sighandler.NewEmptyAdobePKCS7Detached(sigLen)
if err != nil {
    return err
}

signature := model.NewPdfSignature(handler)
signature.SetName("Test External Signature")
signature.SetDate(now, "")

if err := signature.Initialize(); err != nil {
    return err
}

The handler from NewEmptyAdobePKCS7Detached sets Filter, SubFilter and a Contents string of signatureLen zero bytes when Initialize runs, and never signs anything. A signatureLen of 0 or less falls back to 8192. The rest of the signing flow is ordinary: build the field with annotator.NewSignatureField, call appender.Sign(pageNum, field), and write.

Getting the signed bytes

signature.ByteRange is populated by appender.Write, not by Sign. The offsets depend on where the signature dictionary lands in the output, which is only known once the revision has been laid out. Write the document to a buffer first, then read the byte range off the same *model.PdfSignature and join the two covered spans:

// byteRange is {start1, end1, start2, end2}.
byteRange, err := parseByteRange(signature.ByteRange)
if err != nil {
    return err
}

content := append(append([]byte{}, pdfData[byteRange[0]:byteRange[1]]...),
    pdfData[byteRange[2]:byteRange[3]]...)

content is what the service signs, or what you hash with SHA-256 and send if the service takes a digest. It has to come back as a detached PKCS7 signature, because the handler wrote adbe.pkcs7.detached into SubFilter.

Patching in the signature

signatureData, err := getExternalSignature(content)
if err != nil {
    return err
}
if len(signatureData) > sigLen {
    return fmt.Errorf("signature is %d bytes, but only %d bytes are reserved", len(signatureData), sigLen)
}

sigBytes := make([]byte, sigLen)
copy(sigBytes, signatureData)

sig := core.MakeHexString(string(sigBytes)).Write()
copy(pdfData[byteRange[1]:byteRange[2]], sig)

The gap between the two covered spans, byteRange[1] up to byteRange[2], is the hex string of the signature including its angle brackets. Padding the external signature back out to the reserved length keeps the replacement exactly as long as the placeholder, so no other offset in the file shifts and the cross-reference table stays valid.

Limitations

The reserved length is fixed at Initialize time and the patch has to fit it. copy stops at the end of the destination, so without the length check a signature longer than the placeholder would be cut short and the file would carry a signature that does not verify. The example reserves 8192 bytes in sigLen and exits with an error when the returned signature is larger. A signature that embeds a certificate chain, a timestamp token or revocation data needs more room, so over-allocate rather than guessing the exact size.

Nothing in the PDF may change between computing the byte range and patching in the signature. Any edit outside the gap invalidates what the service signed.

Run the example

getExternalSignature stands in for the signing service: it generates a throwaway RSA key and self-signed certificate, builds a detached SHA-256 PKCS7 signature over content with github.com/unidoc/pkcs7, and returns its bytes. The example runs offline. generateSignedFile and the patching in main are the parts you would keep.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/external
go run pdf_sign_external.go <INPUT_PDF_PATH> <OUTPUT_PDF_PATH>

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

Sample output

The input document:

Sample PDF file

The same document after the external signature has been patched in:

PDF signed with an external service

Last updated on