Skip to content
LTV Enable with a Document Timestamp

LTV Enable with a Document Timestamp

Validation data added in a second revision is not covered by the signature, so nothing in the file proves when it was added or that it has not been swapped since. A document timestamp in that same revision fixes both: it is a signature by a trusted timestamp authority over everything written so far, including the DSS. That is the shape a PAdES B-LTA signature has.

The example builds it in three revisions.

RevisionWritten byContents
1signFileThe signature.
2ltvEnableAndTimestampThe DSS for the signature, plus a document timestamp covering it.
3ltvEnableTimestampSigThe DSS for the timestamp signature. Optional.

Chaining the revisions

Each revision needs its own reader and appender, since an appender can only be written once. The bytes travel from one step to the next in memory:

signedBytes, err := signFile(inputPath, priv, cert)
if err != nil {
    return err
}

signedBytes, err = ltvEnableAndTimestamp(bytes.NewReader(signedBytes), certChain)
if err != nil {
    return err
}

signedBytes, err = ltvEnableTimestampSig(bytes.NewReader(signedBytes))
if err != nil {
    return err
}

return os.WriteFile(outputPath, signedBytes, 0644)

The helpers take *bytes.Reader rather than io.ReadSeeker because model.NewPdfAppender requires a source that satisfies both io.ReadSeeker and io.ReaderAt.

The DSS and the timestamp together

Inside ltvEnableAndTimestamp, the DSS is filled in and the timestamp signature applied to the same appender, so one write produces both:

ltv, err := model.NewLTV(appender)
if err != nil {
    return nil, err
}

if err := enableSignedFields(reader, ltv, certChain); err != nil {
    return nil, err
}

handler, err := sighandler.NewDocTimeStamp("https://freetsa.org/tsr", crypto.SHA512)
if err != nil {
    return nil, err
}

signature := model.NewPdfSignature(handler)
signature.SetName("Test Sign Timestamp")
signature.SetReason("TestSignTimestamp")

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

sigField := model.NewPdfFieldSignature(signature)
sigField.T = core.MakeString("Test Sign 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 nil, err
}

A document timestamp has no signer and nothing to show, so the field is built directly with model.NewPdfFieldSignature and given a zero rectangle rather than going through annotator.NewSignatureField.

LTV enabling the timestamp

The last revision calls enableSignedFields(reader, ltv, nil). No chain is passed because there is nothing to add: a timestamp signature has the subfilter ETSI.RFC3161, and for that subfilter the certificate chain is read out of the PKCS7 token in the signature contents, which the TSA populates with its own certificates. The result keeps the timestamp verifiable after the TSA certificate expires, which is what distinguishes a B-LTA signature from a B-LT one.

This step is optional. Skip it and the document signature stays long-term verifiable; the timestamp over it does not.

Limitations

NewDocTimeStamp performs a mock Sign while initializing the signature in order to size the contents, so each timestamp costs two requests to the TSA. sighandler.NewDocTimeStampWithOpts with DocTimeStampOpts.SignatureSize skips the mock call; the default size is 4192 bytes, and a token larger than the reserved size fails with model.ErrSignNotEnoughSpace.

The supported hash algorithms are crypto.SHA1, crypto.SHA256, crypto.SHA384 and crypto.SHA512. Anything else fails when the response is parsed.

The example points at https://freetsa.org/tsr, a public test service. Each revision that timestamps makes a live HTTP request, so the example does not run offline.

Everything under LTV enable a signed file about skipped revocation lookups applies to both calls here. enableSignedFields calls ltv.Enable on each signature field whose value has a /Contents entry and skips the rest, which EnableAll would reject. A field whose /Contents is an empty placeholder still reaches Enable and fails with invalid signature field; the loop in that guide also skips those.

Run the example

signFile, ltvEnableAndTimestamp and ltvEnableTimestampSig map one to one onto the three revisions in the table above. The PKCS12 file is decoded with pkcs12.DecodeChain from software.sslmate.com/src/go-pkcs12 and has to hold an RSA key. The signing chain is the signer certificate, the CA certificates bundled in the PKCS12 file, and any issuers from the optional PEM argument. The PEM argument applies to the signing chain only; the timestamp chain comes from the token. The Running the examples section of the LTV README shows how to check the output offline.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/ltv
go run pdf_sign_ltv_timestamp_revision.go <FILE.p12> <P12_PASS> <INPUT_PDF_PATH> <OUTPUT_PDF_PATH> [<EXTRA_CERTS.pem>]

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