M27 — Long-term signatures and signature validation
State: to do — Depends on: M26 — Cryptography, time and trust anchors placed by ADR 41; everything M04 established without cryptography is its input
Goal
Keep signatures verifiable for years — PAdES B-LT and B-LTA, the document security store, archive time stamps renewed before they lapse, and long-term material added to signatures received from others before they are archived — and give a verdict on every signature a document carries, in the form of an ETSI TS 119 102-2 validation report, offline by default, that agrees with EU DSS.
A contract signed today must still prove, in ten years, who signed it and that it has not changed since, long after the signer's certificate has expired and its authority's revocation service has gone; a bailiff's case file must hold the evidence for a court that asks later. And a law firm receives signed documents every day — a counterparty's DocuSign envelope, a notary's qualified seal, a gazette's certified extract — and needs to know whether each signature holds, at which PAdES level, against which trusted list, and whether anything changed after it: the last question is M04's, answered without cryptography; this milestone answers the others. The failures it exists to prevent are the ordinary ones: a verdict of valid for a signature whose chain nobody built, because the platform's chain engine went online, or failed quietly, or behaved differently on Windows; a signature called invalid because its certificate expired, when an archive time stamp proves it was signed before; a revocation check that fetched a CRL behind the caller's back; a trusted list accepted without verifying its own signature; a report whose verdict changes from one run to the next because the validation time was read from the clock; an archive time stamp that lapsed because nobody was told it was due.
Scope
In:
- validation of every signature and document time stamp a document carries, M04's model as its input — revision,
coverage, permissions, the changes after it — never re-derived:
- the CMS parsed by the framework (
SignedCms), the integrity — themessage-digestover the byte range, the signature over the signed attributes, or over the digest for the legacy forms — and the signing certificate identified by its ESS attribute; - path building and certificate validation of our own, deterministic, against the caller's trust anchors or
the EU trusted lists — never the platform's
X509Chain; - revocation from the document security store first, then the CMS's own revocation data, then online only through clients the caller supplies;
- time stamps — signature, content, document and archive —, and the proofs of existence they give;
- the PAdES level reached (B-B, B-T, B-LT, B-LTA) or the PKCS #7 level for
adbe.pkcs7.detached; - the legacy
adbe.pkcs7.sha1andadbe.x509.rsa_sha1signatures and usage-rights signatures, for integrity and chain; - algorithms: RSA PKCS #1 v1.5 and RSASSA-PSS, ECDSA on NIST and Brainpool curves, EdDSA (Ed25519, Ed448), SHA-2, SHA-3 and SHAKE256 (ISO/TS 32001 and 32002), each judged by the policy's cryptographic constraints;
- the ISO/TS 32004 MAC attached to a signature, which M16 listed and left here;
- the CMS parsed by the framework (
- the ETSI EN 319 102-1 procedures — basic validation, validation with time, validation with long-term material and past signature validation — with their indications and sub-indications, under an immutable validation policy whose defaults follow EU DSS's default policy wherever this milestone decides them;
- the report in the XML of ETSI TS 119 102-2 at a pinned version, and as JSON with a published schema, deterministic for given inputs;
- trust: anchors from the caller, and the EU List of Trusted Lists with the national lists it points to
(ETSI TS 119 612), parsed and verified — the LOTL against the signing certificates the caller supplies from the
Official Journal — with each service's status history; qualification of a signature or seal as ETSI
TS 119 615 determines it, as EU DSS reports it; in a satellite of its own,
AdCodicem.Pdf.Signing.TrustedLists; - B-LT and B-LTA: the document security store with certificates, OCSP responses and CRLs,
/VRIentries on request, an archive document time stamp after it; renewal — the store completed for the last time stamp and a new one added — and a report of when renewal falls due; - long-term material added to received signatures before they are archived, within what M04 classes as a security-store change, allowed under any certification;
- network off by default: OCSP, CRLs, the issuers' certificates named by
caIssuersand the trusted lists are read from the file and from what the caller supplies; each is fetched only through a client the caller gives; the validation time is the caller's; - the command-line tool's
verifyverb, andsign --level b-lt|b-ltaandsign --renew.
Out, explicitly:
- coverage, revisions and changes after a signature — M04's, reused as they are; M27 never re-derives them;
- signing at B-B and B-T — M26; post-quantum signatures — not planned until the PDF specification text exists;
- AdES formats other than PAdES — XAdES, JAdES, ASiC, detached CAdES files — not planned; XML signatures are verified only where a trusted list needs it;
- verifying the legacy object digests of a
/Reference(/DigestMethod /MD5,/DigestValue,/DigestLocation, which M04 recorded) — reported and left unverified: ISO 32000-2 deprecates them, their canonicalization was never specified openly, and no referee verifies them; - XFA's XML signatures and dynamic XFA — never (ADR 37);
- the meaning of usage rights — M16's; M27 verifies the usage-rights signature like any other and trusts Adobe's root only if the caller supplies it;
- seeing whether a change after a signature covers signed content — M24's visual comparison on M25's rasters; the report names M04's changes, and a caller may attach a visual comparison to it;
- Adobe's Approved Trust List read from its own format, evidence records (RFC 4998), and hosting validation as a service — not planned; a caller may pass any list's certificates as anchors;
- legal conclusions: the report says what the procedures of EN 319 102-1 establish under a policy; whether a signature is binding is a court's question, and the documentation says so.
Design
Where it lives
| Package | Holds | Depends on |
|---|---|---|
AdCodicem.Pdf.Signing | AdCodicem.Pdf.Signing.Validation (the validator, the procedures, the policy, the report), .Revocation (OCSP and CRL parsing, the clients), .LongTerm (the security store, archive time stamps, renewal) | The core, System.Security.Cryptography.Pkcs (ADR 41) |
AdCodicem.Pdf.Signing.TrustedLists (new) | The LOTL and national lists: parsing, their XML signatures, services and history, qualification | AdCodicem.Pdf.Signing, System.Security.Cryptography.Xml |
Trusted lists are XML signed with XAdES; verifying them needs SignedXml, which lives in the out-of-band
System.Security.Cryptography.Xml package. A satellite of its own keeps it away from the caller who brings its own
anchors, and from the Signing package's AOT profile if SignedXml proves to trim badly. The package, and the
parsing below, are recorded in an ADR amending ADR 41 before slice 3 is written.
What the framework parses and what we do. SignedCms parses CMS, Rfc3161TimestampToken parses tokens,
X509Certificate2 parses certificates. The framework has no public parser for OCSP responses or CRLs, and its
X509Chain can be given neither: it fetches revocation on its own, behaves differently on each operating system,
and cannot be told to use a security store — against invariants 6 and 12 at once. So the satellite parses
BasicOCSPResponse (RFC 6960) and CertificateList (RFC 5280) itself, with AsnReader — the framework's hardened
reader — under explicit bounds, fuzzed from the day they are written, and builds and validates paths itself. That
is the one place this project writes security-sensitive parsing of hostile input, which ADR 41 had hoped to avoid;
the amending ADR says why it cannot be avoided and how it is contained.
The validator
PdfSignatureValidator ValidateAsync(document, PdfSignatureValidationContext, cancellationToken, progress)
-> PdfSignatureValidationReport; stateless and thread-safe
PdfSignatureValidationContext immutable: the validation time (required, or a TimeProvider passed explicitly),
the trust anchors, a trusted-list snapshot, the policy, extra certificates, CRLs and
OCSP responses the caller supplies, and the online clients — none by default
PdfTrustAnchors certificates the caller trusts, each for signatures, for time stamps, or both
PdfSignatureValidationPolicy immutable: cryptographic constraints (algorithms, key sizes and the dates they
expire), revocation freshness, what each M04 change class and permission breach
means, the constraints on certificates; PdfSignatureValidationPolicy.Default;
loadable from JSON
PdfSignatureValidationReport per signature: M04's info (field, kind, revision, coverage, changes after it), the
indication and sub-indication, the constraints evaluated, the chain, the revocation
data and the time stamps used with their proofs of existence, the level reached, the
qualification when a trusted list decided it, the diagnostics;
WriteEtsiXml(stream), WriteJson(stream), a Markdown summary
The names say signature throughout: M20's PdfValidationPolicy and ValidationContext are the core rule engine's,
and a caller who validates a document's conformance and its signatures imports both namespaces.
The validator opens nothing: the caller opens the document with its PdfReaderOptions — and its password; a
signature over an encrypted document is validated once M16 has decrypted it, /Contents being clear.
The procedures
EN 319 102-1's building blocks, in the order its validation processes run them — each clause checked against the text of the pinned version before its slice is written:
| Block | What it checks here |
|---|---|
| Format checking | M04's coverage (WholeFile, WholeRevision; Partial, Malformed and Placeholder fail with FORMAT_FAILURE), the sub-filter and the CMS agreeing, the signed attributes the format requires |
| Identification of the signing certificate | ESS signing-certificate or -v2: the certificate whose hash and issuer-serial it names, among the CMS's, the security store's and the caller's certificates; else NO_SIGNING_CERTIFICATE_FOUND |
| Validation context initialization | The policy, the anchors, the validation time |
| Cryptographic verification | The digest over the byte range against message-digest (HASH_FAILURE); the signature over the DER of the signed attributes (SIG_CRYPTO_FAILURE); for adbe.pkcs7.sha1, the encapsulated SHA-1 digest; for adbe.x509.rsa_sha1, the PKCS #1 value in /Contents over the digest, with /Cert as the chain |
| X.509 certificate validation | Paths built and validated (below), at the validation time or at a proof of existence |
| Revocation freshness | Each revocation datum's issuance against the policy's freshness and the time it must speak for |
| Signature acceptance | The policy's constraints: algorithms at their dates, key sizes, the signing certificate's key usage, M04's changes after the signature and the permissions in force |
Then basic validation, validation with time — the best signature time from a signature or document time
stamp, each token validated in its own right —, and past signature validation with proofs of existence from
archive time stamps, so that a certificate that expired or was revoked after a proven time does not fail a
signature made before it. Indications are TOTAL-PASSED, TOTAL-FAILED and INDETERMINATE, with the standard's
sub-indications; nothing outside them, and never a pass where the procedure could not decide.
M04's changes in the verdict. A change a certification or a lock forbids is a signature-acceptance failure under
the default policy; an Other change after an approval signature no permission restricts is reported against the
signature and left to the policy — the default follows what EU DSS's default policy decides for each, and the
policy file records every place the two differ.
Paths
- Candidates: the CMS's certificates, the security store's
/Certs, each time-stamp token's, the caller's extras, the trusted list's service certificates, and — online, only with a fetcher —caIssuersfrom the authority information access extension. - Building: depth-first from the signing certificate toward an anchor, an issuer chosen by name (compared as RFC 5280 §7.1 prescribes, not byte for byte) and, where present, by the authority key identifier; a visited set on subject and key, so that cross-certificates cannot loop; every path found is tried in a deterministic order until one validates, and the report says which.
- Validation (RFC 5280 §6, the subset business signatures meet): each signature verified with its issuer's
key over the
tbsCertificatebytes; validity at the time in force; basic constraints and path length; key usage for certificate signing; unknown critical extensions fail the path; name constraints on directory names and e-mail addresses applied, any other form reported and the path leftINDETERMINATE; certificate policies read and reported, not enforced unless the policy file asks.
Revocation
- Sources, in order: the security store (
/DSSand/VRI, read lazily through the core), the CMS's own revocation data (Adobe'sadbe-revocationInfoArchivalsigned attribute, RFC 3161 tokens' certificates), what the caller supplies, then — only throughIPdfRevocationClientinstances the caller gives — OCSP and CRL distribution points. - OCSP: the response's signature by the certificate's issuer, or by a responder the issuer delegated with the
OCSP-signing extended key usage (its own revocation skipped only under
id-pkix-ocsp-nocheck); the certificate identifier matched by the hashes it names;thisUpdateandnextUpdateagainst the time it must speak for. - CRLs: the issuer's signature, the validity window, the distribution point and scope, the revoked certificates walked as a stream over the encoded list — no allocation per entry — for the serial in question, with its reason and date; delta and indirect CRLs reported and not relied on in the first slice that meets them.
- A revocation that happened after a proof of existence of the signature does not fail it; one before, or with no
proof, does —
REVOKED,REVOKED_NO_POE,REVOKED_CA_NO_POEas the standard names them. No revocation data, offline:INDETERMINATEwith the standard's sub-indication, and the certificate named — never a pass.
Algorithms
| Algorithm | Verified with | When the platform lacks it |
|---|---|---|
| RSA PKCS #1 v1.5, RSASSA-PSS | The BCL's RSA | — |
| ECDSA on P-256, P-384, P-521 | The BCL's ECDsa | — |
| ECDSA on Brainpool curves | The BCL's ECDsa where ECCurve names the curve | INDETERMINATE, verify.algorithm-unavailable |
| SHA3-256, 384, 512, SHAKE256 | The BCL's SHA3_* and Shake256 where IsSupported | INDETERMINATE, verify.algorithm-unavailable |
| Ed25519, Ed448 | Decided by the ADR of slice 6: a managed, verification-only implementation in the satellite — no secret is involved, so constant time is not at stake, and RFC 8032's and Wycheproof's vectors test it — or a dependency that provides both | The same outcome as SHA-3 until the ADR is taken |
The policy's cryptographic constraints give each algorithm and key size the date after which it no longer
counts — the catalog EU DSS's default policy encodes from ETSI TS 119 312 — and a signature whose algorithm expired
before any proof of existence is INDETERMINATE with CRYPTO_CONSTRAINTS_FAILURE_NO_POE, as SHA-1 and 1024-bit
RSA signatures in the corpus will show.
Trusted lists
- Offline by default:
TrustedListSnapshot.Load(directory, lotlSigningCertificates)reads a LOTL and the national lists a caller downloaded, verifies the LOTL's signature against the certificates the caller took from the Official Journal publication, and each national list's against the certificates the LOTL gives for it. Online, anITrustedListFetcherthe caller supplies downloads them; caching is the caller's. - Parsing hostile XML:
XmlReaderwith DTDs prohibited, no resolver, a size bound;SignedXmlrestricted to one enveloped signature over the whole document with exclusive canonicalization, its reference required to be the document itself — the shapes of XML signature wrapping refused. - Services: each trust service's type (
CA/QC,TSA/QTSTand the others), its status history, its extensions (qualifiers, additional service information) resolved at the time a signature needs them; anchors derived from services, for signatures and for time stamps separately. - Qualification: the signing certificate's QC statements, the list's qualifiers and the service's status at the
signing time, combined as ETSI TS 119 615 says, give
QESig,QESeal,AdESig-QC,AdESeal-QC, the plain advanced forms orN/A— as EU DSS names them — with every input that decided it. - A list past its
NextUpdateat the validation time is used and reported stale (trust-list.stale); one whose signature fails is not used (trust-list.signature-invalid), and every service it held is absent from the report.
Long-term material
PdfLongTermSigning AddValidationDataAsync(document, output, context, options) -> the store in a new revision (B-LT)
AddArchiveTimestampAsync(document, output, timestampClient, options) -> a document time stamp (B-LTA)
RenewAsync(document, output, context, timestampClient, options)
RenewalDue(report) -> the date by which the last archive time stamp must be renewed, and why
- The store (ISO 32000-2 §12.8.4.3):
/DSSin the catalog with/Certs,/OCSPsand/CRLsarrays of streams, each object written once, deduplicated by digest;/VRIentries on request, keyed by the upper-case hexadecimal SHA-1 of the signature — ISO 32000-2 §12.8.4.3's words, read as the/Contentsbytes —, with/TUfrom the caller's time. An existing store is extended, never pruned: its objects are kept and the catalog's/DSSredefined with the union, which M04 classesSecurityStore, allowed under any certification. - What goes in: for each signature and each time stamp, the chain to its anchor and the revocation data for each
certificate but the anchor — the time-stamp authority's included —, as the validator found it. What could not be
found is reported per certificate (
ltv.material-missing) and the level reached is what the material allows — never announced higher. - Archive time stamps: a document time stamp over the whole file, the store included, through M26's
TimestampAsync. In 1.7 output the/ESICextension is declared as M26 declares it (ADR 40). - Renewal: the store completed for the last time stamp's chain, then a new document time stamp, before the last
one's certificate expires or its algorithm leaves the policy;
RenewalDuereports the date and the reason, so that a caller's job knows when to run. - Memory: the store's streams are written through M03's writer as they are produced — a large CRL is copied, not held —; the revision that holds them needs no reserved region, and only the time stamp's revision does.
Bounds, classified (invariant 12, ADR 34)
Documents, their stores and their CMS are read through the core, whose guards apply. What the satellite parses besides:
| Bound | Kind | Why |
|---|---|---|
| A certification path's length | Option, PdfSignatureValidationPolicy.MaxPathLength (proposed 16), reported verify.path-too-long, CERTIFICATE_CHAIN_GENERAL_FAILURE | A valid path is rarely over five; a hostile store of cross-certificates can offer exponentially many |
| Paths tried per certificate | Option, MaxPathCandidates (proposed 1,000), the same code | Depth-first search with a visited set is still exponential in the worst case |
| An OCSP response's and a CRL's length, fetched or supplied | Option, MaxRevocationDataLength (proposed 64 MB); inside the file, the reader's MaxDecodedStreamLength | Some authorities' CRLs are tens of megabytes, validly |
| A CRL's entries | None needed: streamed, never materialized | Memory follows one entry |
| A trusted list's length | Option, TrustedListSnapshot.MaxListLength (proposed 32 MB) | National lists run to a few megabytes; a hostile server can send more |
| Lists followed from the LOTL | Internal: the LOTL's pointers only, never a national list's pointers onward | The standard's topology is two levels; following further is how a loop starts |
| Online fetches per validation | Option, MaxFetches | A hostile document can name thousands of distribution points |
| ASN.1 nesting in OCSP and CRL structures | The framework's AsnReader, under the structure's fixed depth | The structures have a fixed shape; anything deeper is malformed |
Diagnostics
| Code | Severity | Meaning |
|---|---|---|
verify.algorithm-unavailable | Warning | An algorithm the platform cannot run: the signature is INDETERMINATE, not failed |
verify.revocation-offline | Information | Revocation data absent and no client supplied; the certificate named |
verify.path-too-long | Warning | A path or search bound reached; the option named |
verify.legacy-digest-unverified | Information | A /Reference object digest recorded by M04 and not verified |
verify.policy-difference | Information | A constraint where our default policy departs from EU DSS's, named in the policy file |
trust-list.signature-invalid | Error | A list whose signature does not verify; its services unused |
trust-list.stale | Warning | A list past its NextUpdate at the validation time |
ltv.material-missing | Warning | A certificate for which no chain or revocation data could be found |
ltv.renewal-due | Information | The date by which the last archive time stamp must be renewed |
The command-line tool
verify FILE [--trust ANCHORS.pem] [--trusted-lists DIR --lotl-certs OJ.pem] [--time 2026-09-27T10:00:00Z | --time now] [--policy POLICY.json] [--online] [--report etsi.xml | --json], with M06's exit codes: 0 only when every
signature is TOTAL-PASSED, 1 when one is TOTAL-FAILED, and 5 — the code M20 adds for a verdict that waits — when
none failed and one is INDETERMINATE; sign FILE --level b-lt|b-lta and sign FILE --renew, each with --online
or with --revocation DIR for material the caller gathered.
Slices
Each slice ends on a green commit, with its codes documented and its benchmark, if it has one, recorded in
docs/status.md. Every referee runs in a container
(ADR 27); the test PKI is M26's.
- Integrity and the report's skeleton. Delivers format checking over M04's coverage,
SignedCmsparsing of every corpus signature, the digest and signature checks for every sub-filter, the report with its first indications, the ETSI XML and JSON writers,expect.signatures[].etsiin the manifest's schema. Proved by unit tests (a signed-attribute set out of DER order, as node-signpdf writes it; no signed attributes, as Foxit and the BOE write; a/Contentszero-padded; a signature value with one bit flipped); integration: on every signed corpus document, intact where pyHanko finds it intact andopenssl cms -verifyagrees; every report valid against the ETSI schema withxmllint. Leaves chains. - The signing certificate and paths against the caller's anchors. Delivers identification, the path builder,
RFC 5280 validation without revocation,
PdfTrustAnchors. Proved by unit tests over the test PKI (a cross-certified pair, an expired intermediate, a critical unknown extension, name constraints, a loop of cross-certificates atMaxPathCandidates); integration: the path pyHanko's certvalidator builds, for every signed corpus document with its own root trusted. Leaves revocation. - Revocation offline, and the ADR. Delivers the ADR amending ADR 41; the OCSP and CRL parsers, bounded and
fuzzed; the store,
/VRIand Adobe's archival attribute as sources; freshness; the revocation sub-indications. Proved by unit tests (a delegated responder with and withoutnocheck, a CRL of a million entries walked in constant memory, a response for another certificate, every field malformed); integration: every OCSP response and CRL in the corpus's stores parsed to the fieldsopenssl ocsp -resp_textandopenssl crl -textprint; the test PKI's revoked certificates reported revoked at the right date. Leaves time. - Time stamps and proofs of existence. Delivers token validation for signature, content, document and archive time stamps; best signature time; validation with time. Proved by unit tests (a token over the wrong imprint, a TSA without the time-stamping usage, tokens without certificates as the DIIA files carry them); integration: each corpus time stamp's verdict and each signature's best signature time equal DSS's. Leaves past validation.
- Past signature validation and levels. Delivers validation with long-term material and past signature validation; the PAdES and PKCS #7 level reached. Proved by the expired and renewed corpus signatures, and the DIIA B-B, B-LT and B-LTA trio, each at the level DSS gives. Leaves algorithms.
- The policy and the algorithms. Delivers
PdfSignatureValidationPolicywith its JSON form and its default beside DSS's, the cryptographic constraints, Brainpool, SHA-3, and EdDSA after the ADR this slice takes; the ISO/TS 32004 MAC. Proved by unit tests (RFC 8032 and Wycheproof vectors for EdDSA, Wycheproof for ECDSA and PSS; SHA-3 forced unavailable givingINDETERMINATE); integration: the generated SHA-3, EdDSA and Brainpool signatures (below) as DSS and pyHanko judge them; SHA-1 and 1024-bit RSA signatures in the corpus as DSS judges them. Leaves the lists. - The trusted lists. Delivers the new satellite, the snapshot, the restricted XML signature verification, the services and their history, anchors per service type, the fetcher. Proved by unit tests (a list with a wrapped signature, an external entity, a service whose status changed on a date, a stale list); integration: the pinned snapshot verifies; each qualified signature of the corpus chains to the service DSS names, with the same status at its signing time. Leaves qualification.
- Qualification. Delivers the TS 119 615 determination. Proved by each qualified signature and seal of the
corpus qualified as DSS qualifies it, and the test PKI's signatures
N/A. Leaves the network. - Online, on request. Delivers
HttpOcspClient,HttpCrlClient, thecaIssuersfetcher and the list fetcher over the caller'sHttpClient,MaxFetches. Proved by the test PKI's services in a container, and every offline test run again with a handler that fails on any request — the default never opens a socket. Leaves writing. - B-LT, B-LTA and renewal. Delivers
PdfLongTermSigning, the store,/VRI, archive time stamps, renewal,RenewalDue, thesignoptions. Proved by the long-term rows below: DSS reportsPAdES-BASELINE-LTand-LTAand passes them, M04 classes each revision, and every earlier signature is intact. Leaves received signatures. - Long-term material for received signatures. Delivers
AddValidationDataAsyncon documents we did not sign, with the material in their CMS, the caller's, or fetched. Proved by the received-signature rows below. Leaves the whole. - Agreement, budgets, the tool and the documentation. Delivers
expect.signatures[].dss— indication, sub-indication, format, qualification, configuration, validation time — written bybuild_corpus.pyrunning DSS, and a disagreement field beside it, as M20 gives veraPDF's verdicts one; every disagreement fixed or recorded there with its reason,SignatureValidationBenchmarkswithMemoryDiagnoser, theverifyverb, the documentation. Proved by the agreement and budget rows and a greenRemote corpusrun recorded indocs/status.mdwith its date.
Tests required
Unit — tests/AdCodicem.Pdf.Tests, under Signing/Validation/ and Signing/LongTerm/:
- Integrity: every sub-filter; each digest and signature algorithm; signed attributes out of order, absent, or
duplicated; a
message-digestof the wrong length; a certificate with a negative serial, as Tecxoft's has. - Identification: v1 and v2 attributes, an attribute naming no certificate present, two candidates with one key.
- Paths: every RFC 5280 check the design lists, each failing alone; loops, exponential cross-certification, the bounds and their codes; names compared under RFC 5280's rules.
- Revocation: every OCSP and CRL field malformed; delegation; freshness at its boundary; revocation before and after a proof of existence.
- Time stamps: each rejection; nested time stamps over time stamps; a token whose TSA certificate expired before the archive time stamp that protects it.
- Procedures: each indication and sub-indication reached by a minimal fixture; the policy's JSON round trip.
- Trusted lists: signature wrapping, DTDs, entities, oversized lists, stale lists, a service withdrawn on a date.
- Long-term: the store extended and never pruned; deduplication;
/VRIkeys; memory flat with a 64 MB CRL. - Hostile: fuzzed CMS, OCSP, CRL and trusted-list inputs seeded from the corpus — each ends in a report or a typed exception within time and allocation budgets; the parsers join M23's nightly fuzzing campaign.
- Determinism: two validations give byte-identical reports; no clock read without a
TimeProviderpassed. - Cancellation and progress on validation and on writing the store, per M03's property.
Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container:
- EU DSS — M26's image, given the file, the trusted certificates, the trusted-list snapshot, the validation time and DSS's default policy; its ETSI report, simple report and detailed report read by the tests; also the writer of the manifest's expectations.
- pyHanko — validation with its certvalidator under the same anchors and time, fetching off; its difference analysis for the rows where changes follow a signature.
- OpenSSL —
cms -verify,ocsp -respin … -resp_text,crl -text,ts -reply -text, as independent parses. - poppler's
pdfsig— a third opinion on integrity for each signed corpus document. - xmllint — every ETSI report against TS 119 102-2's schema; every trusted list against TS 119 612's.
- Certomancer — the test PKI's OCSP responder, CRLs and time-stamp authority, for the online slice and for B-LT.
Acceptance conditions
"The signed corpus" is every committed and remote document with a signature, a document time stamp or a
usage-rights signature: the committed seven M04 names and the usage-rights forms; remote, the files under
remote/eu-dss/, remote/pdfcpu/, remote/pdfium-tests/, remote/boe/, the DocuSign, Adobe Sign and Yousign
files, remote/govinfo/us-code-2023-title42.pdf, remote/node-signpdf/openoffice21-node-signpdf-signed-twice.pdf,
the two JHOVE theses (remote/opf-format-corpus/jhove-hul-154-cairo-thesis-signed-etsi-cades.pdf, HelloSign;
jhove-hul-154-signnow-thesis-rsa-sha1-signed.pdf, SignNow's adbe.x509.rsa_sha1), the certified Canadian forms,
and M26's own signed outputs. "Both configurations" means the trusted-list snapshot alone, and the snapshot with
each file's own root trusted; "at the snapshot's time" means the validation time recorded with the snapshot.
| Documents | Behavior | Verified by |
|---|---|---|
| The signed corpus, in both configurations, at the snapshot's time | For each signature and time stamp, the main indication, the sub-indication and the signature format equal DSS's; each disagreement is fixed or recorded in the manifest (expect.signatures[].dss) with its reason, and none is recorded where we say TOTAL-PASSED and DSS does not | CorpusSignatureValidationTests.Verdicts_agree_with_eu_dss (new), DssValidationRefereeTests (new) |
| The signed corpus; its tampered copies — not in the corpus (below): a byte of covered content changed, one bit of a signature value flipped | Intact exactly where pyHanko and openssl cms -verify find it intact; the copies HASH_FAILURE and SIG_CRYPTO_FAILURE | CorpusSignatureValidationTests.Integrity_agrees_with_independent_verifiers (new) |
Legacy signatures: remote/pdfcpu/acrobat-web-capture8-x509-rsa-sha1-signed.pdf (adbe.x509.rsa_sha1, /Cert, a negative serial; once M23 closes #47), jhove-hul-154-signnow-thesis-rsa-sha1-signed.pdf, the BOE's adbe.pkcs7.sha1 seals (remote/boe/indesign-apdfl9-boe-law-2015-aeboe-seal.pdf, antenna-house-openpdf-boe-royal-decree-2026-aeboe-seal.pdf, remote/eu-dss/eboe-fnmt-boe-seal-pkcs7-sha1-then-anf-test-signature.pdf), Foxit's signatures without signed attributes | Integrity verified as OpenSSL verifies it; the verdict under the policy's constraints on SHA-1 and 1024-bit RSA equal to DSS's | CorpusSignatureValidationTests.Legacy_signatures_are_verified_and_judged_by_the_policy (new) |
Algorithms: RSASSA-PSS (remote/eu-dss/ghostscript-certipost-universign-mentana-four-revisions.pdf), ECDSA (neooffice-polysys-hu-two-seals-two-doc-timestamps.pdf; an ECDSA time stamp in the 2026 BOE decree), SHA-512 (word2010-cs-plugtest-pades-lta-two-doc-timestamps.pdf); the SHA-3, SHAKE256, Ed25519, Ed448 and Brainpool signatures — not in the corpus (below) | Each verified; each verdict equal to DSS's and pyHanko's; with SHA-3 forced unavailable, INDETERMINATE and verify.algorithm-unavailable, never a failure | CorpusSignatureValidationTests.Every_algorithm_is_verified_or_honestly_undecided (new) |
Time stamps: vendor/pyhanko/acrobat-reader-signed-twice.pdf, vendor/lu-legilux/antenna-house-legilux-memorial-pades-lta.pdf, fop22-legilux-memorial-seal-renewed-timestamps.pdf; remote, the DIIA trio (remote/pdfcpu/word2019-diia-test-pades-b-b.pdf, word2019-diia-test-pades-b-lt.pdf, word2019-diia-test-pades-b-lta.pdf, tokens without certificates), the Mentana file (time stamps of 2013 and 2019), the BOE seals (tokens as unsigned attributes) | Each time stamp's verdict and each signature's best signature time equal DSS's; the level reached — PAdES-BASELINE-B, -T, -LT, -LTA, PKCS7-* — equal to DSS's signature format | CorpusSignatureValidationTests.Time_stamps_and_levels_agree_with_eu_dss (new) |
Expired certificates: fop22-legilux-memorial-seal-renewed-timestamps.pdf, vendor/us-federal/docusign-pdfkit-gsa-sf30-contract-modification.pdf; remote, remote/pdfcpu/quartz-avow-certified-docmdp-md5-sigref.pdf (revocation data in Adobe's archival attribute), remote/maine-legislature/ricoh-docusign-itextsharp-state-contract-amendment.pdf, remote/node-signpdf/openoffice21-node-signpdf-signed-twice.pdf (a certificate expired at signing) | Past signature validation as DSS performs it: what a proof of existence saves passes, what it cannot is INDETERMINATE with the sub-indication DSS gives | CorpusSignatureValidationTests.Expired_certificates_are_judged_at_their_proofs_of_existence (new) |
Security stores: the Excel sheet (remote/eu-dss/excel365-nowina-25-signatures-dss-vri.pdf, 31 certificates, 29 OCSP responses, 24 /VRI), the DIIA stores (OCSP only, no /VRI), the plugtest's CRLs, remote/eu-dss/indesign-docusign-envelope-seal-postsignum-doc-timestamp.pdf, the Legilux memorials | Every OCSP response and CRL parsed to the fields OpenSSL prints; for each certificate, the revocation datum we use is the one DSS uses, by digest | CorpusSignatureValidationTests.Revocation_data_is_read_as_openssl_reads_it_and_used_as_dss_uses_it (new) |
M04's crafted fixtures — certifications followed by allowed and forbidden changes, the shadow attacks, the incremental saving attack —; vendor/us-federal/itext-govinfo-us-code-certified.pdf; the Slovak NBÚ and Foxit certifications (remote) | The report carries M04's classification for each signature; the verdict is DSS's under its default policy, and pyHanko's modification level is recorded beside it; every place our default policy departs from DSS's is named in the policy file | CorpusSignatureValidationTests.Changes_after_signing_enter_the_verdict_as_the_policy_says (new) |
The qualified signatures and seals: the Legilux memorials (LuxTrust), remote/eu-dss/pdfmaker11-nbu-sk-qualified-seal-docmdp-fieldmdp.pdf, the Hungarian plugtest seals, the BOE seals, the Mentana 2020 signature (D-TRUST pseudonym), remote/qpdf-issues/yousign-signserver-qualified-seal-test-page.pdf, remote/pdfcpu-issues/adobe-sign-certified-aes128-test-agreement.pdf, vendor/fr-licence-ouverte/fop-dictao-dila-signed-joafe-notice.pdf | Against the snapshot alone: each chains to the trust service DSS names, with the same status at the signing time, and its qualification — QESig, QESeal, AdESeal-QC and the others — is DSS's | TrustedListTests.Qualified_signatures_are_qualified_as_eu_dss_qualifies_them (new) |
| The pinned trusted-list snapshot; a copy with one byte changed in a national list; a list with a wrapped signature (crafted) | The snapshot verifies; the altered list is refused (trust-list.signature-invalid) and none of its services anchors anything; the wrapped signature is refused | TrustedListTests.Only_lists_whose_signature_verifies_are_trusted (new) |
Our B-B and B-T outputs from M26, raised to B-LT and B-LTA with the test PKI's revocation data; the GPO certification (P=1) given a store and an archive time stamp | DSS reports PAdES-BASELINE-LT and -LTA and passes them; M04 classes the revisions SecurityStore and DocumentTimestamp; every earlier signature, the certification included, is intact | CorpusLongTermSigningTests.Signatures_reach_lt_and_lta (new) |
| Our B-LTA signature on the contract, validated at a time after its signing certificate's expiry, and after its signature time stamp's authority certificate's; its B-T revision, cut out through M04, at the same time | DSS passes the B-LTA — the roadmap's condition — and we agree; the B-T revision alone is INDETERMINATE in DSS and in our report, with the same sub-indication | DssValidationRefereeTests.A_b_lta_signature_outlives_its_certificate (new) |
fop22-legilux-memorial-seal-renewed-timestamps.pdf; our B-LTA contract renewed twice, the second archive time stamp after the first authority certificate's simulated expiry | RenewalDue gives the date the last archive time stamp's certificate or algorithm stops counting; after each renewal DSS passes the signature at a time past the previous authority's expiry | CorpusLongTermSigningTests.Renewal_keeps_a_signature_alive (new) |
Received signatures: fop-dictao-dila-signed-joafe-notice.pdf (no store), the Avow certification (revocation data in its CMS), acrobat-reader-signed-twice.pdf, and M26's outputs from another test root | Material added offline from the CMS and from files the test supplies: the store holds exactly what the validator used, DSS's level rises to what that material allows, and each certificate without material is named by ltv.material-missing; no earlier signature changes verdict | CorpusLongTermSigningTests.Received_signatures_gain_long_term_material_honestly (new) |
| M16's ISO/TS 32004 fixture with a MAC attached to its signature, and a tampered copy — filled by M16 | The MAC verified with the file key; the tampered copy fails it | CorpusSignatureValidationTests.A_signature_mac_is_verified (new) |
| The signed corpus with no online client, in a container without a network | The same reports as with the network available and no client; no socket opened; INDETERMINATE wherever material is missing, never a guess | CorpusSignatureValidationTests.Validation_is_offline_by_default (new) |
Hostile: remote/eu-dss/dss-signed-widget-self-parent-startxref-past-eof.pdf, remote/pdfium-tests/foxit-phantompdf-certification-signature-garbage-bbox.pdf, vendor/node-signpdf/pdfkit-node-signpdf-unsigned-placeholder.pdf, remote/canada/livecycle-es9-cfia-fish-export-license-dynamic-xfa.pdf (a legacy /UR without byte range), the fuzzing corpus | No untyped exception, no hang; FORMAT_FAILURE or INDETERMINATE as DSS reports them; within time and allocation budgets | CorpusSignatureValidationTests.Hostile_signatures_never_break_the_validator (new) |
| Any signed document | Every report valid against TS 119 102-2's schema and our JSON schema; two validations byte-identical | EtsiReportSchemaTests (new), CorpusSignatureValidationTests.Validation_is_deterministic (new) |
| The Excel sheet (25 signatures in 48 updates) and the 9,302-page US Code (remote) | Validation within the time and memory budgets recorded in status.md; memory follows the signatures, not the document | CorpusSignatureValidationTests.Validating_many_signatures_holds_its_budget (new), SignatureValidationBenchmarks (new) |
| The same operations through the tool | verify writes the report the API writes; its exit code follows the verdicts | CorpusToolTests.Verify_matches_the_api (new) |
The remote rows close only on a green Remote corpus run, recorded in docs/status.md with its date.
Corpus
What the corpus holds
- Every PAdES level from real producers: B-B, B-LT and B-LTA on one page (the DIIA trio), B-LTA from a qualified seal (Legilux), a seal renewed by document time stamps over two years, plugtest seals each followed by a document time stamp, a file re-signed over seven years ending in RSASSA-PSS, 24 signatures in 48 updates.
- Security stores:
dss,vri,dss-ocsp-only,dss-without-vri,crls,ocsp, and Adobe's archival attribute with embedded CRL and OCSP (adbe-revocation-info-archival,embedded-crl,embedded-ocsp). - Time stamps:
signature-timestamp,document-timestamp,two-document-timestamps,timestamp-token-without-certificates,rfc3161-timestamp-unsigned-attribute,ecdsa-timestamp. - Legacy and weak cryptography:
signed-adbe-pkcs7-sha1,signed-adbe-x509-rsa-sha1,raw-pkcs1-signature,sha1-rsa-signature,rsa-1024-key,cms-without-signed-attributes,cms-signed-attributes-not-der-sorted,negative-certificate-serial. - Certificates in trouble:
expired-signing-certificate,signing-certificate-expired,signer-certificate-expired-at-signing,self-signed-certificate,test-certificate,pseudonym-certificate. - Qualified signatures and seals under EU trusted lists: LuxTrust, NBÚ, e-Szigno, FNMT for the BOE, D-TRUST,
Certigna for DILA, Yousign's and Adobe Sign's qualified seals (
qualified-seal,eidas-qualified-seal). - Commercial services: DocuSign (three), Adobe Sign, Yousign, HelloSign, SignNow.
- Damage near signatures: M04's list — a byte range past the end,
/Contentsshorter than its gap, damage inside a signed range, a field tree cycle, the PDFKit placeholder. - Scale: 25 signatures in 48 updates; the 9,302-page US Code, signed.
What it lacks
| Need | Why | Priority | Likely source |
|---|---|---|---|
EU DSS's verdict for every signature of the signed corpus — indication, sub-indication, format, qualification — in both configurations, at a recorded time (expect.signatures[].dss) | The roadmap's acceptance is agreement with DSS, and it must rest on DSS's opinion recorded in advance, never on ours | 1 | Generated here: build_corpus.py --committed-only and --remote running the DSS image, pinned like the other referees |
| A pinned snapshot of the LOTL, the national lists the corpus's qualified signatures need, and the Official Journal's LOTL signing certificates, with the validation time it fixes | Validation against trusted lists must be reproducible; the live lists change weekly and their URLs are not immutable, so the remote corpus cannot pin them | 1 | Generated here by a recorded download script; committed under tests/trusted-lists/ if the Commission's reuse terms and the size allow, otherwise published once as an immutable archive of our own and fetched by the nightly job under a pinned SHA-256 (ADR 33) |
| Crafted invalid signatures from the test PKI: covered content altered, a signature value altered, a signing-certificate attribute naming another certificate, no signing certificate, an untrusted root, a revoked signer with and without a proof of existence, a not-yet-valid certificate, a token over the wrong imprint, an OCSP response from an unauthorized responder, an expired CRL | Every sub-indication the design names must be reached by a real file another implementation wrote, not only by unit fixtures | 1 | Generated here: pyHanko and Certomancer over M26's test PKI, then recorded byte edits in build_corpus.py |
| SHA3-256 and SHA3-512, Ed25519, Ed448 (SHAKE256) and Brainpool ECDSA signatures, with their ISO/TS 32001 and 32002 extensions | The roadmap names SHA-3 and EdDSA, and no document in the corpus carries either | 1 | Generated here with pyHanko over the test PKI if its pinned version writes them; otherwise iText 9 in a container, as a producer only |
| Our own B-LT and B-LTA outputs, committed, and a renewal chain over a simulated decade | The roadmap's expiry condition and the renewal row need signatures whose whole history is known | 1 | Generated here, by this milestone, from M26's committed outputs and the test PKI's short-lived certificates |
| A document whose ISO/TS 32004 MAC is attached to its signature, and a tampered copy | M16 left the verification here; M16 lists the fixture as its gap | 2 | Filled by M16 if its pinned producer writes it; otherwise generated here the same way |
| A delegated OCSP responder, a delta CRL, an indirect CRL, a CRL with an issuing distribution point | Each is handled by the design and met in no file | 2 | Generated here with Certomancer |
| A French qualified signature with long-term material from a French trust service provider — Certigna, ChamberSign, Universign or Yousign — committed | The market this library serves first; the committed DILA signature carries no store, and the others are remote | 2 | A contribution (W05 in docs/corpus-contributions.md) |
| A real archive whose archive time stamp lapsed before it was renewed | The INDETERMINATE a missed renewal earns is proven only on simulated time | 3 | Generated here by simulation; a contribution |
Traps
- Covers is not valid, and valid is not trusted. M04 says what a signature covers; integrity says the bytes were signed by a key; the path says whose key; revocation and time say whether it still counts. The report keeps them apart.
- The platform's chain engine is not a validator for this purpose.
X509Chainfetches on its own, differs by operating system, and cannot use a security store. Paths are ours. - Names are compared as RFC 5280 says, not byte for byte: case, spaces and string types differ between an issuer field and a subject field that mean the same name.
- An expired certificate does not fail a signature that a time stamp proves older. Past signature validation is the reason B-LTA exists.
- Revocation after the proof of existence is not revocation of the signature. Dates decide, not the mere presence of a revocation entry.
- Offline means offline. No fetch without a client the caller supplied; a missing datum is
INDETERMINATE, and the documentation says which option would fetch it. - The validation time is the caller's. A report that reads the clock is not reproducible, and invariant 6 says it must be.
- Trusted lists are signed XML, and XML signatures are wrapped as easily as PDF ones: one enveloped signature over the whole document, or nothing.
- A store is extended, never pruned. Removing another signer's material is a change M04 will not classify as a security store, and it voids their long-term validation.
- VRI keys are the SHA-1 of the signature's
/Contentsvalue, not of the byte range nor of the signature value inside the CMS, in upper case; whether the zero padding counts is where implementations part, so the key written is the one DSS and pyHanko look up, checked in slice 10. - SHA-1 and 1024-bit RSA fail on dates, not on sight. A 2008 signature with a time stamp from 2008 may pass; the policy's catalog decides.
- EdDSA and SHA-3 are not everywhere. Where the platform lacks them, say so:
INDETERMINATE, never a verdict of failure the signature did not earn. - DSS is a referee, not the specification. Where DSS and the text of EN 319 102-1 part, the text wins and the difference is recorded — but only after reading the text, never to make a test pass.
Documentation
docs/website/docs/guides/validating-signatures.md(new): validating a document's signatures, anchors and trusted lists, the validation time, offline and online, reading the report.docs/website/docs/guides/long-term-signatures.md(new): B-LT, B-LTA, renewal and when it falls due, adding material to received signatures before archiving.docs/website/docs/concepts/signature-validation.md(new): the procedures, indications and sub-indications, proofs of existence, the policy and its differences from DSS's, what the report does and does not establish.docs/website/docs/reference/validation-report-schema.md(new): the ETSI XML, the JSON schema and the extension that carries M04's findings.docs/website/docs/concepts/signatures.md: M26's page, completed with the long-term levels.docs/website/docs/reference/tool/:verify,sign --level b-lt|b-lta,sign --renew.docs/website/docs/introduction.mdanddocs/features/features.json: theltv-validationentry brought to its state.docs/adr/: the ADR amending ADR 41 (the trusted-list satellite andSystem.Security.Cryptography.Xml, the OCSP and CRL parsers, paths of our own); the ADR on EdDSA verification.docs/architecture.md: the Signing satellite's validation and long-term parts, the trusted-list satellite.docs/corpus.mdandtests/corpus/manifest.schema.json:expect.signatures[].etsiand[].dss, and the referee that writes them;docs/corpus-contributions.md: the new wants.docs/status.md: the snapshot's date, the budgets, the recorded disagreements with DSS.
Exit criteria
- Every signature and time stamp of every signed document gets a verdict under EN 319 102-1's procedures, with paths, revocation and proofs of existence of our own, offline by default.
- The report is written as TS 119 102-2 XML and as JSON, schema-valid and deterministic.
- The trusted-list satellite verifies the LOTL and the national lists and determines qualification.
- B-LT, B-LTA, renewal and long-term material for received signatures are written within M04's allowed classes.
- SHA-3 and EdDSA are verified where the platform or the ADR allows, and honestly undecided elsewhere.
- The ADRs amending ADR 41 and deciding EdDSA are accepted.
-
expect.signatures[].dssis in the schema and the manifest, written by DSS; every disagreement is fixed or recorded with its reason. - The priority-1 gaps above are filled; each remaining gap is recorded in
docs/corpus-contributions.md. - The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote rows on a
green
Remote corpusrun recorded indocs/status.md. - Unit tests cover each behavior, its degenerate cases and its hostile ones; the parsers are in the nightly fuzzing campaign.
- Integration tests run EU DSS, pyHanko, OpenSSL,
pdfsig, xmllint and Certomancer, each in a container. -
SignatureValidationBenchmarksruns withMemoryDiagnoser;docs/status.mdrecords the budgets. -
verifyand the long-termsignoptions ship in the dotnet tool and the AOT binary, documented. - The documentation site publishes the pages listed above.
- Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).