Skip to content

feat(security): #759 signed-contact leg — restore wallet-or-signed-contact allowance behind a sound trust root (B1 + §6.1 + §6.2 prereqs) #773

Description

@angelazhy

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: 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)

  1. 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.
  2. §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.
  3. §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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsecurity_findingSecurity gap surfaced via adversarial smoke-testtrackingGated on an external precondition; not actionable today

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions