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. |
These constructors take an *rsa.PrivateKey. Each has an ECDSA twin with the same
arguments that takes an *ecdsa.PrivateKey instead: NewEtsiPAdESLevelBEcdsa,
NewEtsiPAdESLevelTEcdsa and NewEtsiPAdESLevelLTEcdsa.
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
}
var handler model.SignatureHandler
switch key := priv.(type) {
case *rsa.PrivateKey:
handler, err = sighandler.NewEtsiPAdESLevelB(key, cert, cacert)
case *ecdsa.PrivateKey:
handler, err = sighandler.NewEtsiPAdESLevelBEcdsa(key, cert, cacert)
default:
return fmt.Errorf("unsupported private key type %T", priv)
}
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.
priv is a crypto.PrivateKey, so the switch picks the constructor that matches
the key. ECDSA in PAdES signatures is defined by PDF 2.0. UniPDF does not enforce
that and does not raise the version, so signing a PDF 1.7 file produces a PDF 1.7
file with an ECDSA signature. A validator that checks conformance strictly may
reject it; see Sign with an ECDSA Key.
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 the key and certificate from a PFX file with
pkcs12.DecodeChain from software.sslmate.com/src/go-pkcs12, which reads files
written by OpenSSL 3 and files that carry a CA chain. The chain inside the PFX is
not used; the CA certificate comes from the PEM file. A key that is neither RSA nor
ECDSA stops the example with unsupported private key type. It 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/pades
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.