feat(seal): report the level a seal actually came back at - #292
Merged
Merged
Conversation
This was referenced Sep 13, 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.
Closes #290. Closes #291.
The seal request names a conformance level. The boot gate checks that level against the backend's advertised capabilities. Then the returned bytes were taken on trust, and nothing ever looked at them again.
A provider answering
CAdES_BASELINE_Tto a request forLT— a client enabled for the wrong profile, a plan change, a provider-side default — produced a seal that was correct in every record this node kept. It stops verifying when the signing certificate expires, years after the passport was retention-locked and long after anything could be done about it. That is precisely the failureSealConformanceLeveldescribes as uncorrectable, and nothing was watching for it.What this adds
dpp_seal::cades::evidenced_levelreads the returned CMS and reports the highest baseline level whose distinguishing material is present: asignature-time-stampforT, long-term revocation material forLT, an archival timestamp forLTA.The drain calls it on every seal, logging at
errorwhen something bought as long-term carries no long-term material, and counting it in a newseal_downgraded_total.Two properties chosen deliberately, both tested:
Nothing here is validated, in keeping with the rest of that module: a timestamp token is counted because it is in the bytes, not because anyone confirmed it timestamps this signature or that its TSA is trusted. Establishing that remains an AdES validator's job.
seal_downgraded_totalis its own counter rather than anotherseal_total{outcome=…}. That label is a partition — a row is sealed or retried or exhausted — and a downgraded row is a sealed row too, so putting it there would makesum(seal_total)exceed the rows drained and quietly corrupt every ratio built on it.The part that is easy to get wrong
The two standards in play disagree about where an LT seal's revocation material lives.
SignedData.crls, and marks therevocation-valuesattributeshall not be presentat that level.So a seal built to the profile the law cites carries exactly what the modern standard forbids at the same level. Both homes are accepted.
This is not theoretical: with only the modern home checked, the legacy fixture reports
BaselineTand the drain raises a downgrade alarm against a provider that did nothing wrong. That failure was reproduced before the test was trusted —either_home_of_the_revocation_material_evidences_baseline_ltpins it.Related trap, in the code comments so it survives: the legacy
certificate-values/revocation-valuesattributes are forbidden at B-LT under EN 319 122-1, so looking for them as evidence of LT finds the opposite of what it means under that standard. AndSignedData.certificatesisshall be presentat every level including B-B, so it does not discriminate at all.Also fixed (#291)
sealedAtread as evidence and was not. Documented as "When the QTSP produced it"; it is this node's clock when the backend answered, and for the local backend there is no QTSP at all. A seal carries an attested signing time only fromTupward. Handler doc and published schema; no behaviour change.LocalIdentity::generated_atreturnedUtc::now()under a doc comment claiming it returned the certificate's generation time. No callers, so nothing was wrong today — anything wiring it in would have got a plausible-looking wrong answer. Removed.Tests
dpp-seal55 → 61,dpp-node's drain suite gains one. New: unreadable bytes evidence no level; the local backend's own seal evidencesBaselineB(which also pins its advertisement against its bytes); a timestamp alone evidencesT; both homes evidenceLT; both archival timestamp generations evidenceLTA; material for a high level without the levels beneath it reports the floor; and a seal below the requested level is still recorded rather than retried.The fixtures decorate genuine bytes from the local backend rather than assembling CMS from nothing — a hand-rolled structure would only prove the reader agrees with the fixture.
just checkgreen.Not in this PR
Persisting the level that was requested, so the record and the bytes can be compared, needs a
dpp-corefield that is merged but unreleased. Tracked in #289, with the release-and-repin sequence and the[patch]trap that sits in the middle of it.