Skip to content
Add a Timestamp to a Signature

Add a Timestamp to a Signature

The date on a signature is whatever the signer’s machine claimed at the time. A document timestamp replaces that with an assertion from a timestamp authority: a signature of subfilter ETSI.RFC3161 over everything written so far, proving the document existed in that form at a time the signer did not choose. Over a PAdES B-B signature, a document timestamp is one way to reach PAdES B-T.

The timestamp has to go in its own revision, since it has to cover the revision that holds the signature, and an appender can only be written once.

Doing it

Write the signed revision to a buffer, then start again over those bytes:

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

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

appender, err = model.NewPdfAppender(reader)
if err != nil {
    return err
}

handler, err = sighandler.NewDocTimeStampWithOpts("https://freetsa.org/tsr", crypto.SHA256, &sighandler.DocTimeStampOpts{
    SignatureSize: 8192,
    Client: &sigutil.TimestampClient{
        HTTPClient: &http.Client{Timeout: 30 * time.Second},
    },
})
if err != nil {
    return err
}

signature = model.NewPdfSignature(handler)
signature.SetName("Document Timestamp")
signature.SetReason("RFC 3161 document timestamp")
signature.SetDate(time.Now(), "")

if err := signature.Initialize(); err != nil {
    return err
}

sigField := model.NewPdfFieldSignature(signature)
sigField.T = core.MakeString("Document Timestamp")
sigField.Rect = core.MakeArray(core.MakeInteger(0), core.MakeInteger(0),
    core.MakeInteger(0), core.MakeInteger(0))

if err = appender.Sign(1, sigField); err != nil {
    return err
}

return appender.WriteToFile(outputPath)

bytes.NewReader matters: model.NewPdfAppender returns an error unless the reader’s source satisfies both io.ReadSeeker and io.ReaderAt, and a *bytes.Buffer satisfies neither.

There is no signer and nothing to display, so the field is built with model.NewPdfFieldSignature and a zero rectangle rather than through annotator.NewSignatureField. The example still sets a name, reason and date on the signature dictionary. A validator reports them, but the timestamp’s own time is the GeneralizedTime inside the token.

The handler options

NewDocTimeStampWithOpts takes the TSA URL, the hash algorithm and a DocTimeStampOpts.

The hash algorithm is the one UniPDF digests the document with and sends to the TSA as the message imprint. Supported values are crypto.SHA1, crypto.SHA256, crypto.SHA384 and crypto.SHA512; others fail when the response is parsed. The TSA signs the token with a digest of its own choosing, and that is the one most viewers display. freetsa.org signs with SHA-512, so a SHA-256 timestamp from it shows up as SHA-512 in a signature panel. The imprint inside the token is still SHA-256.

SignatureSize is the number of bytes reserved for the token in /Contents, and it has to be set. Left at zero it defaults to 4192, and a SHA-256 token from freetsa.org is about 4.6 KB, so signing would fail with model.ErrSignNotEnoughSpace (insufficient space allocated for the signature contents). 8192 leaves room; unused bytes are padded with zeros and cost nothing else. The size depends on the TSA and how many certificates it puts in the token, so check it against the authority you use.

Client is optional. It replaces the default sigutil.TimestampClient, whose HTTP client gives the TSA 5 seconds to answer. The example raises that to 30 seconds; a proxy or an authenticated TSA fits in the same place.

Limitations

Timestamping is a live HTTP request, so this cannot run offline.

sighandler.NewDocTimeStamp(url, hash), without options, does not know the token size. It makes a mock Sign call while initializing the signature to find out, so every timestamp costs two requests to the TSA. Stating the size with NewDocTimeStampWithOpts avoids that.

The timestamp only proves existence, not validity. Keeping it verifiable in the long run means putting its own certificate chain in the DSS as well, which is what LTV enable with a document timestamp covers.

Run the example

main does both revisions in sequence: sign with sighandler.NewAdobePKCS7Detached from a PKCS12 file, then add the ETSI.RFC3161 document timestamp in a second revision. The timestamp comes from freetsa.org, so the example needs network access, and it exits with Fail: and the error if the TSA request fails.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/signing
go run pdf_sign_timestamp.go <FILE.p12> <PASSWORD> <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