Adobe Acrobat reported every signature in the batch as valid. The customer's eIDAS conformance checker rejected every one of them. The gap came down to a single name in the signature dictionary: the files carried /SubFilter /adbe.pkcs7.detached, which produces a perfectly sound ISO 32000-1 §12.8 signature and a non-conformant PAdES one, because ETSI EN 319 142-1 requires ETSI.CAdES.detached at every baseline level. The cryptography was flawless; the document simply was not claiming the profile the regulator demanded. If your Delphi application signs invoices, contracts, or lab reports that must survive European-style validation policy, this distinction is the first thing to get right — and it is one call in losLab PDF Library (PDFlibPas), whose signing, timestamping, and DSS toolchain this article works through from baseline signature to long-term validation.
What turns a PDF signature into a PAdES signature
ETSI EN 319 142-1 defines four baseline levels stacked on the CMS format. PAdES-B-B is the entry point: a CAdES signature in a PDF signature field with the ETSI.CAdES.detached SubFilter and a signed signing-certificate attribute. PAdES-B-T adds an RFC 3161 timestamp over the signature value, proving the signature existed before a point in time nobody can backdate. PAdES-B-LT embeds the certificates, CRLs, and OCSP responses needed for validation into a Document Security Store, so the file remains verifiable after the issuing CA retires its infrastructure. PAdES-B-LTA caps the stack with a document timestamp that re-protects the accumulated evidence as algorithms weaken.
PDFlibPas maps these concepts onto its sign-process API. The profile marker is SetSignProcessCustomSubFilter; ETSI commitment-type indications — proof of origin, proof of approval, and the other ETSI-defined identifiers 1 through 6 — go through SetSignProcessCommitmentType; an explicit signature policy attaches with SetSignProcessSignaturePolicy, which takes the policy OID and its digest. One default deserves attention: with the digest algorithm left on auto, the library selects SHA-256 for ETSI and adbe.pkcs7.detached signatures and falls back to SHA-1 only on the legacy adbe.pkcs7.sha1 path. Set it explicitly anyway; auditors ask, and explicit beats inferred in every compliance review.
Producing the baseline signature
The flat API drives signing as a one-shot state machine: open a process on the source file, configure it, finish to an output file, read the result code. The sequence below produces a PAdES-B-B signature with SHA-256 — and deliberately reserves extra space inside the /Contents placeholder, which is the one line you cannot retrofit later if a timestamp ever needs to be added to this signature.
var
Pdf: TPDFlib;
SignId: Integer;
begin
Pdf := TPDFlib.Create;
try
SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
if SignId = 0 then
raise Exception.Create('cannot open source PDF');
Pdf.SetSignProcessField(SignId, 'Sig1');
Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
Pdf.SetSignProcessDigestAlgorithm(SignId, 2); // SHA-256
Pdf.SetSignProcessReserveContentsBytes(SignId, 8192); // room for a timestamp later
Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
if Pdf.GetSignProcessResult(SignId) <> 1 then
raise Exception.CreateFmt('signing failed, code %d',
[Pdf.GetSignProcessResult(SignId)]);
Pdf.ReleaseSignProcess(SignId);
finally
Pdf.Free;
end;
end;
NewSignProcessFromFile returns 0 when the source cannot be opened at all. After that, GetSignProcessResult separates the failure modes that actually occur in production: 4 means a wrong PDF password, 7 a wrong PFX password, 9 a certificate file with no private key, 10 an unwritable output path, 11 a failure while applying the signature bytes. Logging the numeric code next to the input file name converts a vague support ticket into a one-minute diagnosis.
Adding the RFC 3161 timestamp the library will not fetch for you
PDFlibPas ships no TSA client, and that is a deliberate boundary rather than a gap. The library computes the hash the timestamp authority must countersign and re-embeds the augmented CMS afterwards; the HTTP exchange and the CMS surgery in between belong to the caller. There is a hard technical reason for the split: the Windows CryptoAPI control that nominally adds unsigned attributes, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, fails with CRYPT_E_INVALID_INDEX on the detached SignedData layout PAdES uses, so the enhanced CMS must come from a CMS encoder under your own control — no library can quietly do it with one system call.
var
Pdf: TPDFlib;
StsId: Integer;
HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
Pdf := TPDFlib.Create;
try
StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
// both calls below are application code: an HTTP POST to your TSA,
// and a CMS re-encode that attaches the token as an unsigned attribute
TstDer := RequestTimeStampToken(HashHex);
TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
raise Exception.Create('timestamp embedding failed');
Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
finally
Pdf.Free;
end;
end;
Watch the result codes here: 12 means the named signature field does not exist, 11 that the existing CMS could not be parsed, and 13 that the augmented CMS no longer fits the reserved /Contents placeholder. Code 13 is the one that hurts, because the only fix is re-signing: a typical timestamp token with its certificate chain runs 4 to 6 KB, and the 8192-byte reservation made during the B-B step exists precisely so this step has room to land.
Validation starts at the ByteRange, not the certificate chain
A green check mark in a viewer is a trust decision against that machine's certificate store, not a structural verdict about the file. Programmatic validation should begin lower, with the question incremental updates make subtle: which bytes does each signature actually cover? Every enhancement discussed here — second signatures, DSS dictionaries, document timestamps — arrives via incremental update, and each update appends bytes outside the earlier signature's /ByteRange. Those appended bytes are legitimate, but a validator must classify them against the document's modification policy; the per-field DocMDP level is readable with GetSignatureDocMDPLevelByName.
var
Doc: TPDFlibSignDoc;
Names: TStringList;
I: Integer;
B0, B1, B2, B3, FileSize: Int64;
begin
FileSize := TFile.GetSize('invoice-bt.pdf'); // before Open: SignDoc holds a share lock
Doc := TPDFlibSignDoc.Create;
try
if not Doc.Open('invoice-bt.pdf', '', False) then
raise Exception.Create('cannot open for audit');
Names := TStringList.Create;
try
Doc.GetSignatureFieldNames(Names);
for I := 0 to Names.Count - 1 do
if Doc.GetSignatureValueObjNum(Names[I]) > 0 then // >0 means actually signed
begin
B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
if (B0 = 0) and (B2 + B3 = FileSize) then
Writeln(Names[I], ': covers the file to EOF')
else
Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
end;
finally
Names.Free;
end;
Doc.Close;
finally
Doc.Free;
end;
end;
Two traps live in this audit path. TPDFlibSignDoc.Open holds the file with an exclusive share lock, so a validator that also wants to hash raw file bytes for CMS verification must read the file into memory before opening it for audit — the order matters. And the flat-API counterpart GetSignProcessByteRange returns Integer while the underlying offsets are Int64: past 2 GB the flat call truncates silently, which is why this example pulls offsets through the audit class instead. Note also what is deliberately absent from the flat layer: there is no VerifySignature wrapper at all. Cryptographic verdicts come from the class-level TPDFlibSignatureVerifier, which returns vsValid, vsInvalid, or vsUnknown, or from an external validator your compliance policy already trusts.
Long-term validation: DSS, VRI, and the document timestamp
PAdES-B-LT exists because revocation infrastructure is mortal. ETSI EN 319 142-1 §5.4.2.2 specifies the Document Security Store: a document-level dictionary carrying certificates, CRLs, and OCSP responses, optionally indexed per signature through VRI entries keyed by the hash of each signature's /Contents. The PDFlibPas flow mirrors the timestamp design: NewPAdESDSSProcessFromFile opens the process; AddPAdESDSSCertificate, AddPAdESDSSCRL, and AddPAdESDSSOCSP accept DER blobs; AddPAdESDSSVRI binds selected material to one signature; EndPAdESDSSProcessToFile writes everything as an incremental update. Fetching the revocation material — and judging whether it is fresh enough to embed — remains the caller's responsibility; the library guarantees the dictionaries are structurally conformant, not that your OCSP responder told the truth.
The archival endpoint, B-LTA, adds a document timestamp: a separate signature field whose type is DocTimeStamp rather than Sig, produced through SetSignProcessDocTimeStamp with a reserved signature length. For readers that predate these structures, TPDFlibSignDoc.EnsurePAdESExtensions records the ESIC developer extension in the document catalog, announcing that the file uses ETSI-defined features.
Frequently asked questions
Why does Acrobat say "validity unknown" when the PAdES structure is correct?
Because trust and structure are independent. The viewer cannot chain the signer to a root it trusts on that machine — routine with private CAs and test certificates — while the ByteRange audit and CMS verification pass at the same moment. Distribute the root certificate properly, or evaluate against the EU trusted lists when eIDAS qualification is the actual goal.
Can a timestamp be added to a signature that reserved no extra Contents space?
Usually not. The augmented CMS must fit the original placeholder, and a default-sized placeholder fits the original signature snugly. Expect result code 13, and plan to re-sign with SetSignProcessReserveContentsBytes from the start.
Does a document timestamp replace the signature timestamp?
No. The signature timestamp proves when one signature existed; the document timestamp protects the whole file including its DSS evidence, and is the element that gets renewed over decades. Archival profiles end up carrying both.
For the audit-side perspective — enumerating signature fields across a corpus, dumping ByteRange layouts, and reading DocMDP levels in bulk — see the companion piece on the compliance and signing workbench. Signed documents that must also satisfy archival policy belong in the workflow described in PDF/A and PDF/UA preflight in Delphi. Full API documentation and evaluation downloads are on the losLab PDF Library for Delphi product page.