feat(seal): read and verify EU Trusted Lists - #316
Merged
Merged
Conversation
Member
Author
|
@coderabbitai review |
|
LKSNDRTMLKV
force-pushed
the
feat/trusted-list-revived
branch
from
September 15, 2026 05:06
79d0082 to
7503b7e
Compare
LKSNDRTMLKV
force-pushed
the
feat/trusted-list-revived
branch
from
September 15, 2026 06:25
7503b7e to
ad247db
Compare
This was referenced Sep 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Revives #294, which GitHub auto-closed when its base branch
feat/seal-evidenced-levelmerged as #292. The work was finished and pushed; nothing was wrong with it except that its PR closed underneath it.just checkis green (1138/1138). #312 has merged, so this targetsmaindirectly.Why this is stacked rather than independent
It needs
dpp_domain::trusted_listandSealedEnvelope::conformance_level, both new indpp-core0.20.0, so it does not build against the pin onmain. That is the dependency the original note recorded as "blocked on the core repin", and #312 clears it.What it adds
dpp_seal::trustlist—verify_lotlfor the List of Trusted Lists,verify_trusted_listfor a Member State's, plus parsers, fetcher and typed rejections. Nothing calls it yet. This is the reader, not a policy.The trust anchor is an Official Journal notice, not a certificate authority. The LOTL's signing certificates chain to no commercial root — they are published in the OJ C series, which the LOTL names through its own
SchemeInformationURI. There is nothing to walk up to, so the anchor is six SHA-256 digests taken from notice52026XC01944, compiled in with the location, CELEX and pin date beside them. Compiled in and never configuration: an operator who can repoint the anchor can make any list verify.Those six digests are the entire pinned surface. No Member State's certificates live in the repository — they arrive inside a document that has already been verified, so a country joining or rotating needs no code change and no release.
authorisesis a precondition, not a verdict: anyone can copy the genuine certificate into a forgery and pass it, and only the signature separates them. The converse is the sharper half — Finland's list is genuine and correctly signed by Finland, which does not sign the LOTL, and it verifies cleanly with the anchor check disabled. That pair is why both halves exist, and there is a test for each.What reviewers should look at hardest
The recurring operational obligation. The pin must be refreshed when the Commission republishes the notice, and the current signer expires 2027-11-17 — a calendar date, not a discovery. A stale pin fails closed and reads as an outage. The reader carries the early signal (the LOTL's first
SchemeInformationURIentry is the current notice), but nothing acts on it yet.The
xml-secfork. Italy (2.86 MB) and France (2.55 MB) exceed a compile-time node-set ceiling in the published crate and cannot be verified at any configuration — the constant ispub(crate)and the policy knob that looks like it raises the limit is validated against the same number. They miss by four and five nodes, and trusted lists only grow. The fork changes that one constant and nothing else, pinned by rev rather than vendored because the crate is ~66k LOC.Changes made during the revival
A
deny.tomlsource allowance. The sources policy denies git dependencies deliberately — "a git dependency is how unreviewed code enters a build" — so the fork needs an explicit entry. Added with the justification and the exit condition: whenstructured-world/xml-sec#158resolves, the[patch.crates-io]stanza and thedeny.tomlentry go together. The rule still stands for everything else; this one is allowed because the review is a single constant, not because forks are fine.A CHANGELOG entry. The work had none.
One conflict that was a real disagreement, not a textual one. This branch recorded a seal's
conformance_levelas what the bytes carry; #312 records what was requested. Core settles it — the field's doc says it is "a record of what was asked for, not proof of what arrived", and the conformance kit'sseal.misrecorded_levelrule fails an envelope whose stored level disagrees with the request. I kept the request.The concern behind the other version was legitimate and is preserved in the comment: echoing a request could launder a broken
can_produceguard. What actually catches that iscades::evidenced_level, read independently by the drain, which alarms on the disagreement rather than letting either side vouch for itself.Folded in from #317
The TS 119 612 signature profile (#297) is asserted here.
3e03920was cherry-picked onto this branch as504b33f, and #317 is closed.It was folded rather than merged behind this one because splitting them would put a verifier on
mainthat accepts an arbitrary transform chain, for as long as it took to merge the second PR. Transforms decide what a signature covers, andxml-sec'sTransformPolicy::allowed_algorithmsdefaults toNone— every implemented algorithm, XPath and XPath Filter 2.0 included. A gap that narrow is not worth the seam.The anchor check is now bound to the key that verifies
chain_tests::each_signature_names_exactly_one_certificatealready recorded this hazard: this module reads a certificate out of the document and checks it against the anchor, whilexml-secverifies the signature with a certificate it selects —select_x509_signing_certificateresolves the leaf of the embedded chain and only falls back to document order when that finds nothing.VerifyResultexposes no certificate, so a disagreement cannot be caught afterwards.That test guards two committed fixtures. The documents that matter arrive at runtime from 43 URLs, so it could detect the problem and never prevent it.
The sharp part, which the test showed rather than confirmed:
ds:KeyInfosits insideds:Signature, which the mandated enveloped-signature transform removes from the digest input. A certificate can therefore be added to a genuine, correctly signed trusted list without disturbing its signature — injecting one into the published LOTL comes backAmbiguousSigningCertificate, notSignatureInvalid. Reading "the first certificate" would have taken the real anchored one, passed the anchor, and gone on to verify. What actually binds the certificate is XAdESSigningCertificatein the signed properties, which this module does not read.Two constraints make the divergence impossible instead of merely visible:
ds:KeyInfo, the only placexml-secresolves keys from — ads:Objectcarrying XAdESCertificateValuesholds certificates too;ds:KeyInfomust carry exactly one certificate, so every selection rule resolves to the same bytes.New
AmbiguousSigningCertificateon both enums. Refusing is free today — every published list checked carries exactly one — and when that stops being true the fix is to resolve the leaf the wayxml-secdoes, not to relax the check.Two defects fixed from #317's branch
TrustedListRejected::NonConformantProfilerendered a literal\nplus seventeen spaces where itsLotlRejectedtwin used a line continuation. Neitherrustfmtnorclippyreads inside a string literal, and no test rendered that variant.no_rejection_message_carries_a_stray_line_breaknow covers all fifteen variants across both enums.signing_certificate's doc comment and the function, silently reattaching that doc toENVELOPED_SIGNATURE.just checkgreen (1138/1138), andjust lint-integration— the tiercheckomits, per #332 — is clean.