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. It is what turns a plain signature into a PAdES B-T one.

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.NewDocTimeStamp("https://freetsa.org/tsr", crypto.SHA512)
if err != nil {
    return err
}

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

sigField := model.NewPdfFieldSignature(signature)
sigField.T = core.MakeString("Signature1")
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. SetName and SetReason on the signature are accepted but describe nothing a verifier uses.

Limitations

NewDocTimeStamp makes a mock Sign call while initializing the signature to find out how large the token is, so every timestamp costs two requests to the timestamp authority. Use NewDocTimeStampWithOpts to state the size up front:

handler, err := sighandler.NewDocTimeStampWithOpts("https://freetsa.org/tsr", crypto.SHA512,
    &sighandler.DocTimeStampOpts{SignatureSize: 4192})

SignatureSize defaults to 4192 when zero. If the token comes back larger than the reserved space, signing fails with model.ErrSignNotEnoughSpace. Client in the same options struct replaces the default sigutil.TimestampClient, which is where a proxy or an authenticated TSA fits.

Supported hash algorithms are crypto.SHA1, crypto.SHA256, crypto.SHA384 and crypto.SHA512. Others fail when the response is parsed.

Timestamping is a live HTTP request, so this cannot run offline, and the default client gives the TSA 5 seconds to answer.

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 timestamp. Note that the example swallows the error from the timestamp’s Initialize with a bare return, so a TSA failure at that point exits quietly with no output file and no message.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures
go run pdf_sign_timestamp.go cert.p12 password input.pdf output.pdf

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