Skip to content
Signing a PAdES B-T Signature

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.

View the full source
Last updated on