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 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 the finished file.

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

handler, err := sighandler.NewEmptyAdobePKCS7Detached(8192)
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
}

NewEmptyAdobePKCS7Detached sets Filter, SubFilter and a Contents string of signatureLen zero bytes, and does nothing else. 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.

Patching in the signature

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:

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

sigBytes := make([]byte, 8192)
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 a signature longer than the placeholder is silently truncated: you get a file that opens fine and a signature that will not verify. Nothing returns an error. Over-allocate rather than trying to guess the exact size.

The bytes the external service signs are the two spans outside the gap. If you generate the document once to obtain the byte range and again to obtain the signature, both passes have to produce byte-identical output outside the gap. This is why the example fixes now in a package-level variable rather than calling time.Now() inside the signing helper: a differing signature date changes the length of the dictionary and invalidates the result.

sighandler.NewAdobePKCS7Detached pads its own output to 8192 bytes regardless of what came out of pkcs7, so when you simulate an external service with UniPDF itself, keep the placeholder at 8192 too.

Run the example

getExternalSignature stands in for the signing service: it generates a throwaway key and certificate, signs the same input with sighandler.NewAdobePKCS7Detached, and returns just the signature bytes. generateSignedFile is the part you would keep.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures
go run pdf_sign_external.go input.pdf output.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

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