Signing a PAdES B-B Signature
PAdES describes four baseline levels, each one adding material that a validator will need later on. B-B is the floor: a CAdES signature embedded in the PDF and nothing else. It shows who signed and that the bytes have not changed since, and it stays verifiable only as long as somebody can still reach the certificate chain and revocation data on their own.
Choosing a baseline level
| Level | Handler | What it adds over the level below |
|---|---|---|
| B-B | sighandler.NewEtsiPAdESLevelB | Nothing. The signature on its own. |
| B-T | sighandler.NewEtsiPAdESLevelT | A timestamp token from a TSA over the signature, so the signing time no longer rests on the signer’s clock. |
| B-LT | sighandler.NewEtsiPAdESLevelLT | The certificate chain, OCSP responses and CRLs, collected at signing time and written into the document’s DSS. |
| B-LTA | NewEtsiPAdESLevelLT, then a NewDocTimeStamp revision | A document timestamp covering the whole file, which can be renewed before the algorithms protecting it weaken. |
The SDK has no level constant for B-LTA. sighandler.EtsiPAdESLevel defines
LevelB, LevelT and LevelLT only, and a B-LTA file is a B-LT file with a
document timestamp appended in a further revision. See
PAdES B-LTA for that.
B-B is enough when the document is checked shortly after it is signed and the verifier is expected to have their own trust store and network access. Go to B-T as soon as the signing time matters to anyone, since without a timestamp there is nothing but the signer’s own clock to date the signature. B-LT and B-LTA are about archival: they make the document self-contained, so it still validates after the issuing CA retires its OCSP responder.
All four levels write the same Adobe.PPKLite filter and
ETSI.CAdES.detached subfilter, and all of them digest with SHA-256. The level
only changes what gets attached alongside.
Signing at B-B
appender, err := model.NewPdfAppender(reader)
if err != nil {
return err
}
handler, err := sighandler.NewEtsiPAdESLevelB(priv, cert, cacert)
if err != nil {
return err
}
signature := model.NewPdfSignature(handler)
signature.SetName("John Doe")
signature.SetReason("Approval")
signature.SetDate(time.Now(), "")
if err := signature.Initialize(); err != nil {
return err
}Initialize fills in the signature dictionary and reserves space in its
Contents entry by running the handler through a mock signing pass. It also
takes a copy of the handler, so configure the handler fully before you call it;
changing the handler afterwards has no effect on the signature.
The signing key must be an *rsa.PrivateKey here. For an ECDSA key use
NewEtsiPAdESLevelBEcdsa, which needs a PDF 2.0 or later document.
Once the signature exists, wrap it in a field and sign a page:
field, err := annotator.NewSignatureField(signature, lines, opts)
if err != nil {
return err
}
field.T = core.MakeString("Self signed PDF")
if err := appender.Sign(1, field); err != nil {
return err
}The page number is 1-based and Sign returns page N not found if it falls
outside the document. Because the work goes through a PdfAppender, the output
is an incremental update: the original bytes are preserved, so signatures already
in the file stay valid.
Limitations
The CA certificate argument may be nil. The signature then carries no issuer certificate beyond the signing one, and a verifier has to build the chain itself.
Initialize returns certificate must not be nil or private key must not be nil when either is missing, which is the usual reason a working validation
handler cannot be reused for signing.
Nothing in the B-B path touches the network, and nothing is written to the DSS. If you need revocation data inside the file, that is B-LT.
NewEtsiPAdES(level) is the alternative to the fixed-argument constructors. It
returns a handler configured through setters (SetPrivateKey, SetCertificate,
SetCA, SetTimestampServerURL, SetTimestampClient, SetCertClient,
SetOCSPClient, SetCRLClient, SetAppender, SetDSS), which is what you want
when the HTTP clients need custom transports or proxies.
Run the example
The example loads a key pair from a PFX file and a CA certificate from a PEM
file, then signs page 1 and writes the result. Everything happens in main.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures
go run pdf_sign_pades_b_b.go <FILE.PFX> <PASSWORD> <FILE.PEM> <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.