Skip to content

PRD-02: Clear-signing v2 + message-signing intents #46

Closed
0 of 1 issue completed
Closed

Description

@hellno

Part of #44 (ADR 0001). Phase 1b. Extends the shared clear-signing engine and the Intent surface to off-chain signatures — the real drainer vector. Independent of transport; needed by PRD-03 (curated swap needs Permit2/EIP-712) and PRD-04.

Why

A dapp's most dangerous request is an off-chain signature that moves value with no on-chain tx the wallet simulates. In 2024, permit (56.7%) + setOwner (31.9%) = 88.6% of drainer losses ($494M) (research §30). Deckard can sign one structured payload today (SwapOrder/SignOrder, EIP-712) but has no general message-signing path and no decode/clear-signing for arbitrary typed data.

Goals

  • Add personal_sign (EIP-191) + eth_signTypedData_v4 (EIP-712) signing, routed propose → Decision → human approval, never auto-allowed.
  • Decode + clear-sign before the human: EIP-712 domain binding (chainId + verifyingContract); recognize high-risk shapes (permit, Permit2, Seaport, setOwner/ownership transfer); a distinct alarming screen for EIP-7702 delegation.
  • Consume ERC-7730 descriptors where available; explicit "blind signing — can't decode this" fallback for the uncovered long tail.
  • Hard-handle chainId-mismatch + unknown verifyingContract (warn loudly — most wallets silently don't, research §33).

Non-goals

The transport delivering these (PRD-04); building the ERC-7730 registry (we consume); supporting eth_sign (raw 32-byte blind hash) — refuse outright (research §31).

Design

Contract (deckard-contract), mirror the SwapOrder/SignOrder/PendingPayloadView pattern (don't overload Intent, a tx shape):

  • SignerRequest::ProposeMessage { message: SignMessage } → Decision; SignMessage { request_id } → SignMessageResult { signature }. Never broadcasts.
  • SignMessage enum: PersonalSign { bytes } (decode UTF-8 or hex + caution); TypedDataV4 { typed_data } (parse domain + primaryType).
  • PendingPayloadView::Message(SignMessageView) so the GUI inbox (PendingList) renders it.
  • New deny_reasons: ETH_SIGN_REFUSED, CHAINID_MISMATCH, DELEGATION_REFUSED, UNDECODABLE_TYPED_DATA.
  • Parse untrusted typed-data via the bounded Reader (strict length/depth caps; no OOM/recursion).

Decision logic (policy.rs): pure evaluate_message(&SignMessage, &Policy, wallet, now) -> Decision next to evaluate/evaluate_order (keep mock⇄daemon parity). Messages always NeedsApproval in v1. EIP-712: deny on CHAINID_MISMATCH; flag unknown verifyingContract; danger-flag (red, top of card) unlimited/large permit & Permit2, Seaport, owner-transfer.

EIP-7702 (research §34): refuse delegation by default (DELEGATION_REFUSED) — its own spec says "there is no safe way" to sign arbitrary delegations. If ever supported: curated trusted-delegator allowlist + a distinct "you are handing control of your account to " screen.

Card (deckard-app): extend the shared clear-signing card to render SignMessageView — plain-language headline, one canonical key/value list, danger red at top, deliberate hold. ERC-7730 descriptor renders human-readable fields; otherwise the explicit blind-sign caution (demoted-but-present). Origin shown unverified.

wallet_addEthereumChain/switchEthereumChain guard (research §36): sign only with user-submitted chainId, never an RPC-returned one; confirm requester + target chain (daemon invariant here; UI lands with PRD-04).

Acceptance tests

eth_sign_refused; typed_data_chainid_mismatch_denies; permit_flagged_danger; delegation_refused; message_never_broadcasts; undecodable_typed_data_bounded (fuzz vector); evaluate_message mock/daemon parity; transcript hygiene (no message bytes/signature in reason strings).

DoD

Shared DoD (#44) plus: ⌘K commands for user-initiated signing; DESIGN.md card spec honored (screenshot in PR); EIP-7702 refusal + blind-sign fallback documented in THREAT-MODEL.md (new message-signing surface).

Sources: research §30–36; EIP-712/-7702/-3085/-3326, ERC-7730; MetaMask MIP-3; scamsniffer 2024; existing swap_order.rs/SignOrder.

Activity

  1. hellno commented on Jun 14, 2026

    @hellno
    OwnerAuthor

    Cross-link to #33: PRD-02's "refuse EIP-7702 delegation by default; if ever supported, curated trusted-delegator allowlist" and #33's session-key grant are the same 7702-authorization primitive from two entry points — dapp/message-requested (refuse) vs. Deckard self-originated (allow-if-scoped).

    Suggest they share one 7702-auth-signing chokepoint carrying #33's invariants, rather than two paths that can drift:

    • hardcoded/versioned audited-delegate allowlist (user/dapp-supplied delegates rejected unconditionally),
    • unconditional chain_id = 0 refusal,
    • self-sponsored nonce + 1 rule.

    Then PRD-02's DELEGATION_REFUSED default reads as "refuse unless the delegate is on #33's allowlist and the grant is self-originated" — keeping one allowlist + one set of invariants instead of two implementations to keep in sync.


    Generated by Claude Code

  2. added
    eip-1193Injected provider / browser bridge roadmap
    signingMessage/typed-data/transaction signing surface
    securitySecurity-relevant
    on Jun 23, 2026
  3. added a commit that references this issue on Jun 23, 2026
    e543dbf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    eip-1193Injected provider / browser bridge roadmapsecuritySecurity-relevantsigningMessage/typed-data/transaction signing surface

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions