SealedEnvelope in dpp-domain now carries conformance_level: Option<SealConformanceLevel>, serialised as conformanceLevel. The engine cannot use it until it repins dpp-domain past the release that contains it (currently pinned at 0.19.0, from crates.io, with no [patch]).
Blocked on that repin. Filing now so the work is not rediscovered.
Why the field exists
SealConformanceLevel documents why the level is on the request: a BaselineB seal stops verifying when its signing certificate expires, ESPR retention outlives certificate lifetimes comfortably, the seal is bought once, and the passport it covers is retention-locked — so the choice cannot be corrected afterwards.
All of that is about the stored seal, and the stored seal did not record it. A node that runs the local backend at B, then a hosted provider at T, then LT, leaves three populations of passport that are indistinguishable in storage — and the ones that will stop verifying first are the ones nobody can identify.
What to change
crates/dpp-seal/src/local/sealer.rs — LocalIdentity::seal sets conformance_level: Some(SealConformanceLevel::BaselineB). It advertises BaselineB only, and sign_detached sets unsigned_attrs: None, so BaselineB is what the bytes are. Not req.conformance_level: the boot gate already refuses a mismatch, so echoing would only launder a bug.
crates/dpp-seal/src/eideasy/client.rs — set it from level_for_profile(&self.config.signature_profile), the same source capabilities() uses. That is the level the client was configured to ask for, which is the honest claim available without reading the returned CMS. Reading it out of the bytes is #TODO-DETECT.
crates/dpp-seal/src/adapter.rs and src/ghost.rs — the ghost echoes the request, matching GhostSeal in dpp-domain.
api/components/schemas/passport/SealedEnvelope.yaml — add an optional conformanceLevel referencing a new SealConformanceLevel.yaml enum (baseline-b, baseline-t, baseline-lt, baseline-lta; kebab-case, matching the serde representation). Optional in both directions: an envelope written before the field reads back absent, and an absent level must be omitted, never serialised as null — a "conformanceLevel": null appearing on a stored passport would be a content change on a retention-locked document.
crates/dpp-vault/src/handlers/seal.rs — add conformance_level to SealResponse, documented in the register the rest of that struct already uses: a record of what was requested, the same standing as sealedPayloadHash, not proof of what arrived. Mirror it in api/components/schemas/seals/SealResponse.yaml.
ops/pg/ — no migration needed. The seal lives inside passport.doc, seal is already on the retention guard's mutable_keys, and the field is additive within that JSON.
Tests worth having
- A seal drained end to end carries the level the backend advertises.
- A stored envelope predating the field still deserialises, with the level absent.
openapi_contract.rs covers the new property in both directions.
The dpp-domain conformance kit already gained rule 6 (seal.misrecorded_level), which fails an adapter whose envelope records a level disagreeing with the request it answered. adapter.rs runs that kit, so an implementation mistake here shows up as a kit failure rather than as a wrong record in production.
SealedEnvelopeindpp-domainnow carriesconformance_level: Option<SealConformanceLevel>, serialised asconformanceLevel. The engine cannot use it until it repinsdpp-domainpast the release that contains it (currently pinned at0.19.0, from crates.io, with no[patch]).Blocked on that repin. Filing now so the work is not rediscovered.
Why the field exists
SealConformanceLeveldocuments why the level is on the request: aBaselineBseal stops verifying when its signing certificate expires, ESPR retention outlives certificate lifetimes comfortably, the seal is bought once, and the passport it covers is retention-locked — so the choice cannot be corrected afterwards.All of that is about the stored seal, and the stored seal did not record it. A node that runs the local backend at
B, then a hosted provider atT, thenLT, leaves three populations of passport that are indistinguishable in storage — and the ones that will stop verifying first are the ones nobody can identify.What to change
crates/dpp-seal/src/local/sealer.rs—LocalIdentity::sealsetsconformance_level: Some(SealConformanceLevel::BaselineB). It advertisesBaselineBonly, andsign_detachedsetsunsigned_attrs: None, soBaselineBis what the bytes are. Notreq.conformance_level: the boot gate already refuses a mismatch, so echoing would only launder a bug.crates/dpp-seal/src/eideasy/client.rs— set it fromlevel_for_profile(&self.config.signature_profile), the same sourcecapabilities()uses. That is the level the client was configured to ask for, which is the honest claim available without reading the returned CMS. Reading it out of the bytes is #TODO-DETECT.crates/dpp-seal/src/adapter.rsandsrc/ghost.rs— the ghost echoes the request, matchingGhostSealindpp-domain.api/components/schemas/passport/SealedEnvelope.yaml— add an optionalconformanceLevelreferencing a newSealConformanceLevel.yamlenum (baseline-b,baseline-t,baseline-lt,baseline-lta; kebab-case, matching the serde representation). Optional in both directions: an envelope written before the field reads back absent, and an absent level must be omitted, never serialised asnull— a"conformanceLevel": nullappearing on a stored passport would be a content change on a retention-locked document.crates/dpp-vault/src/handlers/seal.rs— addconformance_leveltoSealResponse, documented in the register the rest of that struct already uses: a record of what was requested, the same standing assealedPayloadHash, not proof of what arrived. Mirror it inapi/components/schemas/seals/SealResponse.yaml.ops/pg/— no migration needed. The seal lives insidepassport.doc,sealis already on the retention guard'smutable_keys, and the field is additive within that JSON.Tests worth having
openapi_contract.rscovers the new property in both directions.The
dpp-domainconformance kit already gained rule 6 (seal.misrecorded_level), which fails an adapter whose envelope records a level disagreeing with the request it answered.adapter.rsruns that kit, so an implementation mistake here shows up as a kit failure rather than as a wrong record in production.