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
#759 ships a wallet-only recipient hard gate (recipient === tx.from). The signed-contact leg — allowing a hard-gated protocol recipient to resolve to a device-signed saved contact, not just the wallet — was deferred here because SEC's round-3 review (#759 comment) found the signed-contact trust root unsound at its foundation: verifyEvmBlob recovers a signature against blob.anchorAddress (src/contacts/verify.ts:221-226), a field inside the signed payload, and no code in src/contacts/ binds it to the paired device. An attacker self-signs a blob naming their own address and it verifies state: "match".
This issue tracks restoring the wallet-or-signed-contact allowance behind a sound trust root. It has ZERO functional cost to defer today — every current protocol builder hardcodes recipient=wallet, so no tool exercises the contact leg.
Prerequisites (all three required before the contact leg can be admitted to the hard gate)
B1 — device-anchor binding.blob.anchorAddress must be bound to the live device anchor (getConnectedAccounts()), and REFUSE when the device is unavailable (fail-closed). The binding must land in the verification primitive both call paths share (so reverseLookup inherits it — it currently calls verifyEvmBlob directly, skipping validateChainBlob). Falsifier: seed contacts.json with a blob signed by a non-device key → reverseLookup returns non-match and the predicate is FALSE, on a cold session AND mid-session.
§6.1 — single-entry-delta approval. Every add_contact/remove_contact re-signs the whole entry set (src/contacts/index.ts:321/332/344) with a fresh version+timestamp, so an entry the user missed is silently re-approved by every later legitimate op. A signed contact should satisfy the hard gate only when its approval was a single-entry delta recorded as such (or an equivalent per-entry confirmation). ChainBlob.entries has no size bound.
§6.2 — proportionate clear-sign treatment (BTC-path model, Inv Release v0.2.0 #8). The repo establishes NOTHING about what Ledger renders for the contacts personal_sign (three prose comments, no firmware artifact/SDK contract/test). Do NOT build the gate on unverified device rendering. Adopt the sign_message_btc treatment: emit the exact preimage + its sha256 to chat before the device prompt with a render directive, bound total entry count / blob size, and state in the receipt that this signature now authorizes fund destinations. Project CLAUDE.md §Typed-Data Signing Discipline binds here in proportionate form.
Adjacent code/doc defects to co-merge (asserting a control that does not exist)
src/signers/contacts/evm.ts:21-24 claims "on read-time verification we re-check that the anchor matches the current session's first account" — no such re-check exists. Correct or it will be cited to reverse the B1 deferral.
SECURITY.md:218 claims the anchor is "captured into in-memory state on session start" — it is captured on the first disk read. SECURITY.md:228/:230 state the contacts signature is a UX/tamper-evidence layer, not the canonical recipient check — must be amended if the contact leg ships as a hard-gate trust root.
Owner: SEC (trust-model / prerequisites 1-3) → DEV (implementation). Refs: #759 (the wallet-only design), PR #765. Do NOT implement until B1's device-binding lands.
Context
#759 ships a wallet-only recipient hard gate (
recipient === tx.from). The signed-contact leg — allowing a hard-gated protocol recipient to resolve to a device-signed saved contact, not just the wallet — was deferred here because SEC's round-3 review (#759 comment) found the signed-contact trust root unsound at its foundation:verifyEvmBlobrecovers a signature againstblob.anchorAddress(src/contacts/verify.ts:221-226), a field inside the signed payload, and no code insrc/contacts/binds it to the paired device. An attacker self-signs a blob naming their own address and it verifiesstate: "match".This issue tracks restoring the wallet-or-signed-contact allowance behind a sound trust root. It has ZERO functional cost to defer today — every current protocol builder hardcodes recipient=wallet, so no tool exercises the contact leg.
Prerequisites (all three required before the contact leg can be admitted to the hard gate)
blob.anchorAddressmust be bound to the live device anchor (getConnectedAccounts()), and REFUSE when the device is unavailable (fail-closed). The binding must land in the verification primitive both call paths share (soreverseLookupinherits it — it currently callsverifyEvmBlobdirectly, skippingvalidateChainBlob). Falsifier: seedcontacts.jsonwith a blob signed by a non-device key →reverseLookupreturns non-matchand the predicate is FALSE, on a cold session AND mid-session.add_contact/remove_contactre-signs the whole entry set (src/contacts/index.ts:321/332/344) with a fresh version+timestamp, so an entry the user missed is silently re-approved by every later legitimate op. A signed contact should satisfy the hard gate only when its approval was a single-entry delta recorded as such (or an equivalent per-entry confirmation).ChainBlob.entrieshas no size bound.personal_sign(three prose comments, no firmware artifact/SDK contract/test). Do NOT build the gate on unverified device rendering. Adopt thesign_message_btctreatment: emit the exact preimage + its sha256 to chat before the device prompt with a render directive, bound total entry count / blob size, and state in the receipt that this signature now authorizes fund destinations. ProjectCLAUDE.md§Typed-Data Signing Discipline binds here in proportionate form.Adjacent code/doc defects to co-merge (asserting a control that does not exist)
src/signers/contacts/evm.ts:21-24claims "on read-time verification we re-check that the anchor matches the current session's first account" — no such re-check exists. Correct or it will be cited to reverse the B1 deferral.SECURITY.md:218claims the anchor is "captured into in-memory state on session start" — it is captured on the first disk read.SECURITY.md:228/:230state the contacts signature is a UX/tamper-evidence layer, not the canonical recipient check — must be amended if the contact leg ships as a hard-gate trust root.Owner: SEC (trust-model / prerequisites 1-3) → DEV (implementation). Refs: #759 (the wallet-only design), PR #765. Do NOT implement until B1's device-binding lands.