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.
| Revision | Written by | Contents |
|---|---|---|
| 1 | signFile | The signature. |
| 2 | ltvEnableAndTimestamp | The DSS for the signature, plus a document timestamp covering it. |
| 3 | ltvEnableTimestampSig | The 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.