Technical Article

PAdES LTV Evidence and Seed Values in HotPDF

A PDF you just signed is a B-B signature and nothing more. It proves who signed and that the bytes have not moved, but it carries no proof that the signer certificate was valid at signing time, so a validator years from now has to go looking for revocation data that may no longer exist. Closing that gap means writing OCSP responses and CRLs into the document-level Document Security Store, and in HotPDF that is one call: PopulatePAdESLTVEvidence walks every loaded signature, derives the revocation requests from the certificate set, executes them through a transport you supply, and writes the fetched material plus the CMS chain into the DSS. It returns the number of signatures whose evidence landed, or minus one when the document has no signature field at all

The design decision worth understanding before you use it is that the library never opens a socket. Every byte that arrives from the network arrives through a callback you wrote. That is not caution for its own sake; it is the only way this feature can work inside the environments that actually demand long-term validation

Why does the library refuse to do its own HTTP?

Because the places that require B-LT signatures are the places where a library cannot be trusted with the network. Signing services run behind authenticating proxies with corporate roots. Air-gapped signing tiers have no route to a responder and must be fed cached evidence. Audit regimes require that every outbound request be logged by the application, not buried in a dependency. And test suites need deterministic responses, which is impossible if the library dials out on its own

The transport is a plain function reference with a fixed shape, so the policy stays yours. HotPDF hands you a request record describing exactly what to fetch, including the content type and a response size cap, and you return the bytes plus a status

HotPDF PopulatePAdESLTVEvidence flow: the caller-supplied FetchEvidence transport, request record fields and per-signature status outcomes
Every network byte passes through your FetchEvidence callback, and each signature gets its own status so one timeout never aborts the pass
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind says whether this is an OCSP POST or a CRL GET;
    // Request.ContentType and Request.Body are already prepared,
    // and Request.MaxResponseBytes is the cap you must honour
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry lets the retry policy back off; use
      // setsPermanentFailure for a 404 or a bad URL
      Result := setsRetry;
    end;
  end;
end;

// One-call B-B to B-LT upgrade for every signature in the loaded file
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // Append-only save: the bytes the existing signatures cover
      // are preserved verbatim
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

Failures are per signature, not per document. A responder that times out for one signer skips that signer material and leaves the rest of the pass intact, which is the behaviour you want in a batch: partial evidence beats an aborted run, and the return value tells you how many signatures actually improved

The chain the CMS forgot to include

Revocation checking needs the issuer certificate, and a surprising number of signing stacks omit intermediates from the CMS container. The recovery route is the Authority Information Access extension, access method 1.3.6.1.5.5.7.48.2, which advertises a URL where the issuer certificate can be downloaded. HPDFFetchAIAIntermediates walks those URLs through the same transport, parses the DER out of each response, and returns only the certificates the CMS did not already carry, keyed by DER hash so duplicates and loops cannot spin

Two details decide whether this works against real certificate authorities. The first is encoding: CA endpoints serve the certificate as bare DER about as often as they serve it PEM armoured, and there is no reliable content type to distinguish them. The robust probe is textual, then structural. Look for the -----BEGIN CERTIFICATE----- marker, strip the armour and decode base64 if it is present, and in both paths confirm that the first byte of the result is $30, the DER tag for a SEQUENCE. The second is depth: a fetched intermediate can itself advertise an AIA URL for its own issuer, so the walk appends new candidates to the queue and completes chains that are two or three hops short. That has to be capped, which is what the MaxFetch parameter is for

AIA chain completion diagram for HotPDF: caIssuers URL fetch, PEM versus DER probe, DER hash deduplication and the MaxFetch depth cap
HPDFFetchAIAIntermediates walks caIssuers URLs through the same transport, probing PEM armour and capping the queue with MaxFetch

What is a signature seed value, and why does it fail silently?

A seed value is a constraint the document author attaches to a signature field to tell the signer what kind of signature is acceptable: which SubFilter, which digest algorithm, which reasons, which minimum PDF version, whether revocation information must be embedded. It lives in an /SV dictionary on the field and is defined in ISO 32000-1 §12.7.5.5. HotPDF writes it with AttachPAdESSeedValue and checks it with CheckLoadedSignatureSeedValue, which returns True when the field is unconstrained or every present constraint passes, and on False names the first failing constraint through an output parameter you can put straight into an error message

The mechanism that makes seed values easy to get wrong is the /Ff flags entry described in §12.7.5.5.3. A set bit marks its constraint as required: a mismatch is an error and the signer must refuse. A clear bit marks the same constraint as a preference: the value filters what the UI should offer and nothing more. Two traps follow from that. First, /Ff lives inside the /SV dictionary, not on the widget annotation, so code that reads the field-level /Ff gets an empty answer forever and concludes that nothing is forced. Second, the bit assignments are not a simple run of one, two, four, eight; in HotPDF the writer emits 2 for SubFilter, 4 for MinVersion, 32 for AddRevInfo and 64 for DigestMethod. A reader that assumes sequential bits decodes every constraint as optional and passes every test except the one that matters

Seed value flag bit table for HotPDF PAdES signing showing Ff bits 2, 4, 32 and 64 and required versus preferred constraint handling
The /Ff entry lives inside /SV, and each bit position decides whether a mismatch is a hard refusal or a UI preference
var
  Violation: AnsiString;
begin
  // Ask the field whether the profile we are about to sign with is allowed
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // Constraint satisfied: proceed with the signing pass
end;

The test that exposed the original decoding bug was not a positive test. It was the assertion that a forced mismatch must be rejected, and it is the only kind of test that can catch this class of defect: a decoder reading the wrong dictionary or the wrong bit positions produces "no constraints violated" for every input, which looks exactly like correct behaviour until you deliberately violate one

Where this sits on the LTV ladder

Four rungs, and each needs the one below it. B-B is the bare signature. B-T adds a trusted timestamp, which fixes the signing time so a validator knows which moment to evaluate revocation against. B-LT adds the revocation evidence to the DSS, which is what PopulatePAdESLTVEvidence automates. B-LTA adds document timestamps that are renewed before the previous one weakens, extending validity indefinitely; HotPDF exposes that as RenewPAdESLTATimestamp, which appends a new timestamp as an incremental revision and preserves every earlier signature, timestamp and DSS entry untouched

An incremental-update model is the only correct way to add evidence to a signed document, because rewriting the file would break the byte ranges the existing signatures cover. If you need to reason about what changed between revisions, and whether those changes are the sort a signature permits, that analysis is covered separately in the DocMDP and FieldMDP revision analysis. The signing pipeline itself, including certificate sources and byte-order pitfalls, is in the PAdES signing walkthrough, and the validation side is in verifying signatures on loaded documents

One practical warning about ordering. Collect evidence as soon as possible after signing, ideally in the same job. The responders that can answer for a certificate are online while the certificate is current and gone years later, so a document that leaves your pipeline as B-B may never be upgradable again. HotPDF runs as a native VCL component for Delphi and C++Builder, and the whole evidence pass is in-process apart from your own transport; the supported profiles are listed on the HotPDF Delphi PDF component product page