Skip to content
Sign with a Custom Timestamp Client

Sign with a Custom Timestamp Client

A PAdES B-T or B-LT signature asks a timestamp authority (TSA) for a token while the document is being signed. By default that request goes through a plain HTTP client with a 5 second timeout. That is too short for some public TSAs, and it has no way to add the credentials a commercial TSA asks for or to route through a company proxy. SetTimestampClient replaces it with a sigutil.TimestampClient you configure.

Setting the client

padEs := sighandler.NewEtsiPAdES(sighandler.LevelLT)
padEs.SetCertificate(cert)
padEs.SetPrivateKey(priv)
padEs.SetCA(cacert)
padEs.SetTimestampServerURL("https://freetsa.org/tsr")
padEs.SetAppender(appender)

padEs.SetTimestampClient(&sigutil.TimestampClient{
    HTTPClient: &http.Client{
        Timeout: 30 * time.Second,
    },
})

SetPrivateKey takes a crypto.PrivateKey, so the same code signs with an RSA or an ECDSA key, with no constructor to choose.

TimestampClient has three fields:

FieldUse it for
HTTPClientTimeout, proxy, TLS settings. Nil selects the default client with a 5 second timeout.
BeforeHTTPRequestA callback that sees each request before it is sent, for an Authorization header or a TSA account key. Returning an error aborts the request.
MaxResponseSizeThe largest response accepted, in bytes. Zero or a negative value selects 1 MiB.

A TSA that wants HTTP basic authentication, for example:

padEs.SetTimestampClient(&sigutil.TimestampClient{
    HTTPClient: &http.Client{Timeout: 30 * time.Second},
    BeforeHTTPRequest: func(req *http.Request) error {
        req.SetBasicAuth(tsaUser, tsaPassword)
        return nil
    },
})

The client is only used at level T and above. At B-LT the handler also fetches certificates, OCSP responses and CRLs while signing, through clients of its own, and SetCertClient, SetOCSPClient and SetCRLClient replace those the same way. Each takes the matching sigutil client with its own HTTPClient field.

Writing the validation data

A B-LT signature needs two passes. The first writes the signature. The handler collects the validation data while it signs, and a second appender over the signed bytes writes it into the document security store:

buffer := bytes.NewBuffer(nil)
if err := appender.Write(buffer); err != nil {
    return err
}

pdf2, err := model.NewPdfReader(bytes.NewReader(buffer.Bytes()))
if err != nil {
    return err
}

appender2, err := model.NewPdfAppender(pdf2)
if err != nil {
    return err
}
appender2.SetDSS(appender.GetDSS())

return appender2.WriteToFile(outputPath)

This is the same flow as PAdES B-LT, which explains the levels and what the DSS holds.

Run the example

The example takes a PKCS12 file with the signing key and certificate, read with pkcs12.DecodeChain from software.sslmate.com/src/go-pkcs12, and a PEM file with the certificate of the CA that issued it. It contacts https://freetsa.org/tsr, so it needs network access.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/pades
go run pdf_sign_custom_client.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

The timestamp only counts towards trust if the TSA’s root is among your trust roots. Validated against the signer’s CA alone, the token is skipped with signature timestamp ignored: x509: certificate signed by unknown authority in TrustWarnings, and the signature is judged at the current time. See Validating Signer Trust and Revocation.

Last updated on