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:
| Field | Use it for |
|---|---|
HTTPClient | Timeout, proxy, TLS settings. Nil selects the default client with a 5 second timeout. |
BeforeHTTPRequest | A callback that sees each request before it is sent, for an Authorization header or a TSA account key. Returning an error aborts the request. |
MaxResponseSize | The 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.