You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The vault settings load (#870, PR #903) reports why it fell back to defaults — DefaultsReason::{Suppressed, RolledBack, TimedOut, Unreadable} — but nothing acts on any of them yet, and the freshness bound on a cold device is weaker than the rest of the resolve plane's.
Surfaced by the /security-review and /crypto-privacy-review gates on PR #903. None of these is exploitable for plaintext disclosure — the record is HPKE-sealed and only the login-secret holder can sign at its name — but each lets an adversary who controls the record plane revert a member's explicit configuration.
The residuals
1. A degraded load still applies the hosted-pinning default. A member who chose PinMode::External ("never put my bytes in CipherBox's store") gets Hosted any time the record is withheld or its head block is unreachable. Suppressed names that case but there is no last-known-good settings copy to prefer instead, and no policy for what a host should do when it sees it. Wanted: cache the last gate-passing settings behind the existing SnapshotCache-shaped durable store and prefer them over VaultSettings::default(), so suppression degrades to stale but chosen rather than reverted.
2. A fresh device accepts an arbitrarily old settings record. Rollback defence is the per-name sequence floor, which is device-local. A new install has no floor, so any owner-signed record the network serves is admitted — including one captured before the member rotated a BYO access_token, which the engine would then present as a bearer credential to an endpoint they stopped trusting. Two bounds are available:
EOL.IpnsRecord::verify checks the signature chain only; verified.validity is available and net/eol.rs::is_expired exists but has one caller (net/liveness.rs). Checking it on this resolve bounds replay to the 90-day EOL window. Deliberately not done in engine: publish and resolve the vault settings record #870 because no resolve path checks EOL today and special-casing one record would be inconsistent — so the decision is "check it everywhere, or here with a stated reason", and it belongs with this issue rather than that slice.
A body revision. The seal binds [domain, v, structTag, ownerTag] — no name, no sequence, no time — so every settings record the owner ever wrote opens forever. A monotonic revision inside the sealed body, with the highest seen persisted per account rather than per device, survives a fresh floor store and is the only bound that does.
3. A same-sequence fork stays admissible forever.publish mints floor + 1 and the floor advances only behind a confirmed publish, so an Unconfirmed publish followed by a retry yields two distinct owner-signed records at the same sequence. The load admits >= floor with no byte comparison, so a chosen-record adversary picks which one every device sees, permanently. The body revision in (2) disambiguates this; the outer sequence cannot.
Scope
Durable last-known-good settings, and a stated host policy for Suppressed / RolledBack.
The EOL decision for the resolve plane, applied consistently.
A monotonic revision in the settings body (a SETTINGS_RECORD_V bump or a body-schema addition, plus its KAT vectors).
Tests: a withheld record must not downgrade External to Hosted; a fresh device must refuse a record below the account's highest known revision; an expired EOL is not authoritative.
Gate: Engine Tests (plus Core KATs if the body schema changes).
Split
Decomposed by the scoping pass in this issue's comment thread. This issue is now an umbrella; the work lands in its slices.
Two corrections the scoping pass established, against this issue's original framing:
A body revision is not "the only bound that survives a fresh floor store" — there is no per-account durable store in the engine at all, so EOL is the only bound that survives a fresh device.
The revision costs noSETTINGS_RECORD_V bump and no KAT regeneration: crates/core treats the settings body as opaque (crates/core/src/seal/settings_record.rs:21).
engine: wire the pin-provider layer across all three modes #871 — it is the first code to dispatch on PinMode, so the silent External -> Hosted downgrade under a withheld record becomes reachable there. Only the durable last-known-good slice is a hard prerequisite.
The vault settings load (#870, PR #903) reports why it fell back to defaults —
DefaultsReason::{Suppressed, RolledBack, TimedOut, Unreadable}— but nothing acts on any of them yet, and the freshness bound on a cold device is weaker than the rest of the resolve plane's.Surfaced by the
/security-reviewand/crypto-privacy-reviewgates on PR #903. None of these is exploitable for plaintext disclosure — the record is HPKE-sealed and only the login-secret holder can sign at its name — but each lets an adversary who controls the record plane revert a member's explicit configuration.The residuals
1. A degraded load still applies the hosted-pinning default. A member who chose
PinMode::External("never put my bytes in CipherBox's store") getsHostedany time the record is withheld or its head block is unreachable.Suppressednames that case but there is no last-known-good settings copy to prefer instead, and no policy for what a host should do when it sees it. Wanted: cache the last gate-passing settings behind the existingSnapshotCache-shaped durable store and prefer them overVaultSettings::default(), so suppression degrades to stale but chosen rather than reverted.2. A fresh device accepts an arbitrarily old settings record. Rollback defence is the per-name sequence floor, which is device-local. A new install has no floor, so any owner-signed record the network serves is admitted — including one captured before the member rotated a BYO
access_token, which the engine would then present as a bearer credential to an endpoint they stopped trusting. Two bounds are available:IpnsRecord::verifychecks the signature chain only;verified.validityis available andnet/eol.rs::is_expiredexists but has one caller (net/liveness.rs). Checking it on this resolve bounds replay to the 90-day EOL window. Deliberately not done in engine: publish and resolve the vault settings record #870 because no resolve path checks EOL today and special-casing one record would be inconsistent — so the decision is "check it everywhere, or here with a stated reason", and it belongs with this issue rather than that slice.[domain, v, structTag, ownerTag]— no name, no sequence, no time — so every settings record the owner ever wrote opens forever. A monotonicrevisioninside the sealed body, with the highest seen persisted per account rather than per device, survives a fresh floor store and is the only bound that does.3. A same-sequence fork stays admissible forever.
publishmintsfloor + 1and the floor advances only behind a confirmed publish, so anUnconfirmedpublish followed by a retry yields two distinct owner-signed records at the same sequence. The load admits>= floorwith no byte comparison, so a chosen-record adversary picks which one every device sees, permanently. The body revision in (2) disambiguates this; the outer sequence cannot.Scope
Suppressed/RolledBack.SETTINGS_RECORD_Vbump or a body-schema addition, plus its KAT vectors).ExternaltoHosted; a fresh device must refuse a record below the account's highest known revision; an expired EOL is not authoritative.Gate:
Engine Tests(plusCore KATsif the body schema changes).Split
Decomposed by the scoping pass in this issue's comment thread. This issue is now an umbrella; the work lands in its slices.
Two corrections the scoping pass established, against this issue's original framing:
SETTINGS_RECORD_Vbump and no KAT regeneration:crates/coretreats the settings body as opaque (crates/core/src/seal/settings_record.rs:21).Depends on
Blocks
PinMode, so the silentExternal->Hosteddowngrade under a withheld record becomes reachable there. Only the durable last-known-good slice is a hard prerequisite.Part of #655