Skip to content

ADR 0004: vault rollback resistance — evaluated, deferred (#71) - #119

Merged
hellno merged 2 commits into
mainfrom
hellno/rollback-resistant-anchor
Jun 20, 2026
Merged

ADR 0004: vault rollback resistance — evaluated, deferred (#71)#119
hellno merged 2 commits into
mainfrom
hellno/rollback-resistant-anchor

Conversation

@hellno

@hellno hellno commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Outcome of the #71 spike. After an empirical dependency-cost check, a fan-out adversarial review (16 attacks landed), and an independent Codex cross-model adjudication — all pointing the same way — the conclusion is to not build this, and to record why so it isn't re-proposed.

This PR is now doc-only: docs/adr/0004-rollback-resistant-state-anchor.md (a plain Deferred record) + one reworded THREAT-MODEL.md residual row. The earlier unwired prototype, its feature flag, and the lib wiring have been removed.

Why defer

  • Deckard's threat model already concedes same-uid (incl. filesystem write) as trusted. Rollback is a same-uid filesystem-write attack, so the most any of this defends is an attacker weaker than the one we already concede.
  • For the vault specifically, the value is thin even against that weaker attacker: the seed is constant across re-seals and balances are on-chain, so rolling vault.bin back only reverts a passphrase/KDF rotation — and a same-uid attacker during an unlocked session already reads the live seed from RAM.
  • The mechanism doesn't hold anyway: nothing legitimately advances the vault counter; a sidecar counter is replayable; the anchor is same-uid-deletable/rewritable; only a TPM NV counter is decrement-proof, and macOS has no app-facing equivalent (a Linux-only promise).

What changes for the siblings

The "one shared mechanism for three issues" framing was the error. #72 and #108 do not depend on #71 and never needed an anchor:

This supersedes the shared-foundation sequencing in ADR 0003 (items #4#6); that ADR's findings/ranking stand.

Refs #71 #72 #108 — closes none of them. #71 stays open as a parked issue carrying blockers B1–B3; #72/#108 proceed decoupled (their comments/bodies updated).

DoD

Doc-only + a code removal; cargo fmt --check and cargo clippy -p deckard-signerd -- -D warnings green; Cargo.lock unchanged.

Spike deliverable for #71: design note answering all five spike questions, the
keystone primitive #72/#108 build on, an honest residual, and a small unwired
reference implementation behind an off-by-default feature.

Method: empirical dep-cost measurement (cargo tree on macOS, diffed vs the
workspace lock) + a fan-out research/adversarial-verification pass whose skeptics
landed 16 attacks that reshaped the conclusion.

Q1 (anchor crate): recommend `keyring` (pinned, per-OS native, default-features
off) on macOS/Windows, file-only on Linux; measured cost is +1 crate on macOS,
+4 (incl. the libdbus C lib) on Linux where the D-Bus Secret Service is also
absent on the headless boxes signerd runs on. NEW DEP — approval-gated, NOT added
here.

Q2 (correctness): binding the epoch into core_bytes() so both AEAD tags cover it
is the only construction that makes the file's epoch un-editable-without-passphrase
— but it is a FORMAT_VERSION->2 change that breaks the frozen KAT, so it is
specified and deferred, not landed. Check strictly after AEAD verify (no oracle to
an unauthenticated caller); key on a re-seal-stable identity, not the per-seal
random vault_id.

Q3/Q4 (restore + residual): fail-open-with-confirm on the Control capability for
testnet alpha; honest that delete-anchor->bootstrap bypasses the gate and that
file-only Linux gives ~zero adversarial resistance. THREAT-MODEL residual row #7
added.

Q5 (generalization): one StateAnchor over a namespaced record (vault/policy/cap),
not a global counter; atomic+fsync+single-writer; the real consumers are #72
(policy version+MAC, fields that must be added first) and #108 (cap
reserve-before-sign, with the intra-generation gap and clock-rollback corrected).
The standalone vault detector is vacuous today (nothing bumps the vault epoch), so
build order is: primitive -> policy/cap -> vault (v2).

Prototype: signerd `state_anchor.rs` behind off-by-default `state-anchor`,
unwired, zero new deps, 7 tests. DoD green (fmt, `just check` default+tray, `cargo
test --workspace`); featured clippy+tests run separately.

Refs #71 #72 #108. Relates ADR 0003 (#105).
Codex cross-model adjudication + the adversarial review agree the rollback
anchor is mispriced for a testnet alpha whose threat model already concedes
same-uid (incl. filesystem write). Vault rollback's entire worst case is
reverting a passphrase/KDF rotation; the seed is constant and balances are
on-chain; and the mechanism doesn't hold anyway (nothing advances the vault
counter; sidecar is replayable; the anchor is same-uid-deletable/rewritable;
only a TPM NV counter holds, and macOS has no equivalent).

So: defer #71 with blockers B1-B3, build NO cross-trust-domain anchor, and
decouple #72/#108 into independent local fixes (policy MAC + fail-closed;
durable cap + reserve-before-sign) that never needed it. The "one shared
mechanism for three issues" framing was the error; this supersedes ADR 0003
items #4-#6's sequencing.

- Rewrite ADR 0004 as a plain Deferred record (no invented vocabulary).
- Remove the unwired StateAnchor prototype, its feature flag, and lib wiring.
- Reword THREAT-MODEL residual #7 to "accepted residual, deferred".

Refs #71 #72 #108. Supersedes ADR 0003 (#105) keystone sequencing.
@hellno hellno changed the title ADR 0004: rollback-resistant security-state anchor (#71 keystone spike) ADR 0004: vault rollback resistance — evaluated, deferred (#71) Jun 20, 2026
@hellno
hellno merged commit 6465faa into main Jun 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant