Signing a PAdES B-T Signature
B-T is B-B plus a timestamp token obtained from an RFC 3161 timestamp authority
and embedded in the signature. Without it, the only record of when the document
was signed is the M entry the signer wrote themselves, which anyone can set to
any value. With it, a third party attests that the signature existed at a
particular moment, which is what lets a verifier decide whether the signing
certificate was still valid at that time.
Everything else matches B-B: same filter, same subfilter, same SHA-256 digest, same appender flow. The only difference is the handler and the TSA URL it needs.
Signing at B-T
timestampServerURL := "https://freetsa.org/tsr"
handler, err := sighandler.NewEtsiPAdESLevelT(priv, cert, cacert, timestampServerURL)
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
}From there it is the same field construction and appender.Sign(1, field) as at
B-B, and the result goes straight out with appender.WriteToFile. Unlike B-LT,
B-T needs no second pass, because nothing is written to the document security
store.
The timestamp request hashes the signature’s encrypted digest with SHA-512 and asks the TSA to include its certificates in the token. Neither is configurable through the handler.
Timestamp clients
If no client is set, the handler builds a default sigutil.TimestampClient,
whose HTTP client allows five seconds per request. TSAs that need
authentication, a proxy, or simply more time want a client of their own:
tsClient := sigutil.NewTimestampClient()
tsClient.HTTPClient = &http.Client{Timeout: 30 * time.Second}
tsClient.BeforeHTTPRequest = func(req *http.Request) error {
req.SetBasicAuth("user", "password")
return nil
}
padesHandler := sighandler.NewEtsiPAdES(sighandler.LevelT)
padesHandler.SetPrivateKey(priv)
padesHandler.SetCertificate(cert)
padesHandler.SetCA(cacert)
padesHandler.SetTimestampServerURL(timestampServerURL)
padesHandler.SetTimestampClient(tsClient)BeforeHTTPRequest runs on each outgoing request, so it is the place for
headers a particular TSA insists on.
Limitations
Initialize returns certificate timestamp server URL must not be empty for Level T & LT signature handler when the URL is missing. The handler will not
silently fall back to an untimestamped signature.
Signing contacts the TSA more than once. Initialize has to know how much space
to reserve for the signature contents, and it works that out by performing a real
signing pass, timestamp request included. Budget for two requests per signature
at B-T, and expect the whole operation to fail if the TSA is unreachable during
either one.
Only the first signer in the CMS structure gets a timestamp, following ETSI EN 319 142-2. This matters only for multi-signer CMS, not for the usual case of several signatures in one PDF, where each signature is timestamped separately.
Validation checks the token against the signature it covers, and returns hash in timestamp is different from pkcs7 when they disagree. A successful validation
puts the TSA’s time in the GeneralizedTime field of the result, which is the
way to tell from code whether a signature actually carries a timestamp. See
Validating PAdES signatures.
Run the example
The example signs page 1 against the free TSA at freetsa.org. main holds the
whole flow; the only line that differs from the B-B example is the
NewEtsiPAdESLevelT call.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures
go run pdf_sign_pades_b_t.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.