Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,36 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md):

### Added

- **A verified list of trusted lists now says whether the pin it verified
against is still the notice in force.** New `AnchorFreshness` on
`VerifiedLotl`: `Current`, `Superseded { lotl_names }`, or `Unknown`.

The trust anchor is pinned from an Official Journal notice rather than chained
to a certificate authority, and the Commission republishes that notice. A pin
nobody refreshes eventually meets a LOTL signed by a certificate it does not
name and fails closed as `NotAnchored` — on a date nobody has in a calendar,
looking like an outage rather than a lapsed pin. This is the only warning
before that.

**A signal, never a verdict.** A superseded pin keeps verifying until the
certificates actually rotate, and that window is the only chance to refresh
without an outage — so folding this into `LotlRejected` would refuse documents
that verify perfectly and turn the early warning into the thing it exists to
prevent. `a_superseded_pin_does_not_refuse_a_document_that_verifies` pins that.

**`Unknown` is not `Current`.** A document naming no notice cannot be checked,
and reporting it as up to date is how a staleness signal goes quiet at the
moment it matters.

Reported and logged: the caller that most needs to act on it is an operator
reading logs, not the code holding the `VerifiedLotl`.

The check rests on the document listing its notice **first** in
`SchemeInformationURI`, ahead of the pivot chain and twenty-three per-language
legal notices. That ordering is asserted against the real published document
rather than trusted, so if it ever changes the result is a red test rather than
a comparison against a pivot URL.

- **A life status that contradicts its own lineage is now reported.** The
plausibility lint gains `lineage.life_status_unsupported`, from
`dpp_rules::lineage::check_life_status_consistency`: a unit claiming
Expand All @@ -75,6 +105,58 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md):
The create/update path and `POST /dpp/{dppId}/lint` now compute the result
through one function. They did not before, so a check added to one and not the
other would have made the route disagree with the record it re-checks.
- **The EU Trusted Lists can now be read and verified.** New
`dpp_seal::trustlist`: `verify_lotl` for the List of Trusted Lists,
`verify_trusted_list` for a Member State's, plus the parsers, the fetcher and
the 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 itself names through
`SchemeInformationURI`. So there is nothing to walk up to, and the anchor is
six SHA-256 digests taken from notice `52026XC01944`, compiled in with the
location, CELEX and pin date beside them.

**Compiled in, never configuration.** An operator who can repoint the anchor
can make any list verify.

**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 its certificate needs no code change
and no release. That is the whole payoff of a list of lists.

`authorises` is a **precondition, not a verdict**: anyone can copy the genuine
certificate into a forgery and pass it. Only the signature separates them.
Conversely a valid signature alone proves nothing about authority — 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.

🚨 **This creates a 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 looks like an outage, so the reader also has the early signal: the
LOTL's first `SchemeInformationURI` entry is the current notice.

`TrustedListRejected` is a separate enum from `LotlRejected` deliberately: a
rejected LOTL usually means our pin is stale, a rejected national list never
does, and the two send an operator in different directions.

- **`xml-sec` is pinned to an organisation fork, temporarily.** 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 is
`pub(crate)` and the policy knob that appears to raise it is validated against
the same number. They miss it by four and five nodes, and trusted lists only
grow.

The fork changes that one constant and nothing else. It is a pinned git rev
rather than a vendored copy because the crate is ~66k LOC: vendoring would make
every upstream sync a 66k-line diff and bury the one line that is ours.
`deny.toml` carries the matching source allowance and the same exit condition.

Filed upstream as `structured-world/xml-sec#158`. **When that resolves, the
`[patch.crates-io]` stanza and the `deny.toml` entry go together.**

- **The passport response now serves `serialNumber`, `lifeStatus` and
`responsibleOperator`.** All three are modelled on the core aggregate and were
Expand Down
Loading
Loading