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.
| Approach | Use it when |
|---|---|
| Placeholder and patch, described here | The service takes bytes and hands back a complete PKCS7 package. |
A custom model.SignatureHandler | The 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.pdfIf 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:

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