LTV Enable a Signed File
A signature can only be checked while the certificates in its chain are still
available and their revocation status can still be looked up. Long-Term
Validation solves that by storing the chain, the OCSP responses and the CRLs
inside the document, in the Document Security Store, so a validator years later
needs nothing from the network. model.LTV collects that data and writes it into
a new revision.
| Where the data goes | Call | Used for validating |
|---|---|---|
| Global DSS entries | EnableChain(chain) | Any signature in the document. |
A VRI entry, plus the global entries | Enable, EnableByName, EnableAll | One specific signature. |
The per-signature form is the recommended one, and the reason it needs its own
revision is that the VRI key is upper(hex(sha1(sig.Contents))). The contents
are not known until the revision holding the signature has been written, and
changing the DSS afterwards would invalidate the signature you just computed. If
everything has to happen in one revision, EnableChain is the only option; see
LTV in one revision.
Adding the data in a second revision
The flow is: sign and write the file, then open the signed bytes with a new reader and appender, LTV enable the signatures, and write again. The second write appends a revision holding only the DSS, so the signed bytes stay untouched and the signature still covers its whole revision. This is the same whether you signed the file a moment ago or received it from someone else.
appender, err := model.NewPdfAppender(reader)
if err != nil {
return err
}
ltv, err := model.NewLTV(appender)
if err != nil {
return err
}
enabled := 0
if reader.AcroForm != nil {
for _, field := range reader.AcroForm.AllFields() {
sigField, ok := field.GetContext().(*model.PdfFieldSignature)
if !ok || sigField.V == nil || sigField.V.Contents == nil || len(sigField.V.Contents.Bytes()) == 0 {
continue
}
if err := ltv.Enable(sigField.V, certChain); err != nil {
return err
}
enabled++
}
}
if enabled == 0 {
return errors.New("no signed signature fields found")
}
return appender.WriteToFile(outputPath)The loop calls Enable once per signature field that holds a signature, and
skips empty signature fields. Enable takes the chain from the signature
dictionary, fetches the validation data, adds it to the global DSS arrays, and
adds a VRI entry keyed by that signature’s contents hash, pointing at the
certificates, OCSP responses and CRLs for that signature. A document timestamp is
a signature field too, so a signed and timestamped file gets two VRI entries,
one per signature. The certChain argument is additive: certificates the
signature does not carry, typically the issuing CA, which is what the optional
PEM file in the example supplies. Where the chain still has a gap, the client
downloads the issuer from the certificate’s IssuingCertificateURL, then requests
OCSP and CRL data for each certificate that advertises a responder.
If an existing DSS is present, NewLTV picks it up from the reader and extends
it rather than replacing it.
Limitations
SkipExisting defaults to true, so a signature that already has a VRI entry is
left alone. Set it to false on the LTV value to refresh the data.
Failures fetching revocation data are not failures. A timed-out OCSP request, an
unreachable CRL server or a missing issuer certificate is logged at debug level
and skipped, and Enable still returns nil. The output can be an LTV-enabled
file with no revocation data in it at all. Certificates that advertise no OCSP
responder and no CRL distribution point contribute nothing either. If you need
certainty, inspect the resulting DSS rather than trusting the absence of an
error.
The default HTTP timeout on CertClient, OCSPClient and CRLClient is 5
seconds per request. Raise it through ltv.OCSPClient.HTTPClient.Timeout and the
equivalents when talking to slow responders. Setting ltv.CRLClient or
ltv.OCSPClient to nil skips that class of data entirely.
Only self-signed certificates count as CA certificates for the purposes of skipping revocation lookups, so intermediates get OCSP and CRL requests too. The chain walk stops at the first self-signed certificate.
When no certificate can be assembled at all, the call fails with
ErrSignNoCertificates, “could not retrieve certificate chain”.
EnableAll is the shorter call, but it does not suit every file. On a document
with no signature fields it does nothing and returns nil, and writing the appender
still adds a revision. On a document with an empty signature field, such as one
left for a second signer, it returns an “invalid signature field” error rather
than skipping the field, despite logging that it intends to skip. Checking each
field and calling Enable, as above, avoids both.
appender.Write and WriteToFile can only be called once per appender. Adding
another revision means a fresh reader and appender over the bytes just written.
Run the example
The example takes an already signed PDF and adds one revision containing the
validation data, with a VRI entry for every signed signature field. The optional
third argument is a PEM file of issuer certificates, concatenated, for chains the
signature does not carry in full. If the file has no signed signature field, the
example writes nothing and exits with Fail: no signed signature fields found.
To get a signed input, run one of the signing examples first. The Running the examples section of the LTV README lists the commands and how to check the result offline with the validation examples.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/ltv
go run pdf_ltv_enable_signed_file.go <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.