Deciding that a PDF signature is qualified under eIDAS means answering a question that has nothing to do with cryptography: was the certificate issued by a trust service that a member state listed as qualified, at the moment the signature was made. The answer lives in a trusted list, an XML document published per territory, and the whole value of that document depends on its authenticity. So the PDFium component refuses to look inside one until somebody has vouched for it. TPdfEuropeanTrustedList.ParseAuthenticated hands the complete raw bytes to a caller-supplied IPdfTrustedListAuthenticator before it parses a single service, and it creates a snapshot only if that authenticator explicitly passes
That ordering is the design. Everything else in this feature follows from it, including the parts that look inconvenient
Parsed is not trusted
A trusted list that parses cleanly tells you the XML is well formed. It tells you nothing about who wrote it. Since the list is what your entire qualified-status decision rests on, accepting one because it parsed would make the decision meaningless: an attacker who can substitute the list can declare their own certificate authority qualified
The same reasoning applies to caching, and this is the trap worth naming. The snapshot cache stores the original XML together with a SHA-256 digest, and it would be easy to treat a matching digest on load as proof the list is genuine. It is not. A digest computed by the same process that stored the file, with no key involved, verifies only that the bytes have not changed since you wrote them; if the list was fraudulent when it was cached, the digest confirms it is the same fraudulent list. So loading a cached snapshot runs through the same authenticator as parsing a fresh one. Integrity and authenticity are different properties and only one of them needs a key
uses
FPdfTrustedList;
type
TListAuthenticator = class(TInterfacedObject, IPdfTrustedListAuthenticator)
public
function Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
end;
function TListAuthenticator.Authenticate(const XmlData: TBytes;
out AuthenticationDetails: string): Boolean;
begin
// Your policy lives here: verify the XMLDSIG enveloped signature
// against the list-signing certificate you pinned out of band,
// and describe what you checked for the audit trail
Result := VerifyEnvelopedXmlSignature(XmlData, FPinnedListSigner);
if Result then
AuthenticationDetails := 'XMLDSIG verified against pinned LOTL signer';
end;
var
List: TPdfEuropeanTrustedList;
Cache: TFileStream;
begin
List := TPdfEuropeanTrustedList.ParseAuthenticated(RawXml,
TListAuthenticator.Create, TPdfTrustedListOptions.Default);
// Snapshot exists only because the authenticator said yes
Cache := TFileStream.Create('tl-de.snapshot', fmCreate);
try
List.SaveCache(Cache);
finally
Cache.Free;
end;
end;
The validator does not own network policy
A PAdES validator has no business deciding how to reach the list of trusted lists, whether to go through a proxy, how often to retry, or what to do when a territory is unreachable. Those are application and deployment decisions, and in regulated environments they are frequently audited. So updates arrive through IPdfTrustedListSource, which is handed a URI and a byte cap and returns bytes
What the component does enforce are the invariants that make an update an update rather than a substitution. Update requires that the territory is unchanged, that the sequence number strictly increases, and that the issue time does not move backwards. Those three checks defeat the most obvious downgrade attacks: replaying an older list that still lists a since-withdrawn service, or swapping in another territory's list whose services you never intended to trust
Parser limits, and no DTD at all
TPdfTrustedListOptions caps the XML size, the token count, the nesting depth, the number of services, the number of certificates and the size of an individual certificate, with a Default class function providing usable values. Trusted lists are published documents of predictable size, so bounds are cheap to set and there is no legitimate list that needs to exceed them
Separately and unconditionally, the parser rejects DTD and entity declarations. That closes both the entity-expansion denial of service and the external-entity disclosure route in one refusal, and it costs nothing because trusted lists do not use entities. Any XML parser reachable from untrusted input should be configured this way; the difference here is that the refusal is not configurable, so it cannot be switched off by a well-meaning option change
Qualified status is recorded next to chain trust, not merged into it
The evaluation side is deliberately separate. TPadesTrustValidationOptions.QualifiedTrustEvaluator takes an IPdfQualifiedTrustEvaluator, which the trusted-list snapshot implements. During validation the evaluator receives the leaf certificate, the chain and a validation time, matches service certificates by exact DER comparison against the signer and chain, combines the service status, the service type identifier and the qualifier URIs at that point in time, and returns an evaluation record
The result lands in two places on each signature: QualifiedTrustStatus as a coarse status, and QualifiedTrust as the full evaluation with territory, provider name, service name, type identifier, status and status starting time. What it does not do is change CertificateTrustStatus. System chain trust and qualified status answer different questions, and a report that collapses them cannot distinguish "trusted but not qualified" from "qualified but the chain does not validate", both of which are real and both of which need different handling
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
I: Integer;
begin
Options := TPadesTrustValidationOptions.Default;
Options.CheckRevocation := True;
Options.QualifiedTrustEvaluator := List; // the authenticated snapshot
Options.QualifiedValidationTime := SigningTime; // not Now
Report := Pdf.ValidatePadesTrust(Options);
for I := 0 to High(Report.Signatures) do
if Report.Signatures[I].QualifiedTrustStatus = pcsValid then
Writeln(Format('signature %d qualified by %s / %s (%s)',
[I, Report.Signatures[I].QualifiedTrust.Territory,
Report.Signatures[I].QualifiedTrust.ProviderName,
Report.Signatures[I].QualifiedTrust.ServiceName]))
else if Report.Signatures[I].QualifiedTrustStatus = pcsIndeterminate then
// No matching service, or the snapshot cannot answer for this time
Writeln(Format('signature %d: qualified status undetermined', [I]));
end;
Why the validation time is not now
Because qualification is a property of a moment. A trust service can be granted qualified status, later have it withdrawn, and later still be reinstated, and each of those transitions carries a starting time in the list. A signature made while the service was qualified stays qualified afterwards; a signature made before the grant does not become qualified retroactively. Evaluating against the current time therefore gives the wrong answer in both directions
The list carries what is needed for this: each service record has a status starting time and a flag distinguishing historical entries from current ones, and the evaluator combines them against the time you supply. In practice that time comes from a trusted timestamp on the signature rather than from the signing time claimed in the CMS, which is why long-term validation material matters even for a question that looks like a policy lookup; the timestamp and DSS side is covered in the long-term signature article
What you still have to build
Three things, and none of them belong in a PDF library. The authenticator, meaning actual XMLDSIG verification against a list-signing certificate you obtained through a channel you trust. The fetch policy, meaning how and how often you refresh, and what your application does when a refresh fails. And the territorial scope, meaning which lists you carry at all, which is a business decision about which member states your counterparties sign in
What you get from the component is the part that is easy to get subtly wrong: authenticate-before-parse ordering, bounded and entity-free XML parsing, monotonic update invariants, exact-DER service matching, historical status evaluation, and a result that stays separate from ordinary chain trust. If your immediate problem is more basic, that a validator rejects a signature you believe is fine, the usual causes are cataloged in why validators reject PAdES signatures, and the signature inspection surface is described in inspecting signatures and PAdES levels. Component capabilities are listed on the PDFium Delphi component product page