Skip to main content

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 — the message-digest over 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.sha1 and adbe.x509.rsa_sha1 signatures 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 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, /VRI entries 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 caIssuers and 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 verify verb, and sign --level b-lt|b-lta and sign --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​

PackageHoldsDepends on
AdCodicem.Pdf.SigningAdCodicem.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, qualificationAdCodicem.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:

BlockWhat it checks here
Format checkingM04'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 certificateESS 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 initializationThe policy, the anchors, the validation time
Cryptographic verificationThe 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 validationPaths built and validated (below), at the validation time or at a proof of existence
Revocation freshnessEach revocation datum's issuance against the policy's freshness and the time it must speak for
Signature acceptanceThe 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 — caIssuers from 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 tbsCertificate bytes; 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 left INDETERMINATE; certificate policies read and reported, not enforced unless the policy file asks.

Revocation​

  • Sources, in order: the security store (/DSS and /VRI, read lazily through the core), the CMS's own revocation data (Adobe's adbe-revocationInfoArchival signed attribute, RFC 3161 tokens' certificates), what the caller supplies, then — only through IPdfRevocationClient instances 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; thisUpdate and nextUpdate against 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_POE as the standard names them. No revocation data, offline: INDETERMINATE with the standard's sub-indication, and the certificate named — never a pass.

Algorithms​

AlgorithmVerified withWhen the platform lacks it
RSA PKCS #1 v1.5, RSASSA-PSSThe BCL's RSA—
ECDSA on P-256, P-384, P-521The BCL's ECDsa—
ECDSA on Brainpool curvesThe BCL's ECDsa where ECCurve names the curveINDETERMINATE, verify.algorithm-unavailable
SHA3-256, 384, 512, SHAKE256The BCL's SHA3_* and Shake256 where IsSupportedINDETERMINATE, verify.algorithm-unavailable
Ed25519, Ed448Decided 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 bothThe 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, an ITrustedListFetcher the caller supplies downloads them; caching is the caller's.
  • Parsing hostile XML: XmlReader with DTDs prohibited, no resolver, a size bound; SignedXml restricted 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/QTST and 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 or N/A — as EU DSS names them — with every input that decided it.
  • A list past its NextUpdate at 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): /DSS in the catalog with /Certs, /OCSPs and /CRLs arrays of streams, each object written once, deduplicated by digest; /VRI entries 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 /Contents bytes —, with /TU from the caller's time. An existing store is extended, never pruned: its objects are kept and the catalog's /DSS redefined with the union, which M04 classes SecurityStore, 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 /ESIC extension 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; RenewalDue reports 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:

BoundKindWhy
A certification path's lengthOption, PdfSignatureValidationPolicy.MaxPathLength (proposed 16), reported verify.path-too-long, CERTIFICATE_CHAIN_GENERAL_FAILUREA valid path is rarely over five; a hostile store of cross-certificates can offer exponentially many
Paths tried per certificateOption, MaxPathCandidates (proposed 1,000), the same codeDepth-first search with a visited set is still exponential in the worst case
An OCSP response's and a CRL's length, fetched or suppliedOption, MaxRevocationDataLength (proposed 64 MB); inside the file, the reader's MaxDecodedStreamLengthSome authorities' CRLs are tens of megabytes, validly
A CRL's entriesNone needed: streamed, never materializedMemory follows one entry
A trusted list's lengthOption, TrustedListSnapshot.MaxListLength (proposed 32 MB)National lists run to a few megabytes; a hostile server can send more
Lists followed from the LOTLInternal: the LOTL's pointers only, never a national list's pointers onwardThe standard's topology is two levels; following further is how a loop starts
Online fetches per validationOption, MaxFetchesA hostile document can name thousands of distribution points
ASN.1 nesting in OCSP and CRL structuresThe framework's AsnReader, under the structure's fixed depthThe structures have a fixed shape; anything deeper is malformed

Diagnostics​

CodeSeverityMeaning
verify.algorithm-unavailableWarningAn algorithm the platform cannot run: the signature is INDETERMINATE, not failed
verify.revocation-offlineInformationRevocation data absent and no client supplied; the certificate named
verify.path-too-longWarningA path or search bound reached; the option named
verify.legacy-digest-unverifiedInformationA /Reference object digest recorded by M04 and not verified
verify.policy-differenceInformationA constraint where our default policy departs from EU DSS's, named in the policy file
trust-list.signature-invalidErrorA list whose signature does not verify; its services unused
trust-list.staleWarningA list past its NextUpdate at the validation time
ltv.material-missingWarningA certificate for which no chain or revocation data could be found
ltv.renewal-dueInformationThe 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.

  1. Integrity and the report's skeleton. Delivers format checking over M04's coverage, SignedCms parsing 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[].etsi in 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 /Contents zero-padded; a signature value with one bit flipped); integration: on every signed corpus document, intact where pyHanko finds it intact and openssl cms -verify agrees; every report valid against the ETSI schema with xmllint. Leaves chains.
  2. 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 at MaxPathCandidates); integration: the path pyHanko's certvalidator builds, for every signed corpus document with its own root trusted. Leaves revocation.
  3. Revocation offline, and the ADR. Delivers the ADR amending ADR 41; the OCSP and CRL parsers, bounded and fuzzed; the store, /VRI and Adobe's archival attribute as sources; freshness; the revocation sub-indications. Proved by unit tests (a delegated responder with and without nocheck, 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 fields openssl ocsp -resp_text and openssl crl -text print; the test PKI's revoked certificates reported revoked at the right date. Leaves time.
  4. 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.
  5. 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.
  6. The policy and the algorithms. Delivers PdfSignatureValidationPolicy with 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 giving INDETERMINATE); 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.
  7. 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.
  8. 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.
  9. Online, on request. Delivers HttpOcspClient, HttpCrlClient, the caIssuers fetcher and the list fetcher over the caller's HttpClient, 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.
  10. B-LT, B-LTA and renewal. Delivers PdfLongTermSigning, the store, /VRI, archive time stamps, renewal, RenewalDue, the sign options. Proved by the long-term rows below: DSS reports PAdES-BASELINE-LT and -LTA and passes them, M04 classes each revision, and every earlier signature is intact. Leaves received signatures.
  11. Long-term material for received signatures. Delivers AddValidationDataAsync on 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.
  12. Agreement, budgets, the tool and the documentation. Delivers expect.signatures[].dss — indication, sub-indication, format, qualification, configuration, validation time — written by build_corpus.py running DSS, and a disagreement field beside it, as M20 gives veraPDF's verdicts one; every disagreement fixed or recorded there with its reason, SignatureValidationBenchmarks with MemoryDiagnoser, the verify verb, the documentation. Proved by the agreement and budget rows and a green Remote corpus run recorded in docs/status.md with 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-digest of 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; /VRI keys; 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 TimeProvider passed.
  • 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.

DocumentsBehaviorVerified by
The signed corpus, in both configurations, at the snapshot's timeFor 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 notCorpusSignatureValidationTests.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 flippedIntact exactly where pyHanko and openssl cms -verify find it intact; the copies HASH_FAILURE and SIG_CRYPTO_FAILURECorpusSignatureValidationTests.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 attributesIntegrity verified as OpenSSL verifies it; the verdict under the policy's constraints on SHA-1 and 1024-bit RSA equal to DSS'sCorpusSignatureValidationTests.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 failureCorpusSignatureValidationTests.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 formatCorpusSignatureValidationTests.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 givesCorpusSignatureValidationTests.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 memorialsEvery OCSP response and CRL parsed to the fields OpenSSL prints; for each certificate, the revocation datum we use is the one DSS uses, by digestCorpusSignatureValidationTests.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 fileCorpusSignatureValidationTests.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.pdfAgainst 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'sTrustedListTests.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 refusedTrustedListTests.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 stampDSS reports PAdES-BASELINE-LT and -LTA and passes them; M04 classes the revisions SecurityStore and DocumentTimestamp; every earlier signature, the certification included, is intactCorpusLongTermSigningTests.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 timeDSS 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-indicationDssValidationRefereeTests.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 expiryRenewalDue 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 expiryCorpusLongTermSigningTests.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 rootMaterial 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 verdictCorpusLongTermSigningTests.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 M16The MAC verified with the file key; the tampered copy fails itCorpusSignatureValidationTests.A_signature_mac_is_verified (new)
The signed corpus with no online client, in a container without a networkThe same reports as with the network available and no client; no socket opened; INDETERMINATE wherever material is missing, never a guessCorpusSignatureValidationTests.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 corpusNo untyped exception, no hang; FORMAT_FAILURE or INDETERMINATE as DSS reports them; within time and allocation budgetsCorpusSignatureValidationTests.Hostile_signatures_never_break_the_validator (new)
Any signed documentEvery report valid against TS 119 102-2's schema and our JSON schema; two validations byte-identicalEtsiReportSchemaTests (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 documentCorpusSignatureValidationTests.Validating_many_signatures_holds_its_budget (new), SignatureValidationBenchmarks (new)
The same operations through the toolverify writes the report the API writes; its exit code follows the verdictsCorpusToolTests.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, /Contents shorter 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​

NeedWhyPriorityLikely 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 ours1Generated 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 fixesValidation against trusted lists must be reproducible; the live lists change weekly and their URLs are not immutable, so the remote corpus cannot pin them1Generated 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 CRLEvery sub-indication the design names must be reached by a real file another implementation wrote, not only by unit fixtures1Generated 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 extensionsThe roadmap names SHA-3 and EdDSA, and no document in the corpus carries either1Generated 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 decadeThe roadmap's expiry condition and the renewal row need signatures whose whole history is known1Generated 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 copyM16 left the verification here; M16 lists the fixture as its gap2Filled 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 pointEach is handled by the design and met in no file2Generated here with Certomancer
A French qualified signature with long-term material from a French trust service provider — Certigna, ChamberSign, Universign or Yousign — committedThe market this library serves first; the committed DILA signature carries no store, and the others are remote2A contribution (W05 in docs/corpus-contributions.md)
A real archive whose archive time stamp lapsed before it was renewedThe INDETERMINATE a missed renewal earns is proven only on simulated time3Generated 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. X509Chain fetches 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 /Contents value, 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.md and docs/features/features.json: the ltv-validation entry brought to its state.
  • docs/adr/: the ADR amending ADR 41 (the trusted-list satellite and System.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.md and tests/corpus/manifest.schema.json: expect.signatures[].etsi and [].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[].dss is 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 corpus run recorded in docs/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.
  • SignatureValidationBenchmarks runs with MemoryDiagnoser; docs/status.md records the budgets.
  • verify and the long-term sign options 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).