The seal request names a conformance level. Nothing checks what came back.
level_for_profile() classifies the configured signature_profile string, and the boot gate compares that configuration against the backend's advertised capabilities. Neither reads the returned CMS. A provider that silently returns CAdES_BASELINE_T when asked for LT — a client enabled for the wrong profile, a plan change, a provider-side default — produces a seal that looks correct in every record this node keeps, and stops verifying when the signing certificate expires, years after the passport was retention-locked.
That is precisely the failure SealConformanceLevel's own documentation describes as uncorrectable.
crates/dpp-seal/src/cades.rs already parses the CMS SignedData down to its single signer and certificate. It has everything needed.
What the levels are distinguished by
From ETSI EN 319 122-1 clause 6.1 and Table 1 (CAdES baseline). Read the table with a table-aware extractor — a plain layout extraction detaches the value columns from their rows and silently misattributes them.
| Level |
Distinguishing material |
| B-B |
none of the below |
| B-T |
signature-time-stamp unsigned attribute (id-aa-signatureTimeStampToken, OID 1.2.840.113549.1.9.16.2.14) — shall be present at B-T and above, * at B-B |
| B-LT |
B-T material, plus the "revocation values in long-term validation" service shall be provided — i.e. SignedData.crls populated (crls.crl or crls.other, the latter carrying OCSP) |
| B-LTA |
B-LT material, plus the archive-time-stamp-v3 unsigned attribute |
Two traps worth writing into the code comments:
- The legacy attributes are forbidden, not required, at LT.
certificate-values, revocation-values, complete-certificate-references and complete-revocation-references are shall not be present at B-LT and B-LTA (cardinality 0). Looking for them as evidence of LT finds the opposite of what it means. At B-LT the validation material lives in SignedData.certificates and SignedData.crls.
SignedData.certificates does not discriminate. It is shall be present at every level, B-B included.
What to build
A cades::evidenced_level(seal_der) -> Option<SealConformanceLevel> reporting the highest level whose distinguishing material is present.
Name and document it as a floor, not a verdict. Full conformance to a baseline level requires many more rows of Table 1 (signing-time shall be present, content-type shall be id-data, the ESS signing-certificate SPO, and so on). This function establishes that the material for a level is there, not that the seal conforms. It belongs in the same register as the rest of that module — reported, never verified — and the module doc already makes that distinction the reason it exists as a separate module.
Then:
- Expose it on
SealResponse beside the requested level, as evidencedConformanceLevel, so an auditor sees request and bytes side by side. Same shape as sealedPayloadHash versus the validator's extracted message digest: this node states both and says which is which.
- Log a warning at drain time when the evidenced level is below the requested one. Not an error — the seal is bought and the row should not retry — but a node that has been quietly buying the wrong thing should say so on the first occurrence, not on the first expiry.
- The local backend is a ready-made fixture:
sign_detached sets unsigned_attrs: None, so it must classify as BaselineB and never higher.
The seal request names a conformance level. Nothing checks what came back.
level_for_profile()classifies the configuredsignature_profilestring, and the boot gate compares that configuration against the backend's advertised capabilities. Neither reads the returned CMS. A provider that silently returnsCAdES_BASELINE_Twhen asked forLT— a client enabled for the wrong profile, a plan change, a provider-side default — produces a seal that looks correct in every record this node keeps, and stops verifying when the signing certificate expires, years after the passport was retention-locked.That is precisely the failure
SealConformanceLevel's own documentation describes as uncorrectable.crates/dpp-seal/src/cades.rsalready parses the CMSSignedDatadown to its single signer and certificate. It has everything needed.What the levels are distinguished by
From ETSI EN 319 122-1 clause 6.1 and Table 1 (CAdES baseline). Read the table with a table-aware extractor — a plain layout extraction detaches the value columns from their rows and silently misattributes them.
signature-time-stampunsigned attribute (id-aa-signatureTimeStampToken, OID 1.2.840.113549.1.9.16.2.14) — shall be present at B-T and above,*at B-BSignedData.crlspopulated (crls.crlorcrls.other, the latter carrying OCSP)archive-time-stamp-v3unsigned attributeTwo traps worth writing into the code comments:
certificate-values,revocation-values,complete-certificate-referencesandcomplete-revocation-referencesareshall not be presentat B-LT and B-LTA (cardinality 0). Looking for them as evidence of LT finds the opposite of what it means. At B-LT the validation material lives inSignedData.certificatesandSignedData.crls.SignedData.certificatesdoes not discriminate. It isshall be presentat every level, B-B included.What to build
A
cades::evidenced_level(seal_der) -> Option<SealConformanceLevel>reporting the highest level whose distinguishing material is present.Name and document it as a floor, not a verdict. Full conformance to a baseline level requires many more rows of Table 1 (
signing-timeshall be present,content-typeshall beid-data, the ESS signing-certificate SPO, and so on). This function establishes that the material for a level is there, not that the seal conforms. It belongs in the same register as the rest of that module — reported, never verified — and the module doc already makes that distinction the reason it exists as a separate module.Then:
SealResponsebeside the requested level, asevidencedConformanceLevel, so an auditor sees request and bytes side by side. Same shape assealedPayloadHashversus the validator's extracted message digest: this node states both and says which is which.sign_detachedsetsunsigned_attrs: None, so it must classify asBaselineBand never higher.