From 6463ead6ae16ace91c72e1d74ab9e9159857f7b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 10:26:37 +0000 Subject: [PATCH 1/4] docs: dapp connectivity research, ADR, and PRD series Add the decision record and execution plan for how Deckard connects to third-party dapps, grounded in a five-angle deep-research synthesis. - docs/research/10-dapp-connectivity.md: cited evidence (extension+bridge, WalletConnect, embedded webview, the resolver-auth crux, clear-signing). - docs/adr/0001: reject the embedded browser; reuse the key-less-proposer -> policy-gating-daemon -> native clear-signing pattern in three phases. - docs/prd/01..05: resolver authentication (closes threat-model residual #1), clear-signing v2 + message-signing intents, curated native integrations, WalletConnect transport, per-origin permissions + registry. Each carries a Definition of Done tied to the project gates. https://claude.ai/code/session_01Vp5m1a3P8XePLbyppqzcjN --- .../0001-dapp-connectivity-architecture.md | 137 ++++++++++ docs/prd/01-resolver-authentication.md | 126 +++++++++ .../02-clear-signing-and-message-intents.md | 134 ++++++++++ docs/prd/03-curated-native-integrations.md | 98 +++++++ docs/prd/04-walletconnect-transport.md | 131 ++++++++++ .../05-per-origin-permissions-and-registry.md | 110 ++++++++ docs/prd/README.md | 40 +++ docs/research/10-dapp-connectivity.md | 240 ++++++++++++++++++ 8 files changed, 1016 insertions(+) create mode 100644 docs/adr/0001-dapp-connectivity-architecture.md create mode 100644 docs/prd/01-resolver-authentication.md create mode 100644 docs/prd/02-clear-signing-and-message-intents.md create mode 100644 docs/prd/03-curated-native-integrations.md create mode 100644 docs/prd/04-walletconnect-transport.md create mode 100644 docs/prd/05-per-origin-permissions-and-registry.md create mode 100644 docs/prd/README.md create mode 100644 docs/research/10-dapp-connectivity.md diff --git a/docs/adr/0001-dapp-connectivity-architecture.md b/docs/adr/0001-dapp-connectivity-architecture.md new file mode 100644 index 0000000..a04e375 --- /dev/null +++ b/docs/adr/0001-dapp-connectivity-architecture.md @@ -0,0 +1,137 @@ +# ADR 0001 — Dapp connectivity architecture + +- **Status:** Proposed (2026-06-14) +- **Deciders:** @hellno (maintainer) +- **Context inputs:** [`docs/research/10-dapp-connectivity.md`](../research/10-dapp-connectivity.md) + (cited evidence), `THREAT-MODEL.md`, `SECURITY.md`, `DESIGN.md`, `docs/build/30-mcp-shape.md`. +- **Supersedes / relates:** establishes the umbrella decision the `docs/prd/*` series executes. + +## Decision drivers (agreed with the maintainer) + +Captured via a requirements pass before research: + +1. **Mobile horizon — "don't compromise desktop for it."** Optimize for macOS/Linux; keep mobile + *possible* but don't let it dictate the desktop design. → favors transports that can survive onto + mobile over desktop-only ones, without over-investing now. +2. **Dapp openness — "curated/allowlisted first."** Start with a vetted set (swaps, bridges); on-brand + "calm sovereign control, not a casino"; per-origin scope; expand later. +3. **Browser engine — "avoid; keep wallet minimal."** Connect to the user's own browser and/or a relay + rather than bundling an engine. Preserves the hardened minimal-core identity. +4. **Deliverable — "ADR first, then multiple PRDs"** with per-workstream Definitions of Done. + +Plus the standing constraint from `SECURITY.md`: Deckard is `0.0.1-alpha`, unaudited, single-maintainer, +testnet-keys-only. Any new attack surface is sequenced *behind* an audit. + +## Context + +Deckard's security model (`THREAT-MODEL.md`) is a hub: **the key lives in `deckard-signerd`; every +other process is an untrusted *proposer* that submits a typed `Intent`; the daemon's policy gate +decides; the human approves via a native clear-signing card.** `deckard-mcp` is the reference proposer +(key-less; no `propose`-arbitrary, no `resolve`). The contract is frozen in `deckard-contract` +(`Intent`/`Decision`/`Policy`/`SignerRequest`). + +Two facts in the current code shape this decision: + +- **`crates/deckard-signerd/src/auth.rs` gates only on `same_uid()`.** Per the threat model's residual + risk #1, `Resolve{approved:true}` is honored from *any* same-uid caller. This is fine for today's + small, human-controlled proposer set; it is **not** fine once a browser-reachable proposer exists. + The research confirms this is the textbook confused-deputy gap (`research §22–24`). +- **Message-signing scaffolding already exists** — `SwapOrder`/`SignOrder` (EIP-712), + `PendingPayloadView::Approve{token,spender,amount}`, the shaped-approve gate. Dapp signing *extends* + this, it doesn't start from zero. + +The user's two original options map to: **A = embedded in-app webview**, **B = external browser +extension**. Research surfaced a third transport, **WalletConnect v2** (relay; desktop + mobile; no +extension), and a crux beneath all three (the resolver-authentication boundary). + +## Decision + +**1. Reject the embedded in-app browser (Option A). Deckard does not bundle a browser engine.** +Rationale (`research §16–21`): `wry` means three engines (WebView2/WKWebView/WebKitGTK) to track; +WebKitGTK on Linux patches via distro lag with default-*off* renderer sandboxing and a live 2025 +ACE-class CVE stream; Tauri's own docs say "don't load untrusted remote content" and have shipped an +IPC-bypass CVE for exactly that. It imports the largest, worst-patched TCB next to the keys to +replicate a *mobile-only* pattern that exists because phones lack the extension/external-browser +options desktop already has. This contradicts decision driver #3 and the `deckard-core` +`#![forbid(unsafe_code)]` minimal-core ethos. + +**2. Adopt a phased model that reuses the existing key-less-proposer → policy-gating-daemon → native +clear-signing pattern. No new trust boundary is invented; we extend the one we have.** + +- **Phase 0 — Curated native integrations (no web transport).** Realize "curated dapps first" + (driver #2) as *native* Deckard surfaces that build typed `Intent`/`SwapOrder`s directly in Rust — + exactly how Shield (Railgun) and the CoW swap path already work. No dapp connection, no browser, no + relay, no new external proposer. Highest security, lowest new surface, fully on-brand. → **PRD-03**. +- **Phase 1 — Foundational hardening (prerequisite for *any* external proposer).** + - **1a. Resolver authentication.** The daemon hands the GPUI app it already spawns + (`supervise.rs`) an **unforgeable approval capability** (a `socketpair()` end / `SCM_RIGHTS`-passed + fd); `Resolve` is accepted *only* on that channel; the public proposer socket can no longer + self-approve. Closes residual-risk #1 (`research §22–28`). → **PRD-01**. + - **1b. Clear-signing v2 + message-signing intents.** Extend the `Intent` surface to off-chain + signatures (`personal_sign`, EIP-712) with decode, domain binding, permit/Permit2/Seaport + recognition, EIP-7702 handling, and ERC-7730 consumption with a safe blind-sign fallback + (`research §30–36`). Phase 0's swap already needs Permit2/EIP-712, so this is not gated on Phase 2. + → **PRD-02**. +- **Phase 2 — Generic dapp connectivity, when warranted (post-audit).** **WalletConnect v2 is the + primary generic transport**, implemented as a new key-less proposer process mirroring `deckard-mcp`, + with CAIP-25 scope negotiation, Verify-API origin attestation, per-origin policy, a curated + registry + anti-phishing blocklist, and explicit relay-privacy mitigations. → **PRD-04** (transport) + + **PRD-05** (per-origin permissions & registry). + - **A browser extension (Option B) is a secondary, optional desktop-convenience add-on**, not the + primary path: it is desktop-only (doesn't reach mobile, driver #1) and the extension artifact is a + recurring drainer/supply-chain target (`research §6`). If ever built, it is a thin EIP-6963 + proposer that speaks the *same* WalletConnect-or-native bridge — never the daemon wire directly, + and never with `resolve` capability. Tracked as a future PRD, not in this batch. + - The embedded webview stays rejected. + +**3. Cross-cutting invariants every phase inherits (non-negotiable):** +- New proposers are **key-less**, reuse `deckard-contract`, and **cannot** `resolve` or submit a raw + arbitrary `Intent` that bypasses a typed builder (the `deckard-mcp` rule). +- **Origin is displayed as attacker-controllable** (unverified unless independently attested); the card + always clear-signs *actual effects*, never a claimed dapp name (`research §29`). +- Every new user action registers a ⌘K `Command` (CLAUDE.md §Command palette reachability). +- The mainnet guardrail and STOP semantics (`THREAT-MODEL.md`) apply unchanged to every new write path. + +## Consequences + +**Positive** +- The security boundary is *strengthened* before any surface is added (PRD-01 fixes a standing + accepted risk regardless of connectivity). +- The minimal-core identity and offline-first posture are preserved (no engine; Phase 0/1 need no + network relay at all). +- Phasing matches the alpha → audit → 1.0 trajectory: ship the safest, smallest thing first. +- WalletConnect gives a single transport that reaches desktop today and mobile later, so the eventual + mobile app doesn't force an architecture rewrite (honors driver #1 cheaply). + +**Negative / costs** +- WalletConnect imports a **centralized relay + Project-ID dependency** (privacy/offline-first tension, + `research §10–11`) and a **non-trivial in-house Rust Sign-protocol build** (no maintained Rust + wallet SDK, `research §14`). PRD-04 must address relay-egress privacy and the build/borrow decision. +- Declining the embedded browser means Deckard never offers a fully in-app "open any dapp" experience; + the curated-native + WalletConnect combination is the deliberate substitute. +- The extension being deprioritized means desktop-browser dapps that only speak injected + `window.ethereum` (not WalletConnect) aren't reachable until/unless the optional extension ships. + +## Alternatives considered (and rejected) + +- **Embedded webview (Option A as clarified):** rejected — see Decision §1. +- **Browser extension as the *primary* transport (Option B):** rejected as primary — desktop-only and + a supply-chain liability; retained only as an optional secondary add-on. +- **Frame-style localhost RPC bridge (`ws://127.0.0.1:1248`):** rejected — web-reachable + (DNS-rebinding / cross-site WebSocket hijacking), defended only at the UI layer (`research §2`). +- **In-process plugins / dylibs loaded into the app or daemon:** rejected outright — same-uid code + with arbitrary execution lands on the *trusted* side of the boundary and can self-approve; this is + the maximal form of residual-risk #1. (Extensibility, if wanted, must be sandboxed, key-less, + intent-only — never in-process native code.) + +## Open questions (resolved inside the PRDs) + +- PRD-01: `socketpair` inheritance vs `SCM_RIGHTS` fd-pass vs `0600` cookie — pick the most portable + capability for macOS + Linux given `supervise.rs` already spawns the daemon (note the + daemon-spawns-app vs app-spawns-daemon direction inversion). +- PRD-02: which `IntentKind`s to add (`SignMessage`, `SignTypedData`, and whether EIP-7702 `Delegate` + is supported-behind-allowlist or refused outright). +- PRD-04: build the WC Sign layer in Rust vs run a sidecar (a key-less WC↔daemon translator akin to + `deckard-mcp`); relay privacy egress; whether a custom/self-hosted relay URL is exposed. +- PRD-05: registry format (adopt ERC-7730 descriptors directly?) and how the curated allowlist ships + and updates offline-first. diff --git a/docs/prd/01-resolver-authentication.md b/docs/prd/01-resolver-authentication.md new file mode 100644 index 0000000..c5ed505 --- /dev/null +++ b/docs/prd/01-resolver-authentication.md @@ -0,0 +1,126 @@ +# PRD-01 — Resolver authentication (capability-gated `Resolve`) + +> Phase 1a of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). Closes `THREAT-MODEL.md` +> residual-risk #1. **Foundational and independent** — has standalone security value even if no dapp +> connectivity ever ships, and is a hard prerequisite for PRD-04. + +## Why this exists + +Today `crates/deckard-signerd/src/auth.rs` authorizes a connection on `same_uid()` alone. Because +`Resolve{request_id, approved:true}` is just another frame on the same socket, **any same-uid process +can approve a pending request** — including, in future, a browser-reachable proposer. The research +(`docs/research/10-dapp-connectivity.md §22–28`) confirms this is the classic confused-deputy problem: +uid is *ambient authority*; peer-cred proves *who* connected but not *which role*. Every hardware/ +companion signer (Trezor, Ledger, GridPlus, Frame) separates *request* (promiscuous endpoint) from +*approval* (a surface the requester cannot drive). Deckard must manufacture the same split in software. + +Right now the risk is *accepted* because the proposer set is small and human-controlled. PRD-04 breaks +that assumption. This PRD must land first. + +## Goals + +- The GPUI app is the **sole** principal that can send `Resolve` / `RevokeAll`-as-approval-grant. +- Approval authority is an **unforgeable capability** the daemon hands only to the app process it + controls — not derivable by any other same-uid process. +- The public proposer socket continues to accept *proposals and reads* from same-uid callers + (unchanged), but **rejects `Resolve`** with a typed error. +- STOP/`RevokeAll` (the brake) remains reachable from *any* transport (it can only ever reduce + authority, never grant it) — see Non-goals. + +## Non-goals + +- Multi-user / multi-principal daemon (still single-uid). Salted request-ids (residual-risk #6) are a + separate future item. +- Changing the keystore, mainnet guardrail, or STOP-latency behavior. +- Authenticating the *dapp origin* (that's PRD-05). This PRD authenticates the **resolver**, not the + requester. + +## Design + +### The capability channel + +`crates/deckard-signerd/src/supervise.rs` already has the app **spawn + supervise the daemon child**. +That direction matters: the *app* is the parent. Two viable capability mechanisms (pick per +portability, decide in implementation and record in the PRD's decision note): + +1. **`socketpair()` inherited by the daemon child (recommended).** Before `Command::spawn`, the app + creates an `AF_UNIX` `SOCK_SEQPACKET`/`SOCK_STREAM` pair; it passes one end to the child as an + inherited fd (e.g. via `CommandExt::pre_exec` / explicit fd inheritance) and keeps the other. This + **control channel** is the only place the daemon accepts `Resolve`. No other process can obtain the + app's end (`research §26`, kernel inheritance). Portable across Linux + macOS. +2. **`SCM_RIGHTS` fd-pass after connect.** App connects to the public socket, the daemon passes back a + dedicated control fd via ancillary data; thereafter `Resolve` is accepted only on that fd. Slightly + more wire complexity; also portable (`research §25, 27`). + +**Fallback (weaker, only if neither fd path is feasible):** a per-launch random cookie written `0600` +by the daemon, read by the app, presented on `Resolve`. Weaker because any process that can read the +user's files can steal it — prefer an fd capability. Document explicitly if used. + +### Daemon changes (`crates/deckard-signerd`) + +- `auth.rs`: keep `same_uid()` for the public socket (defense-in-depth + logging). Add the concept of a + **control channel** that is authenticated by construction (the inherited/passed fd), not by uid. +- `server.rs` / `daemon.rs`: route `SignerRequest::Resolve` **only** from the control channel. A + `Resolve` arriving on the public socket returns a typed denial (new `deny_reasons` entry, e.g. + `RESOLVE_NOT_AUTHORIZED`) — URL-redacted, no payload echo, per existing transcript-hygiene rules. +- Keep `RevokeAll`/STOP reachable on every channel (it only zeroizes; it cannot grant). Add a test + asserting STOP from the public socket still works while `Resolve` from it is refused. +- `peer_uid` staleness caveat (`research §24`/`peercred` timing): capture creds at accept; do not + re-trust across the connection lifetime. + +### App changes (`crates/deckard-app`) + +- `supervise.rs` + `shell.rs`: establish the control channel at spawn; thread its handle to the place + that today sends `Resolve` after a completed hold-to-confirm (`shell.rs:142` neighborhood). The + hold-to-confirm contract is unchanged — only the *channel* the `Resolve` rides changes. + +### Contract changes (`crates/deckard-contract`) + +- No change to `SignerRequest::Resolve`'s shape is required if the channel (not the frame) carries the + authority. If a token/cookie fallback is used, add it as a typed field and ensure its `Debug` is + redacted. Prefer the no-wire-change fd approach. + +## Cross-platform notes + +- Linux: `SO_PEERCRED`/`socketpair`/`SCM_RIGHTS` all available. +- macOS: no `SO_PEERCRED` (use `LOCAL_PEERCRED`; pid via `LOCAL_PEERPID`); `socketpair`/`SCM_RIGHTS` + available — which is why the fd-capability approach is the portable primitive (`research §27`). +- `unsafe` for raw fd handling: confine to the **app crate** (`unsafe_code = "deny"` → add a scoped, + documented `#[allow(unsafe_code)]` with a `// reason:` comment, matching the existing `eth.rs` + pattern). `deckard-core` stays `#![forbid(unsafe_code)]` — keep fd plumbing out of it. Prefer a + vetted existing dependency already in the tree (`nix`, `tokio`) over hand-rolled `libc`; **no new + dependency without approval** (DoD #4) — `nix` and `tokio` are already present. + +## Acceptance tests (add to `crates/deckard-signerd/tests/`) + +- `resolve_rejected_on_public_socket`: a same-uid client on the public socket gets the typed denial for + `Resolve`; the pending record stays `Pending`. +- `resolve_accepted_on_control_channel`: the same `Resolve` over the control capability flips + `Pending → Allowed`. +- `stop_still_works_on_public_socket`: `RevokeAll` from the public socket zeroizes + denies in-flight + (the brake is never gated). +- `second_proposer_cannot_self_approve`: end-to-end — proposer A proposes (over cap → `NeedsApproval`); + proposer A (public socket) cannot `Resolve`; only the control channel can. This is the red-team + assertion that maps to residual-risk #1. +- Pure-fn parity unaffected (mock vs daemon `evaluate` unchanged). + +## Definition of Done + +PRD-series DoD (see `README.md`) **plus**: +- `THREAT-MODEL.md` residual-risk table updated: #1 moves from "Accepted v1 boundary" to "Mitigated" + with the mechanism named; the "Deferred" notes in §2 (daemon socket) updated accordingly. +- A short decision note in this PRD recording which capability mechanism was chosen and why. +- The four acceptance tests above run by **default** `cargo test` (not `#[ignore]`). + +## Risks & fallbacks + +- **fd inheritance across `Command::spawn` is fiddly / platform-specific.** Fallback: `SCM_RIGHTS` + post-connect; last resort: `0600` cookie (documented as weaker). +- **App-restart / daemon-survives races** (the supervise loop respawns the daemon): define the + re-handshake so a restarted app re-establishes the control channel without a window where `Resolve` + is ungated. Test the respawn path. + +## Sources + +`docs/research/10-dapp-connectivity.md §22–29`; `THREAT-MODEL.md` (boundary section, residual-risk #1); +man7 unix(7); blog.cloudflare.com/know-your-scm_rights; capability-myths-demolished. diff --git a/docs/prd/02-clear-signing-and-message-intents.md b/docs/prd/02-clear-signing-and-message-intents.md new file mode 100644 index 0000000..c58bd04 --- /dev/null +++ b/docs/prd/02-clear-signing-and-message-intents.md @@ -0,0 +1,134 @@ +# PRD-02 — Clear-signing v2 + message-signing intents + +> Phase 1b of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). 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 this exists + +A dapp's most dangerous request is not a transaction — it is an **off-chain signature that authorizes +value movement with no on-chain tx the wallet ever simulates**. In 2024, EIP-2612 `permit` signatures +(56.7%) and `setOwner` (31.9%) were ~88.6% of wallet-drainer losses (`research §30`). Deckard today can +sign one structured off-chain payload (`SwapOrder` via `SignOrder`, EIP-712) but has **no general +message-signing path and no decode/clear-signing for arbitrary typed data**. Before any dapp can ask +Deckard to sign, the clear-signing card (`DESIGN.md` "the shared trust engine") must render *what is +actually being authorized*, in plain language, danger-early — never a raw hash. + +## Goals + +- Add typed message-signing to the contract: `personal_sign` (EIP-191) and `eth_signTypedData_v4` + (EIP-712), routed through `propose → Decision → human approval`, **never auto-allowed**. +- **Decode and clear-sign** before the human: EIP-712 domain binding (chainId + verifyingContract), + recognition of high-risk shapes (`permit`, Permit2, Seaport orders, `setOwner`/ownership transfer), + and a distinct, alarming screen for EIP-7702 delegation. +- **Consume ERC-7730 descriptors** where available to render human-readable intent; **fall back to an + explicit "blind signing — we can't decode this" warning** for the uncovered long tail. +- Hard-handle the chainId-mismatch and unknown-`verifyingContract` cases (warn loudly; the research + shows most wallets silently don't — `research §33`). + +## Non-goals + +- The transport that *delivers* these requests (PRD-04). This PRD makes the daemon able to *safely + sign a message it is handed*; where the message comes from is out of scope. +- Building the ERC-7730 registry. We *consume* descriptors; curation/registry shipping is PRD-05. +- Supporting `eth_sign` (raw 32-byte blind hash). **Refuse it outright** — MetaMask is removing it + (`research §31`); Deckard never offers it. + +## Design + +### Contract (`crates/deckard-contract`) + +Add message-signing as first-class, mirroring the existing `SwapOrder`/`SignOrder`/`PendingPayloadView` +pattern (do not overload `Intent`, which is a *transaction* shape): + +- New request variants on `SignerRequest` (rpc.rs), e.g. `ProposeMessage { message: SignMessage }` → + `Decision`, and `SignMessage { request_id }` → a `SignMessageResult { signature: Bytes }` (reuse the + `SignOrderResult` shape). Message signing **never broadcasts** — distinct from `Execute`. +- A `SignMessage` enum: + - `PersonalSign { bytes: Bytes }` (EIP-191 personal_sign; display decoded UTF-8 when valid, else hex + + a "binary message" caution). + - `TypedDataV4 { typed_data: ... }` (EIP-712). Parse the domain (`name, version, chainId, + verifyingContract, salt`) and the primary type. +- A new `PendingPayloadView::Message(SignMessageView)` so the GUI inbox (existing + `PendingList`/`PendingRecord`) renders message requests alongside tx/order/approve. +- New `deny_reasons` entries: `ETH_SIGN_REFUSED`, `CHAINID_MISMATCH`, `DELEGATION_REFUSED`, + `UNDECODABLE_TYPED_DATA`. + +Parse all untrusted typed-data bytes through the bounded `Reader` in `keystore.rs` style (DoD #5): +strict length/depth caps so a hostile EIP-712 blob can't OOM/recurse the daemon before validation. + +### Decision logic (`crates/deckard-contract/src/policy.rs`) + +Add a pure `evaluate_message(&SignMessage, &Policy, wallet, now) -> Decision` next to `evaluate` / +`evaluate_order` (keep the "one decision function, mock⇄daemon parity" charter): + +- Messages are **always `NeedsApproval`** in v1 (like swap orders) — no auto-allow path. +- EIP-712: deny on `CHAINID_MISMATCH` (domain.chainId ≠ daemon chain). Surface unknown + `verifyingContract` as a card-level danger flag (not necessarily a deny — the human decides), per + EIP-712 SHOULD/MAY (`research §33`). +- Recognize and **flag danger-early** (red, top of card, per `DESIGN.md`): unlimited/large `permit` & + Permit2 allowances, Permit2 batch, Seaport orders transferring assets for ~zero, `setOwner`/owner + transfer. These mirror `PendingPayloadView::Approve`'s existing shaped-approve treatment. + +### EIP-7702 delegation (`research §34`) + +EIP-7702's own Security Considerations say wallets MUST NOT offer a generic "sign arbitrary +delegation" interface ("there is no safe way"). Decision (record in PRD): **refuse delegation +authorizations by default** (`DELEGATION_REFUSED`); if ever supported, gate behind a curated +trusted-delegator allowlist (a PRD-05 concern) and a *distinct* "you are handing control of your +account to `
`" screen — never the normal sign card. v1: refuse. + +### Clear-signing card (`crates/deckard-app`, the shared review component) + +Extend the existing clear-signing card (`DESIGN.md` "Clear-signing review card") to render +`SignMessageView`: +- Plain-language **headline** of what's being authorized; one canonical key/value list grouped by + whitespace; danger in red at the top; **deliberate hold** to confirm (never a tap). +- ERC-7730: if a descriptor exists for the `verifyingContract`/call, render its human-readable fields. + If not, show an explicit **"Blind signing — Deckard can't decode this request. Only continue if you + fully trust the source."** caution (amber icon + risk word, per `DESIGN.md`), demoted-but-present. +- Origin shown as **unverified** (PRD-05 may upgrade it). Per `research §29`, never let a claimed name + substitute for the decoded effects. + +### `wallet_addEthereumChain` guard (`research §36`) + +Even pre-transport, codify the rule: a chain add/switch must sign only with the **user-submitted** +chainId, never one returned by a (possibly malicious) RPC; require an explicit confirm naming requester ++ target chain. Land the daemon-side invariant here; the UI lands with PRD-04. + +## Acceptance tests + +- `eth_sign_refused`: a raw-hash sign request is denied with `ETH_SIGN_REFUSED`, nothing signed. +- `typed_data_chainid_mismatch_denies`: EIP-712 with domain.chainId ≠ daemon chain → `CHAINID_MISMATCH`. +- `permit_flagged_danger`: a `permit`/Permit2 typed-data produces a `Decision::NeedsApproval` whose + pending view carries the danger flag (assert the flag, not just the approval). +- `delegation_refused`: an EIP-7702 authorization → `DELEGATION_REFUSED`. +- `message_never_broadcasts`: signing a message returns a signature, never a `tx_hash`; `Execute` on a + message request is rejected. +- `undecodable_typed_data_bounded`: an oversized/deeply-nested EIP-712 blob is rejected by the bounded + reader without OOM/panic (fuzz-style vector). +- Parity: `evaluate_message` gives identical verdicts in `MockSigner` and the daemon. +- Transcript hygiene: no message bytes/signature leak into any reason string (extend the existing + allowlist scan). + +## Definition of Done + +PRD-series DoD **plus**: new ⌘K commands for any user-initiated signing actions; `DESIGN.md` card spec +honored (verify visually per `just check` build + a screenshot in the PR); the EIP-7702 refusal and the +blind-sign fallback are documented in `THREAT-MODEL.md` (new "message signing" surface section). + +## Risks & fallbacks + +- **EIP-712 parsing is a parser-security surface.** Mitigate with the bounded reader + fuzz vectors; + keep the parser in `deckard-core` under its strict lints. +- **ERC-7730 coverage gap** (`research §35`): most contracts have no descriptor. The blind-sign + fallback is therefore the *common* path, not the exception — design it to be honestly alarming, not + normalized-away. +- **Scope creep into a full ABI decoder.** v1: decode the *high-risk shapes* (permit/Permit2/Seaport/ + owner-transfer) + ERC-7730 descriptors; everything else is explicit blind-sign. Don't build a + general decoder. + +## Sources + +`docs/research/10-dapp-connectivity.md §30–36`; eips.ethereum.org eip-712, eip-7702, erc-7730, +eip-3085/3326; MetaMask MIP-3; scamsniffer 2024 drainer report; existing `swap_order.rs`/`SignOrder`. diff --git a/docs/prd/03-curated-native-integrations.md b/docs/prd/03-curated-native-integrations.md new file mode 100644 index 0000000..7da59e2 --- /dev/null +++ b/docs/prd/03-curated-native-integrations.md @@ -0,0 +1,98 @@ +# PRD-03 — Curated native dapp integrations (Phase 0 connectivity) + +> Phase 0 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The "curated/allowlisted dapps +> first" requirement, realized as **native Deckard surfaces** — no browser, no extension, no relay, no +> external proposer. This is the first user-visible value and the most secure interpretation of +> "connect to dapps." + +## Why this exists + +The maintainer chose **curated/allowlisted dapps first** (ADR driver #2). Taken to its logical, most +secure conclusion, the curated set doesn't need a generic *dapp-connection transport at all*: each +curated integration is a **native screen that builds a typed `Intent`/`SwapOrder` in Rust** and routes +it through the existing daemon → policy → clear-signing path — exactly how **Shield** (Railgun) and the +**CoW swap** path already work (`swap_order.rs`, `SignOrder`). This adds zero new attack surface, stays +fully on-brand ("calm sovereign control, not a casino"), works offline-first, and ships *before* the +Phase-2 connectivity machinery (PRD-04/05). + +## Goals + +- A small, vetted set of integrations as **native surfaces**, each producing a typed proposal the + daemon already (or via PRD-02) understands. Initial set: **token swap** (extend existing) and **one + bridge**. Each is a wallet action, reachable from ⌘K, with clear-signing on confirm. +- A **curated integration registry** in-repo (compile-time or signed config) listing the allowed + protocols, their contract addresses per chain, and the `IntentKind`/order they build — the + "allowlist" is the set of code paths we shipped, not a URL allowlist. +- Reuse `Policy.allow_swap_tokens` / `allow_to` fences; no new bypass. + +## Non-goals + +- Generic / open dapp access (PRD-04). No `window.ethereum`, no WalletConnect here. +- Per-origin permissions (PRD-05) — there is no untrusted origin; the integration *is* Deckard code. +- New signing primitives beyond what PRD-02 adds (a swap needs Permit2/EIP-712 → that's PRD-02's job; + this PRD consumes it). + +## Design + +### Integration shape + +Each curated integration is a module under the app (or a small `deckard-integrations` area if it grows) +that: +1. Fetches the quote/route from the protocol's API over the existing verified-read/RPC path (reads + only; never a signing path). +2. Builds a typed `Intent` (`Send`/`ContractCall`/`Shield`) or `SwapOrder` against **registry-pinned + contract addresses** for the active `chain_id`. +3. Calls `Propose`/`ProposeOrder` → renders the clear-signing card → on hold-confirm, + `Execute`/`SignOrder`. + +The daemon's `calldata_ok` shape check and `evaluate`/`evaluate_order` fences apply unchanged — a +native integration is just another well-behaved proposer using the *typed* builders (never raw +arbitrary calldata, per the `deckard-mcp` rule). + +### Curated registry + +- A versioned, in-repo list: `{ protocol, chains: { chain_id: { contracts… } }, kind }`. Compile-time + `const`/`include_str!` of a JSON checked into the repo (offline-first; no network fetch to learn what + is allowed). If it must update without a release, ship it as a **signed** config (verify a maintainer + signature before trusting — never an unsigned downloaded allowlist). +- Addresses are pinned and reviewed; a swap/bridge to an off-registry contract is impossible from this + surface (it would require a code change + review). + +### UI (`DESIGN.md`) + +- Swap already exists; extend per the **Amount input** + **Clear-signing review card** specs. +- Bridge: a new contextual action on the wallet (Send/Receive/Swap siblings), same review-card confirm, + same status-glyph activity rows. Bridges cross chains — the card must show source chain, destination + chain, destination address, and asset, danger-early on any mismatch. + +## Acceptance tests + +- `swap_builds_pinned_addresses`: the swap path only ever targets registry-pinned contracts for the + active chain; an attempt to target another address is rejected before propose. +- `bridge_proposal_clear_signs`: a bridge proposal renders source/dest chain + dest address on the card + and routes through the normal approval path. +- `off_registry_denied`: a constructed proposal to a non-registry contract via this surface is refused. +- Existing swap parity tests (`crates/deckard-signerd/tests/swap_parity.rs`) stay green. +- Registry signature check (if signed-config path chosen): a tampered/unsigned registry is rejected + loudly (mirror the `policy_store.rs` "loud fallback" discipline). + +## Definition of Done + +PRD-series DoD **plus**: each integration is a ⌘K `Command`; the curated registry and its trust model +(compile-time vs signed) are documented; the bridge card matches `DESIGN.md`; a screenshot in the PR. + +## Risks & fallbacks + +- **Protocol API as an input channel.** Quotes/routes from a protocol API are untrusted input — the + daemon still re-derives/validates the typed proposal against pinned addresses and the policy fence; + never sign what the API returns verbatim. +- **Bridge complexity** (two chains, longer settlement). v1: pick one well-understood bridge; show + honest pending/failed states (`DESIGN.md` required states). Don't ship a bridge whose effects we + can't clear-sign. +- **Scope creep toward "just open any dapp".** That is explicitly PRD-04 and gated on an audit; keep + this surface curated-code-only. + +## Sources + +`docs/adr/0001-dapp-connectivity-architecture.md` (Phase 0); existing `swap_order.rs`, `SignOrder`, +`swap_parity.rs`, Railgun Shield path (`docs/build/10-kohaku-shield.md`); `DESIGN.md`. diff --git a/docs/prd/04-walletconnect-transport.md b/docs/prd/04-walletconnect-transport.md new file mode 100644 index 0000000..a83bc6e --- /dev/null +++ b/docs/prd/04-walletconnect-transport.md @@ -0,0 +1,131 @@ +# PRD-04 — WalletConnect v2 transport (`deckard-wcd`) + +> Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The **primary generic +> dapp-connectivity transport**: reaches desktop today and mobile later, no browser extension to ship +> or get hijacked. **Gated on PRD-01 (resolver auth), PRD-02 (clear-signing v2), PRD-05 (per-origin +> permissions), and an external security audit** (`SECURITY.md`). Do not start before its blockers. + +## Why this exists + +When Deckard wants to connect to dapps *it didn't integrate natively* (PRD-03), it needs a generic +transport. Research (`research §8–15`) found WalletConnect v2 dominates the alternatives for Deckard: + +- **Reaches mobile** (driver #1) — the only one of {extension, embedded webview, WalletConnect} that + does. An eventual mobile app reuses the same transport; no rewrite. +- **No extension artifact** to maintain in two hostile stores or get supply-chain-hijacked + (`research §6`). +- **E2E encrypted** (ChaCha20-Poly1305 / X25519); the relay cannot read sign/tx payloads + (`research §9`). +- **Protocol-level scope** via CAIP-25 namespaces (chains × methods × accounts) — maps cleanly onto + per-origin policy (PRD-05). + +The costs are real and this PRD must confront them: a **centralized Reown relay + Project-ID +dependency** (privacy/offline-first tension, `research §10–11`) and **no maintained Rust wallet-side +SDK** — only the low-level relay client; the Sign protocol must be built in-house (`research §14`). + +## Goals + +- A **key-less proposer process, `deckard-wcd`**, mirroring `deckard-mcp`: it speaks WalletConnect to + dapps and the existing `deckard-contract` wire to `deckard-signerd`. It holds **no key**, **cannot + `resolve`** (PRD-01 guarantees that even if it tried), and submits only **typed** intents/messages + through the PRD-02 builders — never raw arbitrary calldata that skips a typed path. +- Implement WC v2 pairing (`wc:` URI), sessions, and **CAIP-25 scope negotiation** restricting a + connected dapp to approved chains × methods × accounts (PRD-05 owns the policy; this owns the wire). +- Map WC method calls → Deckard intents: `eth_sendTransaction` → `Intent`; `personal_sign` / + `eth_signTypedData_v4` → `SignMessage` (PRD-02); `wallet_switchEthereumChain`/`addEthereumChain` → + guarded per PRD-02. Refuse `eth_sign` and (v1) delegation. +- **Verify-API origin attestation** surfaced on the card as MATCH/UNVERIFIED/MISMATCH/THREAT — never as + the sole defense (`research §13, 29`); the card still clear-signs real effects. +- **Relay-privacy posture**: document and implement what a privacy-focused wallet must do (below). + +## Non-goals + +- A browser extension (separate optional future PRD; explicitly secondary per ADR). +- Generic ABI decoding (PRD-02 scope: high-risk shapes + ERC-7730 + blind-sign fallback). +- Mobile app itself (driver #1: don't compromise desktop for it) — but **do not pick a design that + forecloses mobile** (keep `deckard-wcd`'s core protocol logic platform-agnostic). + +## Design + +### Process topology (reuse the `deckard-mcp` shape — `docs/build/30-mcp-shape.md`) + +``` +dapp ──WalletConnect (relay, E2E)──► deckard-wcd ──deckard-contract wire──► deckard-signerd + (key-less, (holds key, policy gate) + no resolve) │ + native clear-signing card ◄─ GPUI app + (control channel, PRD-01) +``` + +`deckard-wcd` is to dapps what `deckard-mcp` is to LLM agents: a thin, key-less translator. Reuse the +same daemon socket, the same `Propose`/`Execute`/`Status` poll loop, and the same native approval card. + +### Build vs borrow the WC Sign layer (decide first; record the decision) + +No maintained Rust wallet SDK exists (`research §14`). Options, in preference order: +1. **Build the Sign protocol in Rust** on `WalletConnectRust` (relay client + RPC types). Most work; + keeps everything native and dependency-light; full control of the crypto + scoping. **No new + heavyweight deps without approval** (DoD #4) — the relay client + an X25519/ChaCha20 stack (some + already in-tree via the keystore) need an explicit dependency review. +2. A separate sidecar in another language only if (1) is infeasible — but that reintroduces a non-Rust + surface; avoid unless forced. + +This PRD's first deliverable is a **spike** (mirror `spikes/`): pin `WalletConnectRust`'s exact surface, +prove a pairing + one `personal_sign` round-trip against a test dapp, and write the build/borrow +recommendation before committing the full implementation. + +### Relay privacy (the offline-first tension, `research §10–11`) + +- Route relay egress through the **same network path Deckard already uses** (so it inherits any + proxy/VPN/Tor the user configures); document that the relay sees IP + timing + topic metadata. +- Rotate topics per session; don't reuse pairing topics. +- Expose a **custom relay URL** setting (for future self-host / testing), defaulting to the public + relay, clearly labeled. Note the Project-ID analytics dependency honestly in-app and in docs. +- WalletConnect is **opt-in and off by default** — it is a network feature in an offline-first wallet; + the user turns it on, sees the relay-dependency notice, and it never silently phones home. + +### Scope & methods + +- Negotiate CAIP-25 with **required namespaces empty/minimal** (per Reown guidance, `research §12`) and + drive real scope from optional namespaces + Deckard's per-origin policy (PRD-05). +- Method allowlist per session; an out-of-scope method request is refused with a typed error and shown + to the user. + +## Acceptance tests + +- Spike report committed: pairing + `personal_sign` round-trip proven; build/borrow recommendation. +- `wcd_is_keyless`: memory/fd scan of `deckard-wcd` finds no key (reuse the `deckard-mcp` red-team + harness, `docs/build/00-test-harness.md`). +- `wcd_cannot_resolve`: `deckard-wcd` attempting `Resolve` is refused by PRD-01's control-channel gate + (cross-PRD integration test). +- `method_out_of_scope_refused`: a dapp requesting a method outside the negotiated CAIP-25 scope is + refused; nothing reaches the daemon. +- `sign_request_clear_signs`: a `personal_sign`/EIP-712 from a dapp renders the PRD-02 card with origin + attestation state; off-chain danger flags fire. +- `relay_opt_in_default_off`: with WC disabled, no relay connection is opened (assert no socket/egress). +- Transcript hygiene extended to `deckard-wcd` (no secret/RPC-token leakage). + +## Definition of Done + +PRD-series DoD **plus**: ⌘K commands for connect/disconnect/list-sessions/STOP-all-sessions; the relay +privacy posture documented in `SECURITY.md` + `THREAT-MODEL.md` (new "WalletConnect transport" +surface); off-by-default verified; **an external audit sign-off recorded** before this ships beyond +testnet (per `SECURITY.md`). New deps (if any) explicitly approved and added to `deny.toml`/`Cargo.lock` +review. + +## Risks & fallbacks + +- **In-house Sign protocol is substantial.** Fallback: ship PRD-03 (native integrations) as the user + value while this bakes; WC is not on the critical path to first value. +- **Relay centralization/availability** (`research §11`): the custom-relay-URL setting + opt-in posture + contain it; track WC's decentralized-relay progress before relying on self-host. +- **Verify API is not bulletproof** (`research §13`): treat as one signal; the clear-signing card + + per-origin policy (PRD-05) + blocklist are the real defenses. +- **Spoofable dapp metadata** (`research §13`): never let the claimed name substitute for decoded + effects (`research §29`). + +## Sources + +`docs/research/10-dapp-connectivity.md §8–15, 29`; specs.walletconnect.com (pairing-uri, crypto-keys, +sign/namespaces, core/verify); docs.reown.com/cloud/relay; github.com/WalletConnect/WalletConnectRust; +`docs/build/30-mcp-shape.md` (the proposer pattern to mirror). diff --git a/docs/prd/05-per-origin-permissions-and-registry.md b/docs/prd/05-per-origin-permissions-and-registry.md new file mode 100644 index 0000000..e0c4204 --- /dev/null +++ b/docs/prd/05-per-origin-permissions-and-registry.md @@ -0,0 +1,110 @@ +# PRD-05 — Per-origin permissions, dapp registry & anti-phishing + +> Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The policy + trust layer that +> makes generic connectivity (PRD-04) safe: scope each connected origin (accounts × chains × methods), +> ship a curated registry + anti-phishing blocklist, and treat origin as attacker-controllable. +> Pairs with PRD-04; depends on PRD-02. + +## Why this exists + +Once a dapp can connect (PRD-04), the wallet must answer "what may *this* origin see and do?" — not +"what may any caller do?". Research (`research §37`) shows the standards: EIP-2255 +(`wallet_requestPermissions`/`getPermissions`, the `{invoker, parentCapability, caveats}` object) and +CAIP-25 session scoping (per-scope accounts × chains × methods). And `research §13, 29` shows origin is +**self-asserted and spoofable** — so scoping must be paired with attestation-as-a-hint + a shipped +blocklist, and the human must always approve on decoded effects, not a claimed name. + +## Goals + +- A **per-origin permission model**: a connected origin is granted a scoped session (which accounts, + which chains, which methods, optional caps) that the daemon enforces — distinct from the global + `Policy`. Persisted, revocable, and visible in the UI. +- A **curated dapp registry** (the "allowlist first" posture for generic connectivity): known-good + origins with metadata (name, verified domain, ERC-7730 descriptor references). Offline-first and + **signed** if updatable without a release. +- An **anti-phishing blocklist** (known-malicious domains), shipped and updatable, mirroring + `eth-phishing-detect` (`research §37`). +- Wire-level guards: `wallet_addEthereumChain`/`switchEthereumChain` per `research §36` (sign only with + user-submitted chainId; confirm requester + target chain). + +## Non-goals + +- The transport (PRD-04) and the signing decode (PRD-02). +- Building/curating ERC-7730 descriptors upstream — we *reference and consume* them (PRD-02 renders). +- Open access to arbitrary origins as a default — default posture is curated-allowlist; unknown origins + are allowed only with an explicit, friction-ful "unverified origin" confirmation (never silent). + +## Design + +### Per-origin policy (`crates/deckard-contract` + `deckard-signerd`) + +- Extend the policy layer with an **origin-scoped permission** type: `OriginGrant { origin, + accounts, chains, methods, caps?, expiry }`. Keep the global `Policy` as the outer fence — an origin + grant can only ever be *narrower* than the global policy (defense-in-depth; a permissive grant can't + widen the global caps/allowlist). +- The daemon evaluates an incoming dapp proposal against **both** the origin grant and the global + policy; the stricter wins. Add a pure `evaluate_origin(&proposal, &OriginGrant, &Policy)` next to the + existing decision functions (preserve the mock⇄daemon parity charter). +- Grants are created at connect-time (the human approves the requested scope, EIP-2255/CAIP-25 style) + and are **revocable** — surface revoke in the agent/governance settings alongside the existing + "Pause all agents" kill switch (`DESIGN.md`). + +### Origin attestation (treat as a hint, `research §29`) + +- The daemon receives an origin string from the (untrusted) proposer. It is displayed as **unverified** + unless corroborated by (a) WalletConnect Verify-API state (PRD-04) and/or (b) a registry match. +- The card shows attestation state (verified-domain / unverified / mismatch / known-scam) using + `DESIGN.md` caution affordances — but the **decoded effects** remain the ground truth the human holds + to confirm. Never gate solely on a green checkmark. + +### Curated registry & blocklist (offline-first, signed) + +- **Registry**: in-repo signed JSON of curated origins → metadata + ERC-7730 descriptor refs. Ships + with the build; if updatable out-of-band, verify a maintainer signature before trust (mirror the + `policy_store.rs` loud-fallback discipline — a tampered/unsigned list is refused loudly, never + silently widening trust). +- **Blocklist**: known-malicious domains; a match is a hard, loud block (red), not a soft warning. + Updatable on the same signed-config mechanism. Default-deny on a blocklist match even if the user + tries to proceed (this is the one place we override user intent — a known drainer domain). + +### ERC-7730 descriptor sourcing + +- The registry references which ERC-7730 descriptors apply to a curated origin's contracts so PRD-02's + card can render human-readable intent; uncovered contracts fall back to PRD-02's explicit blind-sign + warning. + +## Acceptance tests + +- `origin_grant_cannot_widen_global`: an origin grant requesting more than the global `Policy` allows is + clamped to the global fence (stricter wins); assert the effective decision. +- `out_of_scope_method_denied`: a method/chain/account outside the origin grant is denied. +- `blocklist_hard_blocks`: a blocklisted origin is refused even on explicit user "proceed". +- `unsigned_registry_refused`: a tampered/unsigned registry/blocklist is rejected loudly; the wallet + falls back to the safe built-in (curated-empty = nothing auto-trusted). +- `unverified_origin_requires_friction`: an unknown (not-curated, not-blocked) origin connect requires + the explicit unverified-origin confirmation path, not a silent allow. +- `addchain_uses_user_chainid`: a `wallet_addEthereumChain` flow signs only with the user-submitted + chainId; an RPC-returned chainId is never trusted. +- Parity: `evaluate_origin` identical in mock + daemon. + +## Definition of Done + +PRD-series DoD **plus**: revoke-origin and list-connected-origins are ⌘K commands and appear in +settings governance; registry/blocklist trust model documented in `SECURITY.md`; the +"origin-is-untrusted, effects-are-ground-truth" rule documented in `THREAT-MODEL.md`; UI matches +`DESIGN.md` (caution affordances, danger-early, no green-check-as-safety). + +## Risks & fallbacks + +- **Curated registry maintenance burden.** Start tiny (the same protocols PRD-03 integrates natively); + grow by reviewed PR. The registry being small is fine — it's the safe default. +- **Blocklist staleness** (`research §37` counts are directional). It's a backstop, not the primary + defense; the clear-signing card is. Update cadence documented; signed. +- **Origin spoofing** (`research §13`): contained by never substituting a name for decoded effects, by + attestation-as-hint, and by the blocklist. + +## Sources + +`docs/research/10-dapp-connectivity.md §13, 29, 36–37`; eips.ethereum.org eip-2255, eip-3085/3326; +chainagnostic.org CAIP-25; github.com/MetaMask/eth-phishing-detect; existing `policy.rs`, +`policy_store.rs` (loud-fallback discipline). diff --git a/docs/prd/README.md b/docs/prd/README.md new file mode 100644 index 0000000..900848e --- /dev/null +++ b/docs/prd/README.md @@ -0,0 +1,40 @@ +# Dapp connectivity — PRD series + +These PRDs execute [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). Each is written to be +picked up by an independent agent: motivation, scope, implementation guidance mapped to real crates/ +files, and a Definition of Done tied to the project's gates (`docs/AGENTIC-ENGINEERING.md`). + +**Definition of Done — applies to every PRD below** (paste command output as evidence; never claim +done while red): + +1. `cargo fmt --all --check` clean. +2. `just check` green — clippy `-D warnings` on BOTH default and `--features tray`. +3. `cargo test --workspace` green. +4. No new/changed dependencies (`Cargo.toml`/`Cargo.lock`) unless explicitly approved in the PRD. +5. `deckard-core` crate-level lints respected (no `unwrap`/`expect`/`panic!`/raw indexing in non-test + code; `#![forbid(unsafe_code)]`). New untrusted-byte parsing goes through a bounded `Reader`. +6. No secret ever logged or `Debug`-printed (seeds/keys/passphrases/viewing keys stay in `Zeroizing`). +7. Every new user-facing action has a ⌘K `Command` (`palette_commands.rs` + `Shell::run_palette_command`). +8. Any visual/UI surface matches `DESIGN.md` (clear-signing card anatomy, amber=human/cyan=agent, + danger-early, hold-to-confirm). + +## Dependency graph + +``` +PRD-01 Resolver auth ─────────────┐ + ├─► PRD-04 WalletConnect transport ─► (Phase 2, post-audit) +PRD-02 Clear-signing v2 ──────────┤ + │ └─► PRD-05 Per-origin permissions + registry + └─► PRD-03 Curated native integrations (Phase 0, ships first) +``` + +| PRD | Title | Phase | Blocks on | Ships | +|-----|-------|-------|-----------|-------| +| [01](./01-resolver-authentication.md) | Resolver authentication (capability-gated `Resolve`) | 1a | — | independently; closes residual-risk #1 | +| [02](./02-clear-signing-and-message-intents.md) | Clear-signing v2 + message-signing intents | 1b | — | independently; needed by PRD-03 & PRD-04 | +| [03](./03-curated-native-integrations.md) | Curated native dapp integrations | 0 | PRD-02 (for permit/EIP-712) | first user-visible value | +| [04](./04-walletconnect-transport.md) | WalletConnect v2 transport (`deckard-wcd`) | 2 | PRD-01, PRD-02, PRD-05 | post-audit | +| [05](./05-per-origin-permissions-and-registry.md) | Per-origin permissions, registry & anti-phishing | 2 | PRD-02 | with PRD-04 | + +**Recommended execution order:** PRD-01 and PRD-02 in parallel (foundational, independent) → PRD-03 +(first shippable value) → PRD-05 → PRD-04 (gated on an external audit per `SECURITY.md`). diff --git a/docs/research/10-dapp-connectivity.md b/docs/research/10-dapp-connectivity.md new file mode 100644 index 0000000..f2ad537 --- /dev/null +++ b/docs/research/10-dapp-connectivity.md @@ -0,0 +1,240 @@ +# Dapp connectivity — research synthesis (2026-06-14) + +> How should Deckard — a self-custodial desktop wallet (macOS + Linux now, mobile possibly +> later) whose key lives in a process-isolated signer daemon behind a policy gate and a native +> clear-signing card — let third-party dapps interact with it? This is the cited evidence base +> behind [`docs/adr/0001-dapp-connectivity-architecture.md`](../adr/0001-dapp-connectivity-architecture.md) +> and the `docs/prd/*` series. Five parallel research angles, claims ranked by confidence, +> primary sources linked. Where a claim is our own reasoning over the sources it is marked +> **(inference)**. + +## The question, reframed onto Deckard's boundary + +Deckard already chose its trust model (`THREAT-MODEL.md`): **the key lives in `deckard-signerd`; every +other process is an untrusted *proposer* that can only submit an `Intent`; the daemon's policy gate +decides; the human approves via a native clear-signing card.** `deckard-mcp` is the reference proposer +— key-less, no `propose`-arbitrary, no `resolve`. Every dapp-connectivity option below is therefore +judged on one axis: **does it keep third-party code on the untrusted-proposer side of that boundary, +and what new attack surface does it add to reach the daemon?** + +The user's clarified options were: +- **A — embedded in-app browser**: render dapps *inside* Deckard via a bundled webview. +- **B — separate browser extension**: dapps run in the user's own browser; an extension forwards + requests to Deckard. + +Research adds a third transport the question implied but didn't name — **WalletConnect v2** (relay +pairing, no extension, desktop + mobile) — and a crux that sits *under* all of them: **how an +untrusted local proposer reaches the daemon socket without being able to self-approve.** + +--- + +## Angle 1 — Browser extension + bridge transport (the Frame.sh model) + +1. **Frame.sh does *not* use Chrome native messaging — it exposes a localhost RPC server + (`ws://127.0.0.1:1248` / `http://127.0.0.1:1248`) that any browser, CLI, or app connects to; the + extension is a thin relay tagging requests with origin metadata.** (high) — + github.com/floating/frame, github.com/floating/frame-extension (read from source). +2. **A localhost RPC port is reachable by *any* local process and by web pages: same-origin policy + does not stop a page from calling localhost, WebSockets aren't CORS-gated at handshake, and + DNS-rebinding defeats naive Host checks — so the transport must rely on per-request UI confirmation + + origin tagging, not transport-level isolation.** (high) — + github.blog/security/.../localhost-dangers-cors-and-dns-rebinding, portswigger.net/web-security/websockets/cross-site-websocket-hijacking. +3. **Chrome native messaging (the safer bridge) runs an OS-installed stdio host gated by a manifest + `allowed_origins` (exact `chrome-extension://`, no wildcards); the host runs as the invoking + user with no browser sandbox.** (high) — developer.chrome.com/docs/extensions/develop/concepts/native-messaging. +4. **Native-host registration is itself an attack surface: manifests live in user-writable locations + (HKCU / per-user dirs), so any local code running as the user can register or overwrite a host.** + (high) — developer.chrome.com native-messaging docs; textslashplain.com native-messaging writeup. +5. **MV3 background service workers are ephemeral; a `connectNative()` port keeps the worker alive, + but this has had cross-version edge-case bugs — long-lived bridges need reconnection logic.** + (high / medium) — developer.chrome.com service-worker lifecycle; chromium issue 40661802. +6. **The extension is a standing supply-chain target. Documented precedents: "The Great Suspender" + (>2M users) sold then weaponized via auto-update (2020–21); the Cyberhaven OAuth-publish hijack + (Dec 2024, ≥35 extensions, ~2.6M users); a fake "Safery: Ethereum Wallet" exfiltrating seed + phrases; a Trust Wallet extension supply-chain bug (~$7M reported).** (high for the first two; + medium for the crypto cases) — bleepingcomputer, thehackernews, cyberhaven engineering blog, + sekoia.io, cybersecuritynews. +7. **Provider injection should use EIP-6963 (event-based multi-wallet discovery), not fight over + `window.ethereum` (EIP-1193's single global, "last to load wins").** (high) — eips.ethereum.org/EIPS/eip-6963. + +**Takeaway:** an extension is **desktop-only** (it does not transfer to mobile), its bridge is either +the writable-registration native-messaging host or the web-reachable localhost port, and the extension +artifact *itself* is a recurring drainer vector. For a security-first wallet, shipping an extension +means owning a high-value supply-chain target in two hostile stores. + +--- + +## Angle 2 — WalletConnect v2 / Reown (relay transport) + +8. **WC v2 separates pairing (a symmetric key shared in the `wc:` URI, EIP-1328) from sessions + (app↔wallet); the relay is a pub/sub network routing by topic.** (high) — specs.walletconnect.com pairing-uri, relay-network. +9. **Session traffic is end-to-end encrypted (ChaCha20-Poly1305 AEAD, X25519 ECDH + HKDF); by design + the relay cannot read transaction or signing payloads.** (high) — specs.walletconnect.com crypto-keys; docs.walletconnect.network/network. +10. **But the relay *does* see metadata: IP addresses, timing/session duration, topic IDs, and + subscription patterns — it must, to route.** (medium, inference corroborated) — relay-network spec; independent analyses. +11. **Production self-hosting of a relay is officially unsupported; the default endpoint is + `relay.walletconnect.org` and a Reown Project ID (with usage analytics) is required.** (high) — + walletconnect-docs FAQ, docs.reown.com/cloud/relay. *This is in direct tension with Deckard's + "offline-first, privacy-focused" identity.* +12. **Scope is negotiated via CAIP-25 namespaces (per scope: chains CAIP-2, methods, events, + accounts CAIP-10) — the protocol-level lever to restrict what a connected dapp may request.** + (high) — specs.walletconnect.com/sign/namespaces. +13. **Dapp metadata at pairing (name/url/icons) is self-asserted and spoofable; the Verify API is the + anti-phishing mitigation (domain attestation: MATCH / UNVERIFIED / MISMATCH / THREAT) but is + explicitly "not bulletproof" and routes more signal to Reown.** (high / medium) — + specs.walletconnect.com/core/verify, session-proposal. +14. **There is *no maintained Rust wallet-side SDK*: `WalletConnectRust` is only the low-level relay + client + RPC types; the Sign protocol, pairing, and namespace negotiation must be built in-house + for a native Rust wallet.** (high) — github.com/WalletConnect/WalletConnectRust, docs.walletconnect.network. +15. **WC works for a desktop wallet with no phone in the loop (URI via QR or same-device deep link) + and is the only one of the three transports that *also* works on mobile.** (high) — walletkit mobile-linking docs. + +**Takeaway:** WC is the most architecturally aligned generic transport (untrusted proposer, E2E +encrypted, protocol-level scoping, desktop **and** mobile, no extension to get hijacked) — but it +imports a centralized relay + Project-ID dependency that conflicts with Deckard's privacy ethos, and a +**non-trivial build cost** because the wallet-side protocol must be implemented in Rust. + +--- + +## Angle 3 — Embedded in-app browser / webview (Option A) + +16. **The Rust embedding path (`wry`, under Tauri) uses three different engines: WebView2 (Chromium) + on Windows, WKWebView (WebKit) on macOS, WebKitGTK on Linux — divergent JS behavior and divergent + CVE streams to track per OS.** (high) — docs.rs/wry. +17. **WebKitGTK on Linux is not vendor-auto-updated; it patches via distro packages with historic + multi-month lag (the canonical case: Debian shipping WebKitGTK with ~184 known vulns), and still + ships critical ACE-class CVEs in 2025 (Ubuntu USN-7817-1, Oct 2025).** (high) — + blogs.gnome.org/mcatanzaro update-on-webkit-security-updates; lists.ubuntu.com USN-7817-1. +18. **On Linux, wry/WebKitGTK does *not* enable the renderer sandbox by default — the default embedded + posture is an un-sandboxed renderer in the wallet's process tree.** (high) — github.com/tauri-apps/wry issue 935. +19. **Renderer RCE from merely rendering web content is a recurring, exploited-in-the-wild class — V8 + zero-days through 2024–25 (CVE-2024-5274, -7971, 2025-10585) and WebKit (BLASTPASS); embedding the + engine pulls that entire TCB next to the keys.** (high) — qualys/threatprotect, thehackernews, esecurityplanet/Citizen Lab. +20. **Tauri's own security docs say "avoid loading remote content" and treat the webview as untrusted; + a real IPC-bypass CVE (GHSA-57fm-592m-34r7) let embedded remote content reach host commands. An + in-app dapp browser is, by definition, the pattern the framework warns against.** (high) — + v2.tauri.app/security, tauri GHSA-57fm-592m-34r7. +21. **Mobile wallets (MetaMask Mobile, Trust, Rabby, Coinbase Wallet, imToken) embed a dapp browser + only because mobile OSes have no extension model — that rationale does *not* transfer to desktop, + where the user already has a hardened, auto-updating browser.** (high, inference on transfer) — + metamask mobile docs + corroborating writeups. + +**Takeaway:** embedding a browser engine imports a large, separately-versioned, RCE-prone TCB (three +engines, default-off Linux sandbox, distro-lagged patches) into a key-holding app, to replicate a +pattern that exists on mobile only because mobile lacks the options desktop already has. The one real +upside (WKWebView's sandboxed WebContent on macOS) doesn't generalize to Linux. **Reject.** + +--- + +## Angle 4 — The crux: untrusted local proposer → daemon socket without self-approval + +22. **This is the classic confused-deputy problem (Hardy 1988): a privileged program tricked by a + less-privileged caller into misusing its own authority. Root cause is *ambient authority* — Unix + uid-based access is exactly such a model.** (high) — dl.acm.org/10.1145/54289.871709; wikipedia confused-deputy / ambient-authority. +23. **The canonical fix is a *capability*: bundle designation + permission into one unforgeable + reference, so the daemon can tell *where* an "approve" came from.** (high) — capability-myths-demolished (Miller/Yee/Shapiro). +24. **Peer-credential checks (`SO_PEERCRED` / `LOCAL_PEERCRED`) prove only *which uid/pid* connected — + they cannot distinguish a trusted UI from a malicious proposer when both share the uid.** (high) — + man7 unix(7). *This is exactly Deckard's `auth.rs` today: `same_uid()` is the only gate.* +25. **`SCM_RIGHTS` fd-passing yields an unforgeable capability: a received fd "behaves as though + created with dup(2)"; a process cannot fabricate a reference to another's open file description.** + (high) — man7 unix(7); blog.cloudflare.com/know-your-scm_rights. +26. **A `socketpair()` whose one end is inherited only by a specifically spawned child is private by + the kernel security model — the cleanest way to give *only* the trusted UI an approval channel.** + (high) — kernel mechanics; corroborated by US Patent 10,846,152. +27. **macOS/BSD differ: no `SO_PEERCRED` (use `LOCAL_PEERCRED`/`xucred`, no pid; pid via + `LOCAL_PEERPID`); even macOS's richer audit-token has been spoofable when misused — so the + *capability* (private fd) is safer than *identity inference* and more portable.** (high / medium) — + golang issue 27613; hacktricks macos-xpc audit-token attack. +28. **Every hardware/companion signer enforces the same split — request over a promiscuous endpoint, + approval on a surface the requester cannot drive: Trezor (`trezord` localhost gated by a *signed + whitelist*, approval on device), Ledger (Secure Screen + ERC-7730 clear signing), GridPlus + (separate SCE draws the screen), Frame (Electron UI is the confirmation surface).** (high) — + trezord README, ledger secure-screen, gridplus docs, docs.frame.sh. +29. **Origin is attacker-controllable when relayed through an untrusted proposer; the daemon can at + best attest the connecting *process*, not the upstream dapp origin — so the UI must always + clear-sign the *actual effects* and label origin as unverified.** (high, inference) — walletconnect Verify docs; WC monorepo issue 5400. + +**Takeaway:** the fix Deckard's own threat model defers ("authenticate the resolver via a socketpair +or cookie handed only to the supervised app process") is *the* prerequisite for any external proposer. +Approval must move to an unforgeable capability the daemon hands the GPUI app it already spawns +(`supervise.rs`); the public proposer socket must stop accepting `Resolve`. This is established +security engineering, not novel. + +--- + +## Angle 5 — Message-signing danger surface, clear-signing standards, per-origin scoping + +30. **The real danger a dapp connection opens is *off-chain signatures*, not transactions: in 2024 + drainer thefts, EIP-2612 `permit` signatures = 56.7% and `setOwner` = 31.9% (~88.6% combined) of + losses; ~$494M stolen.** (high) — scamsniffer 2024 report; bleepingcomputer. +31. **`eth_sign` signs an arbitrary 32-byte hash (true blind-sign); MetaMask disabled it by default + and is removing it (MIP-3).** (high) — support.metamask.io eth_sign risk; MIP-3. +32. **Permit2 concentrates risk: one phishing signature to the canonical Permit2 contract can drain + allowances across every token the user ever approved to it; Seaport/order signatures are abused as + gasless NFT/token drainers.** (high) — eocene permit2 analysis; zengo offline-signatures. +33. **EIP-712 domain separator (`name, version, chainId, verifyingContract, salt`) lets a wallet + detect chain/contract mismatch — but the spec only says SHOULD/MAY, and a Coinspect study found + none of 40+ wallets warned on a chainId mismatch.** (high / medium) — eips.ethereum.org/EIPS/eip-712; coinspect blog. +34. **EIP-7702 (Final, live since Pectra May 2025) lets an EOA delegate its code to a contract; its + own Security Considerations say wallets MUST NOT offer an interface to sign arbitrary delegations — + "there is no safe way." >97% of observed delegations went to one sweeper bytecode + ("CrimeEnjoyor").** (high) — eips.ethereum.org/EIPS/eip-7702; coindesk/Wintermute. +35. **ERC-7730 (Draft, Ledger-led, registry under `ethereum/clear-signing-erc7730-registry`) is a JSON + metadata standard describing contract calls + EIP-712 messages in human terms; wallets fetch + descriptors keyed to contract addresses. Coverage is a structural gap — the long tail of contracts + has no descriptor (no authoritative coverage % found).** (high / medium on gap) — + eips.ethereum.org/EIPS/eip-7730; github.com/ethereum/clear-signing-erc7730-registry. +36. **`wallet_addEthereumChain` / `wallet_switchEthereumChain` (EIP-3085/3326) let a dapp add a custom + RPC; the spec says sign only with the *user-submitted* chainId, never one returned by the RPC, and + confirm requester + target chain. Malicious RPCs fake balances to bait transactions.** (high) — + eips.ethereum.org/EIPS/eip-3085, eip-3326. +37. **Per-origin scoping foundations: EIP-2255 (`wallet_requestPermissions`/`getPermissions`, the + `{invoker, parentCapability, caveats}` object) and CAIP-25 `wallet_createSession` (per-scope + accounts × chains × methods). Anti-phishing blocklists ship too (MetaMask `eth-phishing-detect`, + ~205k domains).** (high / medium on count) — eips.ethereum.org/EIPS/eip-2255; chainagnostic.org CAIP-25; github.com/MetaMask/eth-phishing-detect. + +**Takeaway:** any dapp surface MUST treat off-chain signatures as first-class danger and extend +Deckard's clear-signing engine to decode EIP-712 / permit / Permit2 / Seaport, hard-handle EIP-7702 +delegation, consume ERC-7730 where available with a safe blind-sign fallback, guard +`wallet_addEthereumChain`, and scope each origin (accounts × chains × methods) with a shipped +blocklist. Deckard already has the seed of this (`SwapOrder`/`SignOrder` EIP-712 signing, +`PendingPayloadView::Approve`). + +--- + +## Cross-cutting conclusions (feed the ADR) + +- **Embedded webview: reject** (Angle 3). Largest TCB, worst patch cadence, contradicts the + hardened-minimal-core ethos; mobile rationale doesn't transfer to desktop. +- **The boundary fix is mandatory and transport-independent** (Angle 4). Resolver authentication + (capability over a daemon-handed fd) must land before *any* external proposer — it is also + `THREAT-MODEL.md` residual-risk #1. +- **Clear-signing must grow to off-chain signatures** (Angle 5) regardless of transport — the curated + native swap/bridge already needs Permit2/EIP-712. +- **For generic connectivity, WalletConnect dominates the extension** (Angles 1–2) on mobile-reach and + on not-shipping-a-hijackable-artifact, at the cost of a relay dependency (privacy tension) and an + in-house Rust Sign-protocol build. The extension, if ever built, is a secondary desktop convenience. +- **"Curated dapps first" has a more secure reading than *connecting* to dapps at all**: integrate the + curated set *natively* (build Intents in Rust, as Shield/Swap already do), and defer generic + connectivity until the boundary + clear-signing foundations are in and an audit has happened. + +## Source index (primary) + +EIPs/ERCs: eip-712, eip-1193, eip-2255, eip-3085, eip-3326, eip-6963, eip-7702, erc-7730, EIP-1328 +(eips.ethereum.org). WalletConnect: specs.walletconnect.com (crypto-keys, pairing-uri, relay-network, +sign/namespaces, core/verify), docs.reown.com/cloud/relay, github.com/WalletConnect/WalletConnectRust. +Frame: github.com/floating/frame(-extension), docs.frame.sh. Webview: docs.rs/wry, +v2.tauri.app/security, tauri GHSA-57fm-592m-34r7, blogs.gnome.org/mcatanzaro, lists.ubuntu.com +USN-7817-1, github.com/tauri-apps/wry#935. IPC/capability: man7 unix(7), +blog.cloudflare.com/know-your-scm_rights, dl.acm.org/10.1145/54289.871709, +papers.agoric.com capability-myths-demolished, trezord README, ledger secure-screen, gridplus docs. +Signing danger: scamsniffer 2024 drainer report, support.metamask.io, MetaMask MIP-3, +github.com/ethereum/clear-signing-erc7730-registry, github.com/MetaMask/eth-phishing-detect, +coindesk/Wintermute (EIP-7702). Browser security: github.blog localhost-dangers, +portswigger.net cross-site-websocket-hijacking. Supply chain: bleepingcomputer (Great Suspender, +$494M), cyberhaven engineering blog, sekoia.io, thehackernews (Trust Wallet). + +> Confidence + inference flags are per-claim above. Items explicitly unverified by the agents: exact +> native-messaging size ceilings; Frame WebSocket auth specifics; ERC-7730 coverage %; the +> Coinspect "40+ wallets" recency; several single-vendor loss figures. Treat those as directional. From be0bc4cc663340b6385dd15ac0efc0c21c045f34 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 10:37:26 +0000 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20own=20the=20dapp=20transport=20?= =?UTF-8?q?=E2=80=94=20Deckard-native=20bridge,=20shelve=20WalletConnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second-pass requirements: pursue universal dapp reach but own the transport end-to-end. No embedded browser, no WalletConnect relay, no store as a trust anchor. - ADR 0001: Phase 2 becomes a Deckard-native bridge — a first-party, key-less connector injecting a standard EIP-1193/6963 provider over a Deckard-owned local wire (native messaging). WalletConnect moved to rejected/shelved with full rationale; mobile universal-reach recorded as an honest open cost. - PRD-04 rewritten as the Deckard-native bridge; old WalletConnect PRD kept as x-walletconnect-shelved.md (rationale preserved, verbatim original retained). - PRD-05 + PRD-03 + README cross-refs updated to the bridge; origin trust now leans on registry + blocklist + always-clear-signed effects (no relay attestation). - PRD-01 (resolver auth) and PRD-02 (clear-signing) kept unchanged — both stand on their own. https://claude.ai/code/session_01Vp5m1a3P8XePLbyppqzcjN --- .../0001-dapp-connectivity-architecture.md | 98 +++++++---- docs/prd/03-curated-native-integrations.md | 3 +- docs/prd/04-deckard-native-bridge.md | 153 ++++++++++++++++++ .../05-per-origin-permissions-and-registry.md | 13 +- docs/prd/README.md | 13 +- ...ransport.md => x-walletconnect-shelved.md} | 24 ++- 6 files changed, 260 insertions(+), 44 deletions(-) create mode 100644 docs/prd/04-deckard-native-bridge.md rename docs/prd/{04-walletconnect-transport.md => x-walletconnect-shelved.md} (84%) diff --git a/docs/adr/0001-dapp-connectivity-architecture.md b/docs/adr/0001-dapp-connectivity-architecture.md index a04e375..b0c9ad9 100644 --- a/docs/adr/0001-dapp-connectivity-architecture.md +++ b/docs/adr/0001-dapp-connectivity-architecture.md @@ -11,13 +11,17 @@ Captured via a requirements pass before research: 1. **Mobile horizon — "don't compromise desktop for it."** Optimize for macOS/Linux; keep mobile - *possible* but don't let it dictate the desktop design. → favors transports that can survive onto - mobile over desktop-only ones, without over-investing now. -2. **Dapp openness — "curated/allowlisted first."** Start with a vetted set (swaps, bridges); on-brand - "calm sovereign control, not a casino"; per-origin scope; expand later. -3. **Browser engine — "avoid; keep wallet minimal."** Connect to the user's own browser and/or a relay - rather than bundling an engine. Preserves the hardened minimal-core identity. -4. **Deliverable — "ADR first, then multiple PRDs"** with per-workstream Definitions of Done. + *possible* but don't let it dictate the desktop design. +2. **Dapp openness — "curated/allowlisted first," but universal reach is an accepted goal.** Start with + a vetted set (swaps, bridges); on-brand "calm sovereign control, not a casino"; per-origin scope. + Users *will* want arbitrary dapps, so the architecture must reach the long tail eventually. +3. **Browser engine — "avoid; keep wallet minimal."** Do not bundle a browser engine. +4. **Own the connection — "no external bad stuff."** A second-pass requirement: Deckard does **not** + build on a bloated third-party transport with bad UX. No WalletConnect relay, no dependence on a + browser-store as a trust anchor. Universal reach is delivered through a **bridge Deckard owns + end-to-end** (our wire, our UX, local-first, no third-party relay). WalletConnect is recorded as a + rejected alternative, not a roadmap item. +5. **Deliverable — "ADR first, then multiple PRDs"** with per-workstream Definitions of Done. Plus the standing constraint from `SECURITY.md`: Deckard is `0.0.1-alpha`, unaudited, single-maintainer, testnet-keys-only. Any new attack surface is sequenced *behind* an audit. @@ -72,17 +76,27 @@ clear-signing pattern. No new trust boundary is invented; we extend the one we h recognition, EIP-7702 handling, and ERC-7730 consumption with a safe blind-sign fallback (`research §30–36`). Phase 0's swap already needs Permit2/EIP-712, so this is not gated on Phase 2. → **PRD-02**. -- **Phase 2 — Generic dapp connectivity, when warranted (post-audit).** **WalletConnect v2 is the - primary generic transport**, implemented as a new key-less proposer process mirroring `deckard-mcp`, - with CAIP-25 scope negotiation, Verify-API origin attestation, per-origin policy, a curated - registry + anti-phishing blocklist, and explicit relay-privacy mitigations. → **PRD-04** (transport) - + **PRD-05** (per-origin permissions & registry). - - **A browser extension (Option B) is a secondary, optional desktop-convenience add-on**, not the - primary path: it is desktop-only (doesn't reach mobile, driver #1) and the extension artifact is a - recurring drainer/supply-chain target (`research §6`). If ever built, it is a thin EIP-6963 - proposer that speaks the *same* WalletConnect-or-native bridge — never the daemon wire directly, - and never with `resolve` capability. Tracked as a future PRD, not in this batch. +- **Phase 2 — Universal reach via a Deckard-native bridge, when warranted (post-audit).** Deliver + "connect to any dapp" through a transport Deckard **fully owns**: a first-party, key-less connector + that injects a *standard* EIP-1193 + EIP-6963 provider into the user's own browser (so every dapp + works — they already speak EIP-1193) and forwards requests over a **Deckard-owned local wire** (native + messaging preferred over a localhost port, `research §2–5`) to a new key-less proposer process + mirroring `deckard-mcp`. No third-party relay, no embedded engine, no store-as-trust-anchor. The UX + is native cards, local and instant — the explicit antidote to WalletConnect's relay-latency/QR-dance + UX. → **PRD-04** (the bridge) + **PRD-05** (per-origin permissions, scope & registry). + - **WalletConnect v2 is rejected and shelved** (see Alternatives; rationale preserved in + [`docs/prd/x-walletconnect-shelved.md`](../prd/x-walletconnect-shelved.md)). It would import a + centralized relay + Project-ID dependency (privacy/offline-first tension) and a UX the maintainer + explicitly does not want. - The embedded webview stays rejected. + - **Honest cost:** a browser-side connector is desktop-only. Rejecting WalletConnect (the + mobile-capable transport) leaves *mobile* universal-reach unsolved; this is consistent with driver + #1 ("don't compromise desktop for it") but is the deliberate trade and an open question for a future + mobile effort, not something this ADR solves. + - **Supply-chain containment:** the connector is a recurring class of attack target (`research §6`), + so its trust is *bounded by design*, not by the store: it is key-less, cannot `resolve` (PRD-01), + submits only typed intents through PRD-02 builders, and every effect is clear-signed. A fully + compromised connector can propose garbage — it can never sign, self-approve, or exfiltrate a key. **3. Cross-cutting invariants every phase inherits (non-negotiable):** - New proposers are **key-less**, reuse `deckard-contract`, and **cannot** `resolve` or submit a raw @@ -97,28 +111,42 @@ clear-signing pattern. No new trust boundary is invented; we extend the one we h **Positive** - The security boundary is *strengthened* before any surface is added (PRD-01 fixes a standing accepted risk regardless of connectivity). -- The minimal-core identity and offline-first posture are preserved (no engine; Phase 0/1 need no - network relay at all). +- The minimal-core, offline-first, privacy identity is preserved *all the way through* — the owned + bridge has **no third-party relay**, so no IP/metadata leaks to a Reown-style intermediary; everything + stays local. This is a genuine privacy win over WalletConnect, not just parity. +- We own the UX end-to-end: native cards, local and instant, no QR pairing or relay round-trip — the + deliberate answer to "WalletConnect UX sucks." +- Universal reach is still achieved (any EIP-1193 dapp) without becoming a browser or depending on a + relay/store. - Phasing matches the alpha → audit → 1.0 trajectory: ship the safest, smallest thing first. -- WalletConnect gives a single transport that reaches desktop today and mobile later, so the eventual - mobile app doesn't force an architecture rewrite (honors driver #1 cheaply). **Negative / costs** -- WalletConnect imports a **centralized relay + Project-ID dependency** (privacy/offline-first tension, - `research §10–11`) and a **non-trivial in-house Rust Sign-protocol build** (no maintained Rust - wallet SDK, `research §14`). PRD-04 must address relay-egress privacy and the build/borrow decision. -- Declining the embedded browser means Deckard never offers a fully in-app "open any dapp" experience; - the curated-native + WalletConnect combination is the deliberate substitute. -- The extension being deprioritized means desktop-browser dapps that only speak injected - `window.ethereum` (not WalletConnect) aren't reachable until/unless the optional extension ships. +- **Mobile universal-reach is unsolved.** A browser-side connector is desktop-only; we gave up the one + mobile-capable transport (WalletConnect). Accepted under driver #1, but flagged as a future open + problem. +- **We carry a first-party connector** (a browser artifact + a native-messaging host) — engineering and + a perpetual supply-chain target to maintain. Contained by the key-less/bounded-boundary design, but + it is real surface we now own. +- **Building our own wire** instead of adopting WalletConnect is more invention; we don't get dapps' + existing WC support for free (mitigated: dapps speak EIP-1193 already, so the *injected provider* + path needs no per-dapp work). +- Declining the embedded browser means Deckard never offers a fully in-app "open any dapp" surface; the + curated-native + owned-bridge combination is the deliberate substitute. ## Alternatives considered (and rejected) - **Embedded webview (Option A as clarified):** rejected — see Decision §1. -- **Browser extension as the *primary* transport (Option B):** rejected as primary — desktop-only and - a supply-chain liability; retained only as an optional secondary add-on. -- **Frame-style localhost RPC bridge (`ws://127.0.0.1:1248`):** rejected — web-reachable - (DNS-rebinding / cross-site WebSocket hijacking), defended only at the UI layer (`research §2`). +- **WalletConnect v2 as the generic transport:** **rejected and shelved** (full rationale: + [`docs/prd/x-walletconnect-shelved.md`](../prd/x-walletconnect-shelved.md)). Summary: a centralized + Reown relay + Project-ID dependency that leaks IP/metadata (against offline-first/privacy, + `research §10–11`), a QR/relay UX the maintainer explicitly rejects, and no maintained Rust + wallet-side SDK (`research §14`). Its one advantage — mobile reach — does not outweigh owning the + transport and UX. Recorded so the decision isn't re-litigated. +- **Generic browser extension over a Frame-style localhost RPC bridge (`ws://127.0.0.1:1248`):** the + *localhost* wire is rejected — web-reachable (DNS-rebinding / cross-site WebSocket hijacking), defended + only at the UI layer (`research §2`). The owned bridge (PRD-04) prefers **native messaging** (stdio + host, not web-reachable) for exactly this reason. A first-party connector is *kept* (it is the bridge's + browser end), but its trust is bounded by the key-less/clear-signing boundary, not by the store. - **In-process plugins / dylibs loaded into the app or daemon:** rejected outright — same-uid code with arbitrary execution lands on the *trusted* side of the boundary and can self-approve; this is the maximal form of residual-risk #1. (Extensibility, if wanted, must be sandboxed, key-less, @@ -131,7 +159,9 @@ clear-signing pattern. No new trust boundary is invented; we extend the one we h daemon-spawns-app vs app-spawns-daemon direction inversion). - PRD-02: which `IntentKind`s to add (`SignMessage`, `SignTypedData`, and whether EIP-7702 `Delegate` is supported-behind-allowlist or refused outright). -- PRD-04: build the WC Sign layer in Rust vs run a sidecar (a key-less WC↔daemon translator akin to - `deckard-mcp`); relay privacy egress; whether a custom/self-hosted relay URL is exposed. +- PRD-04: the browser-reach mechanism for the owned bridge — native-messaging stdio host (preferred, + not web-reachable) vs a hardened localhost wire; how the first-party connector is distributed and + signed without leaning on a store as a trust anchor; and how/whether mobile reach is ever addressed + given the connector is desktop-only. - PRD-05: registry format (adopt ERC-7730 descriptors directly?) and how the curated allowlist ships and updates offline-first. diff --git a/docs/prd/03-curated-native-integrations.md b/docs/prd/03-curated-native-integrations.md index 7da59e2..12b1074 100644 --- a/docs/prd/03-curated-native-integrations.md +++ b/docs/prd/03-curated-native-integrations.md @@ -27,7 +27,8 @@ Phase-2 connectivity machinery (PRD-04/05). ## Non-goals -- Generic / open dapp access (PRD-04). No `window.ethereum`, no WalletConnect here. +- Generic / open dapp access (PRD-04, the Deckard-native bridge). No `window.ethereum`, no external + transport here — these are native Rust code paths only. - Per-origin permissions (PRD-05) — there is no untrusted origin; the integration *is* Deckard code. - New signing primitives beyond what PRD-02 adds (a swap needs Permit2/EIP-712 → that's PRD-02's job; this PRD consumes it). diff --git a/docs/prd/04-deckard-native-bridge.md b/docs/prd/04-deckard-native-bridge.md new file mode 100644 index 0000000..aab75db --- /dev/null +++ b/docs/prd/04-deckard-native-bridge.md @@ -0,0 +1,153 @@ +# PRD-04 — Deckard-native bridge (universal dapp reach, owned end-to-end) + +> Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). Delivers "connect to **any** +> dapp" through a transport Deckard **fully owns** — our wire, our UX, local-first, **no third-party +> relay, no embedded browser, no store as a trust anchor**. This is the deliberate replacement for +> WalletConnect ([shelved](./x-walletconnect-shelved.md)). **Gated on PRD-01 (resolver auth), +> PRD-02 (clear-signing v2), PRD-05 (per-origin permissions), and an external audit** (`SECURITY.md`). + +## Why this exists + +The maintainer wants **universal reach** (users will want arbitrary dapps) **without** the "external +bad stuff": no bundled browser engine (rejected, ADR §1) and no WalletConnect (a centralized relay that +leaks metadata, a Project-ID dependency, and a QR/relay UX the maintainer rejects — `research §10–14`). + +The resolution: dapps already speak **EIP-1193**. So we reach all of them by injecting a *standard* +EIP-1193 + EIP-6963 provider (`research §7, 21–22`) through a **first-party connector** in the user's +own browser, and carry requests over a **wire Deckard defines** to a key-less proposer that speaks the +existing `deckard-contract` wire to `deckard-signerd`. Same proven pattern as `deckard-mcp` +(`docs/build/30-mcp-shape.md`) — a thin, key-less translator — but for dapps, and entirely local. + +The payoffs are exactly the vision: **no relay → no third-party metadata leak** (a real privacy win +over WalletConnect, on-brand offline-first); **native, instant approval cards → the UX we control** +(no QR dance, no relay latency); universal reach without becoming a browser. + +## Goals + +- A **key-less proposer process** (working name `deckard-bridged`) mirroring `deckard-mcp`: speaks the + owned wire to the connector and the `deckard-contract` wire to the daemon. Holds **no key**, **cannot + `resolve`** (PRD-01 enforces this even if it tried), submits only **typed** intents/messages via the + PRD-02 builders — never raw arbitrary calldata that skips a typed path. +- A **first-party browser connector** that injects EIP-1193 + EIP-6963 (`rdns: sh.deckard` or similar), + giving universal reach (every dapp works) without per-dapp integration. +- A **Deckard-owned local wire** between connector and `deckard-bridged`. **Native messaging is the + default** (stdio host, gated by `allowed_origins`/`allowed_extensions`, **not web-reachable**) — + chosen over a localhost RPC port precisely because a localhost port is reachable by any page via + DNS-rebinding / cross-site WebSocket hijacking (`research §2–4`, the Frame weakness). +- Map dapp methods → Deckard surfaces: `eth_sendTransaction` → `Intent`; `personal_sign` / + `eth_signTypedData_v4` → `SignMessage` (PRD-02); `wallet_switchEthereumChain`/`addEthereumChain` → + guarded per PRD-02. **Refuse** `eth_sign` and (v1) EIP-7702 delegation. +- Per-origin scope enforced via PRD-05 (accounts × chains × methods). + +## Non-goals + +- WalletConnect (shelved) and embedded webview (rejected). +- **Mobile.** A browser-side connector is desktop-only. Mobile universal-reach is an explicit open + problem (ADR Consequences), NOT solved here. Keep `deckard-bridged`'s core protocol logic + platform-agnostic so a future mobile mechanism can reuse it, but do not build for mobile now + (driver #1: don't compromise desktop for it). +- Generic ABI decoding (PRD-02 scope: high-risk shapes + ERC-7730 + blind-sign fallback). + +## Design + +### Process topology (mirror `deckard-mcp`) + +``` +dapp ──EIP-1193──► Deckard connector ──owned wire (native messaging)──► deckard-bridged ──contract wire──► deckard-signerd + (any site) (first-party, (key-less, (holds key, + injects std provider) no resolve) policy gate) + │ + native clear-signing card ◄── GPUI app + (control channel, PRD-01) +``` + +`deckard-bridged` is to dapps what `deckard-mcp` is to LLM agents. Reuse the daemon socket, the +`Propose`/`Execute`/`Status` poll loop, and the native approval card unchanged. + +### The owned wire (key decision — record the choice) + +- **Default: native messaging** (Chrome `connectNative` / Firefox `runtime.connectNative`). The host is + an OS-installed stdio binary gated by a manifest `allowed_origins` (exact extension id, no wildcards) + and runs as the user (`research §3, 12`). It is **not reachable from web pages** — the decisive + advantage over a localhost port. Caveats to handle: MV3 service-worker lifecycle (keep the port alive, + reconnect on disconnect, `research §5`); the host-manifest registration lives in user-writable paths + (`research §4`) — install it ourselves and document the integrity expectation. +- **Rejected sub-option: localhost RPC** (Frame's `ws://127.0.0.1`). Web-reachable; would require + origin-allowlist + token + Host-header validation just to approach native-messaging's isolation + (`research §2`). Only consider if a browser without native-messaging support must be served, and then + only with those defenses. + +### The first-party connector (trust is bounded, not store-based) + +- Minimal, open-source, **key-less**: it injects a standard provider and relays JSON-RPC to the host. + It holds no key, has no signing power, and **cannot reach the daemon's `Resolve`** (PRD-01). +- **"No store as a trust anchor":** browser stores (Chrome Web Store, AMO) are distribution channels we + may use, but they are **not** where security comes from. The connector being a recurring attack target + (`research §6`: Great Suspender, Cyberhaven) is *contained by design* — a fully compromised connector + can only **propose**; it cannot sign, self-approve, or exfiltrate a key, and every effect is + clear-signed (PRD-02) against attacker-controllable origin (`research §29`). Prefer self-distributed/ + self-signed where the browser allows (e.g. Firefox self-distribution); treat the store listing as + convenience, not trust. +- Inject via **EIP-6963** (announce with a stable `rdns`), with `window.ethereum` as legacy fallback, + so Deckard coexists with other wallets instead of fighting over the global (`research §7, 21–22`). + +### UX (the explicit anti-WalletConnect) + +- Connecting is a native flow: the dapp requests, Deckard raises a **native connect card** showing the + (unverified) origin + requested scope (PRD-05); approval is local and instant — no QR, no relay. +- Every signing request renders the PRD-02 clear-signing card. Origin shown as unverified unless + corroborated (PRD-05). Reuse `DESIGN.md` card anatomy, danger-early, hold-to-confirm. +- Sessions are listable and revocable from ⌘K + settings governance (next to "Pause all agents"). + +## Acceptance tests + +- **Spike first** (mirror `spikes/`): prove a real dapp → connector → native-messaging host → + `deckard-bridged` → daemon round-trip for `eth_requestAccounts` + one `personal_sign`, with the native + card rendering. Commit a short report before the full build. +- `bridged_is_keyless`: memory/fd scan of `deckard-bridged` finds no key (reuse the `deckard-mcp` + red-team harness, `docs/build/00-test-harness.md`). +- `bridged_cannot_resolve`: `deckard-bridged` attempting `Resolve` is refused by PRD-01's control-channel + gate (cross-PRD integration test). +- `wire_not_web_reachable`: assert the chosen wire (native messaging) exposes no listening TCP/WS port a + web page could reach; if a localhost fallback is ever used, a cross-origin page request without the + token/Host check is rejected. +- `method_out_of_scope_refused`: a dapp method/chain/account outside the PRD-05 per-origin grant is + refused; nothing reaches the daemon. +- `eth_sign_refused` / `delegation_refused`: dapp requests for `eth_sign` and EIP-7702 authorizations are + refused (defense-in-depth with PRD-02). +- `eip6963_announced`: the connector announces via EIP-6963 with the stable `rdns` and coexists with a + second injected provider. +- Transcript hygiene extended to `deckard-bridged` (no secret/RPC-token leakage). + +## Definition of Done + +PRD-series DoD (see [`README.md`](./README.md)) **plus**: +- ⌘K commands: connect, disconnect, list sessions, revoke session, STOP-all-sessions. +- The owned-wire choice (native messaging vs localhost) and the connector distribution/trust model are + documented; the "store is not a trust anchor; trust is bounded by key-less + clear-signing" argument + is written into `SECURITY.md` + `THREAT-MODEL.md` as a new "dapp bridge" surface. +- The desktop-only / mobile-open-question is recorded honestly. +- Off by default — the bridge is an opt-in network/browser feature in an offline-first wallet; it never + installs a host or opens a connection silently. +- New dependencies (if any) explicitly approved and reviewed against `deny.toml`/`Cargo.lock`. +- **External audit sign-off recorded** before shipping beyond testnet (`SECURITY.md`). + +## Risks & fallbacks + +- **First-party connector is a standing supply-chain target** (`research §6`). Contained by design + (key-less, no-resolve, clear-signed, bounded blast radius); keep it minimal and open-source; consider + reproducible builds + signature pinning between connector and host. +- **Native-messaging host registration is user-writable** (`research §4`) — any same-uid code can + overwrite the manifest. This is the same uid boundary the whole model already assumes; document it and + lean on PRD-01 (the host is key-less and can't self-approve regardless). +- **MV3 lifecycle flakiness** (`research §5`) — reconnect logic + heartbeat; covered by the spike. +- **We're inventing a wire.** Keep it small and typed; reuse `deckard-contract` shapes wherever possible + so the connector↔host protocol is a thin envelope, not a second contract. +- **Mobile remains unsolved** — acknowledged; revisit as a separate effort if/when mobile is funded. + +## Sources + +`docs/research/10-dapp-connectivity.md §1–7 (extension/bridge), §22–29 (boundary), §30–37 (signing & +scope)`; `docs/build/30-mcp-shape.md` (the proposer pattern to mirror); EIP-1193, EIP-6963; +developer.chrome.com native-messaging; github.blog localhost-dangers; the shelved WalletConnect +rationale ([`x-walletconnect-shelved.md`](./x-walletconnect-shelved.md)). diff --git a/docs/prd/05-per-origin-permissions-and-registry.md b/docs/prd/05-per-origin-permissions-and-registry.md index e0c4204..3bbc614 100644 --- a/docs/prd/05-per-origin-permissions-and-registry.md +++ b/docs/prd/05-per-origin-permissions-and-registry.md @@ -1,9 +1,9 @@ # PRD-05 — Per-origin permissions, dapp registry & anti-phishing > Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The policy + trust layer that -> makes generic connectivity (PRD-04) safe: scope each connected origin (accounts × chains × methods), -> ship a curated registry + anti-phishing blocklist, and treat origin as attacker-controllable. -> Pairs with PRD-04; depends on PRD-02. +> makes the Deckard-native bridge (PRD-04) safe: scope each connected origin (accounts × chains × +> methods), ship a curated registry + anti-phishing blocklist, and treat origin as +> attacker-controllable. Pairs with PRD-04; depends on PRD-02. ## Why this exists @@ -51,8 +51,11 @@ blocklist, and the human must always approve on decoded effects, not a claimed n ### Origin attestation (treat as a hint, `research §29`) -- The daemon receives an origin string from the (untrusted) proposer. It is displayed as **unverified** - unless corroborated by (a) WalletConnect Verify-API state (PRD-04) and/or (b) a registry match. +- The daemon receives an origin string from the (untrusted) connector/proposer. It is displayed as + **unverified** unless corroborated by (a) a curated-registry match and/or (b) any first-party + attestation the bridge can establish (PRD-04). There is no third-party relay attestation (we shelved + WalletConnect's Verify API along with WalletConnect) — so origin trust leans on the registry + + blocklist + the always-clear-signed effects, never a remote checkmark. - The card shows attestation state (verified-domain / unverified / mismatch / known-scam) using `DESIGN.md` caution affordances — but the **decoded effects** remain the ground truth the human holds to confirm. Never gate solely on a green checkmark. diff --git a/docs/prd/README.md b/docs/prd/README.md index 900848e..4e18c32 100644 --- a/docs/prd/README.md +++ b/docs/prd/README.md @@ -22,8 +22,8 @@ done while red): ``` PRD-01 Resolver auth ─────────────┐ - ├─► PRD-04 WalletConnect transport ─► (Phase 2, post-audit) -PRD-02 Clear-signing v2 ──────────┤ + ├─► PRD-04 Deckard-native bridge ─► (Phase 2, post-audit) +PRD-02 Clear-signing v2 ──────────┤ (universal reach, owned wire) │ └─► PRD-05 Per-origin permissions + registry └─► PRD-03 Curated native integrations (Phase 0, ships first) ``` @@ -33,8 +33,15 @@ PRD-02 Clear-signing v2 ──────────┤ | [01](./01-resolver-authentication.md) | Resolver authentication (capability-gated `Resolve`) | 1a | — | independently; closes residual-risk #1 | | [02](./02-clear-signing-and-message-intents.md) | Clear-signing v2 + message-signing intents | 1b | — | independently; needed by PRD-03 & PRD-04 | | [03](./03-curated-native-integrations.md) | Curated native dapp integrations | 0 | PRD-02 (for permit/EIP-712) | first user-visible value | -| [04](./04-walletconnect-transport.md) | WalletConnect v2 transport (`deckard-wcd`) | 2 | PRD-01, PRD-02, PRD-05 | post-audit | +| [04](./04-deckard-native-bridge.md) | Deckard-native bridge (universal reach, owned wire) | 2 | PRD-01, PRD-02, PRD-05 | post-audit | | [05](./05-per-origin-permissions-and-registry.md) | Per-origin permissions, registry & anti-phishing | 2 | PRD-02 | with PRD-04 | +| [x](./x-walletconnect-shelved.md) | ~~WalletConnect transport~~ | — | — | **SHELVED / rejected** (rationale recorded) | **Recommended execution order:** PRD-01 and PRD-02 in parallel (foundational, independent) → PRD-03 (first shippable value) → PRD-05 → PRD-04 (gated on an external audit per `SECURITY.md`). + +**Connectivity model (ADR 0001, second-pass requirements):** Deckard pursues **universal dapp reach** +but **owns the transport end-to-end** — no embedded browser, no WalletConnect relay, no store as a +trust anchor. Reach comes from injecting a standard EIP-1193/6963 provider via a first-party, +key-less connector over a Deckard-owned local wire (native messaging). WalletConnect is shelved with +rationale; the embedded webview is rejected. diff --git a/docs/prd/04-walletconnect-transport.md b/docs/prd/x-walletconnect-shelved.md similarity index 84% rename from docs/prd/04-walletconnect-transport.md rename to docs/prd/x-walletconnect-shelved.md index a83bc6e..a162d31 100644 --- a/docs/prd/04-walletconnect-transport.md +++ b/docs/prd/x-walletconnect-shelved.md @@ -1,4 +1,26 @@ -# PRD-04 — WalletConnect v2 transport (`deckard-wcd`) +# SHELVED — WalletConnect v2 transport (rejected alternative) + +> **Status: REJECTED / SHELVED (2026-06-14).** Not on the roadmap. Superseded by the **Deckard-native +> bridge** ([PRD-04](./04-deckard-native-bridge.md)), which delivers universal dapp reach over a +> transport Deckard owns end-to-end. Retained only so the decision is recorded and not re-litigated. +> See [ADR 0001 §Alternatives](../adr/0001-dapp-connectivity-architecture.md). +> +> **Why shelved:** +> 1. **Centralized relay + Project-ID dependency** (`research §10–11`) — leaks IP/timing/topic metadata +> to a third-party (Reown) relay and requires a Project ID with usage analytics. Against Deckard's +> offline-first, privacy-focused identity. +> 2. **UX the maintainer explicitly rejects** — QR-pairing / relay round-trips / session churn. Deckard +> wants native, local, instant approval cards, which the owned bridge provides. +> 3. **No maintained Rust wallet-side SDK** (`research §14`) — the Sign protocol is an in-house build +> either way, so "adopt a standard to save work" is weak. +> 4. Its one genuine advantage — **mobile reach** — does not outweigh 1–3. Mobile universal-reach is +> left as an explicit open problem (ADR 0001 Consequences), not solved via WC. +> +> The text below is the original (pre-shelving) proposal, kept verbatim for the record. + +--- + +# PRD-04 — WalletConnect v2 transport (`deckard-wcd`) — ORIGINAL, SHELVED > Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The **primary generic > dapp-connectivity transport**: reaches desktop today and mobile later, no browser extension to ship From f1a7bdb9ee34213e20e62ffe7101989e9e002e74 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 10:48:48 +0000 Subject: [PATCH 3/4] docs: PRD-04 refinements + Deckard-bridge spike brief Per refinement pass: - Wire (native messaging vs hardened localhost) is now a spike decision, not pre-committed; native messaging remains the leading candidate on security. - Connector distribution: self-distributed + signed first (trust = our signature + key-less design, not a store review); target Chromium + Firefox together from the start. - Add spikes/deckard-bridge-spike.md: a self-contained brief (house style) to prove the connector -> host -> mock daemon EIP-1193 round-trip on both browsers and settle the wire + self-distribution choices before the build. https://claude.ai/code/session_01Vp5m1a3P8XePLbyppqzcjN --- docs/prd/04-deckard-native-bridge.md | 67 +++++++++------- spikes/deckard-bridge-spike.md | 109 +++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 26 deletions(-) create mode 100644 spikes/deckard-bridge-spike.md diff --git a/docs/prd/04-deckard-native-bridge.md b/docs/prd/04-deckard-native-bridge.md index aab75db..9e1e2d1 100644 --- a/docs/prd/04-deckard-native-bridge.md +++ b/docs/prd/04-deckard-native-bridge.md @@ -29,11 +29,19 @@ over WalletConnect, on-brand offline-first); **native, instant approval cards `resolve`** (PRD-01 enforces this even if it tried), submits only **typed** intents/messages via the PRD-02 builders — never raw arbitrary calldata that skips a typed path. - A **first-party browser connector** that injects EIP-1193 + EIP-6963 (`rdns: sh.deckard` or similar), - giving universal reach (every dapp works) without per-dapp integration. -- A **Deckard-owned local wire** between connector and `deckard-bridged`. **Native messaging is the - default** (stdio host, gated by `allowed_origins`/`allowed_extensions`, **not web-reachable**) — - chosen over a localhost RPC port precisely because a localhost port is reachable by any page via - DNS-rebinding / cross-site WebSocket hijacking (`research §2–4`, the Frame weakness). + giving universal reach (every dapp works) without per-dapp integration. **Target Chromium *and* + Firefox from the start** (separate manifests; MV3 service-worker quirks on the Chromium side). +- **Distribution: self-distributed + signed first.** Ship a Deckard-signed connector we host ourselves + (Firefox supports self-distribution of signed extensions; sideload/dev paths on Chromium) so trust is + **our signature + the key-less design, not a store review**. Store listings, if used later, are + convenience only — never the trust anchor. +- A **Deckard-owned local wire** between connector and `deckard-bridged`. **The exact wire is a spike + decision** (PRD-04 spike, [`spikes/deckard-bridge-spike.md`](../../spikes/deckard-bridge-spike.md)): + the leading candidate is **native messaging** (stdio host, gated by `allowed_origins`/ + `allowed_extensions`, **not web-reachable**), preferred on security over a localhost RPC port — a + localhost port is reachable by any page via DNS-rebinding / cross-site WebSocket hijacking + (`research §2–4`, the Frame weakness). The spike proves both end-to-end on both browsers and records + the choice before the full build. - Map dapp methods → Deckard surfaces: `eth_sendTransaction` → `Intent`; `personal_sign` / `eth_signTypedData_v4` → `SignMessage` (PRD-02); `wallet_switchEthereumChain`/`addEthereumChain` → guarded per PRD-02. **Refuse** `eth_sign` and (v1) EIP-7702 delegation. @@ -64,30 +72,35 @@ dapp ──EIP-1193──► Deckard connector ──owned wire (native messagin `deckard-bridged` is to dapps what `deckard-mcp` is to LLM agents. Reuse the daemon socket, the `Propose`/`Execute`/`Status` poll loop, and the native approval card unchanged. -### The owned wire (key decision — record the choice) +### The owned wire (settled by the PRD-04 spike) -- **Default: native messaging** (Chrome `connectNative` / Firefox `runtime.connectNative`). The host is - an OS-installed stdio binary gated by a manifest `allowed_origins` (exact extension id, no wildcards) - and runs as the user (`research §3, 12`). It is **not reachable from web pages** — the decisive - advantage over a localhost port. Caveats to handle: MV3 service-worker lifecycle (keep the port alive, - reconnect on disconnect, `research §5`); the host-manifest registration lives in user-writable paths - (`research §4`) — install it ourselves and document the integrity expectation. -- **Rejected sub-option: localhost RPC** (Frame's `ws://127.0.0.1`). Web-reachable; would require - origin-allowlist + token + Host-header validation just to approach native-messaging's isolation - (`research §2`). Only consider if a browser without native-messaging support must be served, and then - only with those defenses. +The spike ([`spikes/deckard-bridge-spike.md`](../../spikes/deckard-bridge-spike.md)) proves both +candidates end-to-end on Chromium **and** Firefox, then records the choice with rationale: + +- **Leading candidate: native messaging** (Chrome `connectNative` / Firefox `runtime.connectNative`). + The host is an OS-installed stdio binary gated by a manifest `allowed_origins`/`allowed_extensions` + (exact id, no wildcards) and runs as the user (`research §3, 12`). It is **not reachable from web + pages** — the decisive security advantage over a localhost port. Caveats the spike must exercise: MV3 + service-worker lifecycle (keep the port alive, reconnect on disconnect, `research §5`); host-manifest + registration lives in user-writable paths (`research §4`) — we install it ourselves. +- **Alternative measured by the spike: hardened localhost RPC** (Frame-style `ws://127.0.0.1`). + Web-reachable, so it would need origin-allowlist + token + Host-header validation just to approach + native-messaging's isolation (`research §2`). The spike documents whether any target browser/setup + forces it; default is native messaging unless the spike finds a blocker. ### The first-party connector (trust is bounded, not store-based) - Minimal, open-source, **key-less**: it injects a standard provider and relays JSON-RPC to the host. It holds no key, has no signing power, and **cannot reach the daemon's `Resolve`** (PRD-01). -- **"No store as a trust anchor":** browser stores (Chrome Web Store, AMO) are distribution channels we - may use, but they are **not** where security comes from. The connector being a recurring attack target - (`research §6`: Great Suspender, Cyberhaven) is *contained by design* — a fully compromised connector - can only **propose**; it cannot sign, self-approve, or exfiltrate a key, and every effect is - clear-signed (PRD-02) against attacker-controllable origin (`research §29`). Prefer self-distributed/ - self-signed where the browser allows (e.g. Firefox self-distribution); treat the store listing as - convenience, not trust. +- **Self-distributed + signed first, Chromium + Firefox.** We host a Deckard-signed connector + ourselves; trust is our signature + the key-less design, not a store review. Firefox supports + self-distribution of signed XPIs directly; Chromium normal installs lean on the Web Store, so the + spike records the Chromium self-/sideload-distribution path and its friction. Any store listing is + convenience, **never** the trust anchor. +- **"No store as a trust anchor" holds because trust is bounded by design.** The connector is a + recurring attack target (`research §6`: Great Suspender, Cyberhaven), but a fully compromised + connector can only **propose** — it cannot sign, self-approve, or exfiltrate a key, and every effect + is clear-signed (PRD-02) against an attacker-controllable origin (`research §29`). - Inject via **EIP-6963** (announce with a stable `rdns`), with `window.ethereum` as legacy fallback, so Deckard coexists with other wallets instead of fighting over the global (`research §7, 21–22`). @@ -101,9 +114,11 @@ dapp ──EIP-1193──► Deckard connector ──owned wire (native messagin ## Acceptance tests -- **Spike first** (mirror `spikes/`): prove a real dapp → connector → native-messaging host → - `deckard-bridged` → daemon round-trip for `eth_requestAccounts` + one `personal_sign`, with the native - card rendering. Commit a short report before the full build. +- **Spike first** ([`spikes/deckard-bridge-spike.md`](../../spikes/deckard-bridge-spike.md)): prove a + real dapp → connector → host → `deckard-bridged` → daemon round-trip for `eth_requestAccounts` + one + `personal_sign`, with the native card rendering, on **both Chromium and Firefox**, and settle the wire + (native messaging vs localhost) + the self-distribution path. Commit the spike report before the full + build. - `bridged_is_keyless`: memory/fd scan of `deckard-bridged` finds no key (reuse the `deckard-mcp` red-team harness, `docs/build/00-test-harness.md`). - `bridged_cannot_resolve`: `deckard-bridged` attempting `Resolve` is refused by PRD-01's control-channel diff --git a/spikes/deckard-bridge-spike.md b/spikes/deckard-bridge-spike.md new file mode 100644 index 0000000..15599aa --- /dev/null +++ b/spikes/deckard-bridge-spike.md @@ -0,0 +1,109 @@ +# Spike prompt: the Deckard-native dapp bridge (PRD-04 #1) + +> Feed this whole file to a fresh coding agent. It is self-contained. Goal: prove (or disprove) that a +> **first-party, key-less browser connector** can give Deckard universal dapp reach over a wire Deckard +> **owns end-to-end** — no third-party relay, no embedded browser, no store as a trust anchor — and +> settle the two open choices: **(a) the local wire** (native messaging vs hardened localhost) and +> **(b) the self-distribution path** on both Chromium and Firefox. Standalone spike — do **not** touch +> the app crates (`crates/`) or ship anything user-facing. + +Executes the first deliverable of [`docs/prd/04-deckard-native-bridge.md`](../docs/prd/04-deckard-native-bridge.md). +Read it, [ADR 0001](../docs/adr/0001-dapp-connectivity-architecture.md), and +[`docs/research/10-dapp-connectivity.md`](../docs/research/10-dapp-connectivity.md) first. The research +section references below (`§N`) point at claims in that file. + +## Context you can trust (verified in research — don't re-litigate) + +- **Dapps already speak EIP-1193**, so injecting a *standard* provider (announced via **EIP-6963**, not + fighting over `window.ethereum`) reaches all of them with no per-dapp work (`§7, 21–22`). +- **Native messaging is the leading wire**: an OS-installed stdio host gated by a manifest + `allowed_origins` (Chrome, exact `chrome-extension://`, no wildcards) / `allowed_extensions` + (Firefox, the gecko id), running as the user, **not reachable from web pages** (`§3, 12, 15`). + Manifest locations differ per browser/OS and live in user-writable paths (`§4, 15`). +- **A localhost RPC port (Frame's `ws://127.0.0.1:1248`) is web-reachable** — same-origin policy does + not stop a page from calling localhost, WebSockets aren't CORS-gated at handshake, and DNS-rebinding + defeats naive Host checks. If used at all it needs origin-allowlist + token + Host validation, and + even then it's weaker than native messaging (`§1, 2`). +- **MV3 service workers are ephemeral**; a `connectNative` port keeps the worker alive but has had + cross-version edge cases — you must reconnect on `onDisconnect` (`§5`). +- **Trust is bounded by design, not by the store.** The connector is key-less: even fully compromised it + can only *propose*; it can never sign, self-approve, or exfiltrate a key (the daemon owns the key; the + resolver capability is PRD-01; effects are clear-signed, PRD-02). This is *why* self-signed + distribution outside a store is acceptable (`§6, 22, 29`). +- The existing **`deckard-mcp`** is the proposer pattern to mirror: a key-less translator speaking the + `deckard-contract` wire to `deckard-signerd` (`docs/build/30-mcp-shape.md`). The mock daemon in + `deckard-contract` (`MockSigner`) answers the socket API without a real key. + +## The two questions the spike must settle + +1. **Wire:** does **native messaging** carry a full EIP-1193 request/response round-trip cleanly on + **both Chromium and Firefox**, with stable reconnection under MV3 SW eviction? If yes, it wins and + localhost is not needed. Only if a target browser/setup *blocks* native messaging do we fall back to + a hardened localhost wire — in which case prove the origin-allowlist + token + Host-check defenses. +2. **Distribution:** can we **self-distribute a Deckard-signed connector** on both browsers without + relying on a store as the trust anchor? Document the exact path + friction: + - Firefox: self-hosted signed XPI (AMO-signed but self-distributed) — confirm it installs. + - Chromium: normal installs lean on the Web Store; document the self-/sideload path (unpacked/dev + mode, enterprise policy, `.crx` + `update_url`) and its real-world friction for an end user. + +## Tasks (do in order) + +1. **Stand up a mock host + mock daemon.** A small Rust binary (the spike's `deckard-bridged` stand-in) + that: (a) speaks the browser **native-messaging stdio framing** (32-bit native-endian length prefix + + UTF-8 JSON, `§12`) on one side; (b) speaks the `deckard-contract` wire to the **`MockSigner`** (no + real key) on the other. Map `eth_requestAccounts` → `Address`, `personal_sign` → a (mock) message + sign, `eth_sendTransaction` → `Propose`/`Status`/`Execute`. Standalone crate under + `spikes/deckard-bridge/` with its own `[workspace]`; `.gitignore` `target/` + `Cargo.lock`. Depend on + `deckard-contract` by path for the wire types; **no real key, no real signing, no broadcasting.** +2. **Build a minimal connector** (Chromium MV3 + Firefox MV2/MV3 as needed; separate manifests). It + injects an EIP-1193 provider, announces it via **EIP-6963** (`rdns` e.g. `sh.deckard`), and relays + page JSON-RPC → background → native-messaging port → host. Handle MV3 SW lifecycle: reconnect on + `onDisconnect`, no lost requests. +3. **Drive a real dapp end to end.** Point a simple test page (or a real read-only dapp) at the injected + provider and complete: `eth_requestAccounts` (returns the mock address) and one `personal_sign` + (returns a mock signature). Confirm the request reaches the host and the mock daemon, on **Chromium + and Firefox**. +4. **Stress the MV3 path.** Force service-worker eviction (idle it out / `chrome://serviceworker-internals`) + mid-session and confirm the next request reconnects and succeeds (`§5`). +5. **Prove the web-unreachability of native messaging.** From an ordinary web page, attempt to reach the + host directly (there should be no port to reach). Then, for contrast, if you also stand up the + localhost fallback, show a cross-origin page *can* reach it without the token/Host defenses — and that + the defenses block it. This is the security evidence for the wire choice. +6. **Self-distribution dry run.** Produce a signed Firefox XPI and install it from a self-hosted URL + (not AMO listing). Produce the Chromium artifact and document the install path + friction. Record + exact steps so PRD-04 can reproduce. + +## Constraints + +- Standalone: `spikes/deckard-bridge/` (Rust host + mock) with its own `[workspace]`; the connector is + plain JS/TS, no build pipeline beyond what's needed. Do **not** edit `crates/` or the app. +- **Key-less throughout.** The host/connector never hold a key; reads/proposes only against `MockSigner`. + No real network broadcast. +- Mirror `deckard-mcp`'s no-secret-in-transcript discipline: no key/passphrase/token in any log or + message (a quick grep assertion is enough for the spike). + +## Success criteria (what "done" means) + +- A clear **YES/NO** on: *"Does native messaging carry a full EIP-1193 round-trip cleanly on both + Chromium and Firefox, with reliable reconnection under MV3 eviction?"* — with the evidence. +- A clear **wire recommendation** (native messaging vs localhost) with the security argument (task 5). +- A clear **self-distribution recommendation** with the exact Firefox + Chromium paths and their + friction (task 6). +- A runnable spike: connector + host + mock daemon completing `eth_requestAccounts` + `personal_sign`. +- A short report feeding PRD-04's "owned wire" + "distribution" sections. + +## Report format (return this) + +``` + + NATIVE MESSAGING / LOCALHOST REQUIRED / BLOCKED + + + + + + + + + +``` From 87c69506f3abc7223f985390e4b41c6a1980a706 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 10:58:12 +0000 Subject: [PATCH 4/4] docs: move PRD/spike work items to GitHub issues; keep ADR + research Per the planning-artifacts convention: work items/epics live as GitHub issues (epic #44 + sub-issues #45-50), research + decision records stay in docs/. - Remove docs/prd/* and spikes/deckard-bridge-spike.md (content now in issues). - ADR 0001 + research: repoint PRD/spike links to the issue tracker. - CLAUDE.md: record the convention (work items -> issues, only open on confirmation; research/ADRs/design -> docs/). https://claude.ai/code/session_01Vp5m1a3P8XePLbyppqzcjN --- CLAUDE.md | 5 + .../0001-dapp-connectivity-architecture.md | 17 +- docs/prd/01-resolver-authentication.md | 126 ------------- .../02-clear-signing-and-message-intents.md | 134 -------------- docs/prd/03-curated-native-integrations.md | 99 ----------- docs/prd/04-deckard-native-bridge.md | 168 ------------------ .../05-per-origin-permissions-and-registry.md | 113 ------------ docs/prd/README.md | 47 ----- docs/prd/x-walletconnect-shelved.md | 153 ---------------- docs/research/10-dapp-connectivity.md | 2 +- spikes/deckard-bridge-spike.md | 109 ------------ 11 files changed, 16 insertions(+), 957 deletions(-) delete mode 100644 docs/prd/01-resolver-authentication.md delete mode 100644 docs/prd/02-clear-signing-and-message-intents.md delete mode 100644 docs/prd/03-curated-native-integrations.md delete mode 100644 docs/prd/04-deckard-native-bridge.md delete mode 100644 docs/prd/05-per-origin-permissions-and-registry.md delete mode 100644 docs/prd/README.md delete mode 100644 docs/prd/x-walletconnect-shelved.md delete mode 100644 spikes/deckard-bridge-spike.md diff --git a/CLAUDE.md b/CLAUDE.md index 259ff7a..3b76ed7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,10 @@ # Deckard — agent notes +## Planning artifacts +Work items and epics (PRDs, tasks, spikes) live as **GitHub issues**, not files in the repo — use an +epic issue with linked sub-issues. Research, ADRs/decision records, and design specs live in `docs/`. +**Only open (create) issues after the user confirms.** Draft in chat first; create on the go-ahead. + ## Design System Always read `DESIGN.md` before making any visual or UI decision. Fonts, colors, spacing, the two-signal actor model (amber = human, cyan = agent), the sidebar/contextual-views IA, diff --git a/docs/adr/0001-dapp-connectivity-architecture.md b/docs/adr/0001-dapp-connectivity-architecture.md index b0c9ad9..14eb40d 100644 --- a/docs/adr/0001-dapp-connectivity-architecture.md +++ b/docs/adr/0001-dapp-connectivity-architecture.md @@ -4,7 +4,10 @@ - **Deciders:** @hellno (maintainer) - **Context inputs:** [`docs/research/10-dapp-connectivity.md`](../research/10-dapp-connectivity.md) (cited evidence), `THREAT-MODEL.md`, `SECURITY.md`, `DESIGN.md`, `docs/build/30-mcp-shape.md`. -- **Supersedes / relates:** establishes the umbrella decision the `docs/prd/*` series executes. +- **Supersedes / relates:** establishes the umbrella decision; the executable work items live as + GitHub issues under **epic [#44](https://github.com/hellno/deckard/issues/44)** — PRD-01 → #45, + PRD-02 → #46, PRD-03 → #47, PRD-05 → #48, spike → #49, PRD-04 → #50. (Convention: work items/epics + live in issues; decisions + research live in `docs/`.) ## Decision drivers (agreed with the maintainer) @@ -84,10 +87,10 @@ clear-signing pattern. No new trust boundary is invented; we extend the one we h mirroring `deckard-mcp`. No third-party relay, no embedded engine, no store-as-trust-anchor. The UX is native cards, local and instant — the explicit antidote to WalletConnect's relay-latency/QR-dance UX. → **PRD-04** (the bridge) + **PRD-05** (per-origin permissions, scope & registry). - - **WalletConnect v2 is rejected and shelved** (see Alternatives; rationale preserved in - [`docs/prd/x-walletconnect-shelved.md`](../prd/x-walletconnect-shelved.md)). It would import a - centralized relay + Project-ID dependency (privacy/offline-first tension) and a UX the maintainer - explicitly does not want. + - **WalletConnect v2 is rejected and shelved** (see Alternatives; rationale recorded in epic + [#44](https://github.com/hellno/deckard/issues/44)). It would import a centralized relay + + Project-ID dependency (privacy/offline-first tension) and a UX the maintainer explicitly does not + want. - The embedded webview stays rejected. - **Honest cost:** a browser-side connector is desktop-only. Rejecting WalletConnect (the mobile-capable transport) leaves *mobile* universal-reach unsolved; this is consistent with driver @@ -136,8 +139,8 @@ clear-signing pattern. No new trust boundary is invented; we extend the one we h ## Alternatives considered (and rejected) - **Embedded webview (Option A as clarified):** rejected — see Decision §1. -- **WalletConnect v2 as the generic transport:** **rejected and shelved** (full rationale: - [`docs/prd/x-walletconnect-shelved.md`](../prd/x-walletconnect-shelved.md)). Summary: a centralized +- **WalletConnect v2 as the generic transport:** **rejected and shelved** (full rationale recorded in + epic [#44](https://github.com/hellno/deckard/issues/44)). Summary: a centralized Reown relay + Project-ID dependency that leaks IP/metadata (against offline-first/privacy, `research §10–11`), a QR/relay UX the maintainer explicitly rejects, and no maintained Rust wallet-side SDK (`research §14`). Its one advantage — mobile reach — does not outweigh owning the diff --git a/docs/prd/01-resolver-authentication.md b/docs/prd/01-resolver-authentication.md deleted file mode 100644 index c5ed505..0000000 --- a/docs/prd/01-resolver-authentication.md +++ /dev/null @@ -1,126 +0,0 @@ -# PRD-01 — Resolver authentication (capability-gated `Resolve`) - -> Phase 1a of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). Closes `THREAT-MODEL.md` -> residual-risk #1. **Foundational and independent** — has standalone security value even if no dapp -> connectivity ever ships, and is a hard prerequisite for PRD-04. - -## Why this exists - -Today `crates/deckard-signerd/src/auth.rs` authorizes a connection on `same_uid()` alone. Because -`Resolve{request_id, approved:true}` is just another frame on the same socket, **any same-uid process -can approve a pending request** — including, in future, a browser-reachable proposer. The research -(`docs/research/10-dapp-connectivity.md §22–28`) confirms this is the classic confused-deputy problem: -uid is *ambient authority*; peer-cred proves *who* connected but not *which role*. Every hardware/ -companion signer (Trezor, Ledger, GridPlus, Frame) separates *request* (promiscuous endpoint) from -*approval* (a surface the requester cannot drive). Deckard must manufacture the same split in software. - -Right now the risk is *accepted* because the proposer set is small and human-controlled. PRD-04 breaks -that assumption. This PRD must land first. - -## Goals - -- The GPUI app is the **sole** principal that can send `Resolve` / `RevokeAll`-as-approval-grant. -- Approval authority is an **unforgeable capability** the daemon hands only to the app process it - controls — not derivable by any other same-uid process. -- The public proposer socket continues to accept *proposals and reads* from same-uid callers - (unchanged), but **rejects `Resolve`** with a typed error. -- STOP/`RevokeAll` (the brake) remains reachable from *any* transport (it can only ever reduce - authority, never grant it) — see Non-goals. - -## Non-goals - -- Multi-user / multi-principal daemon (still single-uid). Salted request-ids (residual-risk #6) are a - separate future item. -- Changing the keystore, mainnet guardrail, or STOP-latency behavior. -- Authenticating the *dapp origin* (that's PRD-05). This PRD authenticates the **resolver**, not the - requester. - -## Design - -### The capability channel - -`crates/deckard-signerd/src/supervise.rs` already has the app **spawn + supervise the daemon child**. -That direction matters: the *app* is the parent. Two viable capability mechanisms (pick per -portability, decide in implementation and record in the PRD's decision note): - -1. **`socketpair()` inherited by the daemon child (recommended).** Before `Command::spawn`, the app - creates an `AF_UNIX` `SOCK_SEQPACKET`/`SOCK_STREAM` pair; it passes one end to the child as an - inherited fd (e.g. via `CommandExt::pre_exec` / explicit fd inheritance) and keeps the other. This - **control channel** is the only place the daemon accepts `Resolve`. No other process can obtain the - app's end (`research §26`, kernel inheritance). Portable across Linux + macOS. -2. **`SCM_RIGHTS` fd-pass after connect.** App connects to the public socket, the daemon passes back a - dedicated control fd via ancillary data; thereafter `Resolve` is accepted only on that fd. Slightly - more wire complexity; also portable (`research §25, 27`). - -**Fallback (weaker, only if neither fd path is feasible):** a per-launch random cookie written `0600` -by the daemon, read by the app, presented on `Resolve`. Weaker because any process that can read the -user's files can steal it — prefer an fd capability. Document explicitly if used. - -### Daemon changes (`crates/deckard-signerd`) - -- `auth.rs`: keep `same_uid()` for the public socket (defense-in-depth + logging). Add the concept of a - **control channel** that is authenticated by construction (the inherited/passed fd), not by uid. -- `server.rs` / `daemon.rs`: route `SignerRequest::Resolve` **only** from the control channel. A - `Resolve` arriving on the public socket returns a typed denial (new `deny_reasons` entry, e.g. - `RESOLVE_NOT_AUTHORIZED`) — URL-redacted, no payload echo, per existing transcript-hygiene rules. -- Keep `RevokeAll`/STOP reachable on every channel (it only zeroizes; it cannot grant). Add a test - asserting STOP from the public socket still works while `Resolve` from it is refused. -- `peer_uid` staleness caveat (`research §24`/`peercred` timing): capture creds at accept; do not - re-trust across the connection lifetime. - -### App changes (`crates/deckard-app`) - -- `supervise.rs` + `shell.rs`: establish the control channel at spawn; thread its handle to the place - that today sends `Resolve` after a completed hold-to-confirm (`shell.rs:142` neighborhood). The - hold-to-confirm contract is unchanged — only the *channel* the `Resolve` rides changes. - -### Contract changes (`crates/deckard-contract`) - -- No change to `SignerRequest::Resolve`'s shape is required if the channel (not the frame) carries the - authority. If a token/cookie fallback is used, add it as a typed field and ensure its `Debug` is - redacted. Prefer the no-wire-change fd approach. - -## Cross-platform notes - -- Linux: `SO_PEERCRED`/`socketpair`/`SCM_RIGHTS` all available. -- macOS: no `SO_PEERCRED` (use `LOCAL_PEERCRED`; pid via `LOCAL_PEERPID`); `socketpair`/`SCM_RIGHTS` - available — which is why the fd-capability approach is the portable primitive (`research §27`). -- `unsafe` for raw fd handling: confine to the **app crate** (`unsafe_code = "deny"` → add a scoped, - documented `#[allow(unsafe_code)]` with a `// reason:` comment, matching the existing `eth.rs` - pattern). `deckard-core` stays `#![forbid(unsafe_code)]` — keep fd plumbing out of it. Prefer a - vetted existing dependency already in the tree (`nix`, `tokio`) over hand-rolled `libc`; **no new - dependency without approval** (DoD #4) — `nix` and `tokio` are already present. - -## Acceptance tests (add to `crates/deckard-signerd/tests/`) - -- `resolve_rejected_on_public_socket`: a same-uid client on the public socket gets the typed denial for - `Resolve`; the pending record stays `Pending`. -- `resolve_accepted_on_control_channel`: the same `Resolve` over the control capability flips - `Pending → Allowed`. -- `stop_still_works_on_public_socket`: `RevokeAll` from the public socket zeroizes + denies in-flight - (the brake is never gated). -- `second_proposer_cannot_self_approve`: end-to-end — proposer A proposes (over cap → `NeedsApproval`); - proposer A (public socket) cannot `Resolve`; only the control channel can. This is the red-team - assertion that maps to residual-risk #1. -- Pure-fn parity unaffected (mock vs daemon `evaluate` unchanged). - -## Definition of Done - -PRD-series DoD (see `README.md`) **plus**: -- `THREAT-MODEL.md` residual-risk table updated: #1 moves from "Accepted v1 boundary" to "Mitigated" - with the mechanism named; the "Deferred" notes in §2 (daemon socket) updated accordingly. -- A short decision note in this PRD recording which capability mechanism was chosen and why. -- The four acceptance tests above run by **default** `cargo test` (not `#[ignore]`). - -## Risks & fallbacks - -- **fd inheritance across `Command::spawn` is fiddly / platform-specific.** Fallback: `SCM_RIGHTS` - post-connect; last resort: `0600` cookie (documented as weaker). -- **App-restart / daemon-survives races** (the supervise loop respawns the daemon): define the - re-handshake so a restarted app re-establishes the control channel without a window where `Resolve` - is ungated. Test the respawn path. - -## Sources - -`docs/research/10-dapp-connectivity.md §22–29`; `THREAT-MODEL.md` (boundary section, residual-risk #1); -man7 unix(7); blog.cloudflare.com/know-your-scm_rights; capability-myths-demolished. diff --git a/docs/prd/02-clear-signing-and-message-intents.md b/docs/prd/02-clear-signing-and-message-intents.md deleted file mode 100644 index c58bd04..0000000 --- a/docs/prd/02-clear-signing-and-message-intents.md +++ /dev/null @@ -1,134 +0,0 @@ -# PRD-02 — Clear-signing v2 + message-signing intents - -> Phase 1b of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). 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 this exists - -A dapp's most dangerous request is not a transaction — it is an **off-chain signature that authorizes -value movement with no on-chain tx the wallet ever simulates**. In 2024, EIP-2612 `permit` signatures -(56.7%) and `setOwner` (31.9%) were ~88.6% of wallet-drainer losses (`research §30`). Deckard today can -sign one structured off-chain payload (`SwapOrder` via `SignOrder`, EIP-712) but has **no general -message-signing path and no decode/clear-signing for arbitrary typed data**. Before any dapp can ask -Deckard to sign, the clear-signing card (`DESIGN.md` "the shared trust engine") must render *what is -actually being authorized*, in plain language, danger-early — never a raw hash. - -## Goals - -- Add typed message-signing to the contract: `personal_sign` (EIP-191) and `eth_signTypedData_v4` - (EIP-712), routed through `propose → Decision → human approval`, **never auto-allowed**. -- **Decode and clear-sign** before the human: EIP-712 domain binding (chainId + verifyingContract), - recognition of high-risk shapes (`permit`, Permit2, Seaport orders, `setOwner`/ownership transfer), - and a distinct, alarming screen for EIP-7702 delegation. -- **Consume ERC-7730 descriptors** where available to render human-readable intent; **fall back to an - explicit "blind signing — we can't decode this" warning** for the uncovered long tail. -- Hard-handle the chainId-mismatch and unknown-`verifyingContract` cases (warn loudly; the research - shows most wallets silently don't — `research §33`). - -## Non-goals - -- The transport that *delivers* these requests (PRD-04). This PRD makes the daemon able to *safely - sign a message it is handed*; where the message comes from is out of scope. -- Building the ERC-7730 registry. We *consume* descriptors; curation/registry shipping is PRD-05. -- Supporting `eth_sign` (raw 32-byte blind hash). **Refuse it outright** — MetaMask is removing it - (`research §31`); Deckard never offers it. - -## Design - -### Contract (`crates/deckard-contract`) - -Add message-signing as first-class, mirroring the existing `SwapOrder`/`SignOrder`/`PendingPayloadView` -pattern (do not overload `Intent`, which is a *transaction* shape): - -- New request variants on `SignerRequest` (rpc.rs), e.g. `ProposeMessage { message: SignMessage }` → - `Decision`, and `SignMessage { request_id }` → a `SignMessageResult { signature: Bytes }` (reuse the - `SignOrderResult` shape). Message signing **never broadcasts** — distinct from `Execute`. -- A `SignMessage` enum: - - `PersonalSign { bytes: Bytes }` (EIP-191 personal_sign; display decoded UTF-8 when valid, else hex - + a "binary message" caution). - - `TypedDataV4 { typed_data: ... }` (EIP-712). Parse the domain (`name, version, chainId, - verifyingContract, salt`) and the primary type. -- A new `PendingPayloadView::Message(SignMessageView)` so the GUI inbox (existing - `PendingList`/`PendingRecord`) renders message requests alongside tx/order/approve. -- New `deny_reasons` entries: `ETH_SIGN_REFUSED`, `CHAINID_MISMATCH`, `DELEGATION_REFUSED`, - `UNDECODABLE_TYPED_DATA`. - -Parse all untrusted typed-data bytes through the bounded `Reader` in `keystore.rs` style (DoD #5): -strict length/depth caps so a hostile EIP-712 blob can't OOM/recurse the daemon before validation. - -### Decision logic (`crates/deckard-contract/src/policy.rs`) - -Add a pure `evaluate_message(&SignMessage, &Policy, wallet, now) -> Decision` next to `evaluate` / -`evaluate_order` (keep the "one decision function, mock⇄daemon parity" charter): - -- Messages are **always `NeedsApproval`** in v1 (like swap orders) — no auto-allow path. -- EIP-712: deny on `CHAINID_MISMATCH` (domain.chainId ≠ daemon chain). Surface unknown - `verifyingContract` as a card-level danger flag (not necessarily a deny — the human decides), per - EIP-712 SHOULD/MAY (`research §33`). -- Recognize and **flag danger-early** (red, top of card, per `DESIGN.md`): unlimited/large `permit` & - Permit2 allowances, Permit2 batch, Seaport orders transferring assets for ~zero, `setOwner`/owner - transfer. These mirror `PendingPayloadView::Approve`'s existing shaped-approve treatment. - -### EIP-7702 delegation (`research §34`) - -EIP-7702's own Security Considerations say wallets MUST NOT offer a generic "sign arbitrary -delegation" interface ("there is no safe way"). Decision (record in PRD): **refuse delegation -authorizations by default** (`DELEGATION_REFUSED`); if ever supported, gate behind a curated -trusted-delegator allowlist (a PRD-05 concern) and a *distinct* "you are handing control of your -account to `
`" screen — never the normal sign card. v1: refuse. - -### Clear-signing card (`crates/deckard-app`, the shared review component) - -Extend the existing clear-signing card (`DESIGN.md` "Clear-signing review card") to render -`SignMessageView`: -- Plain-language **headline** of what's being authorized; one canonical key/value list grouped by - whitespace; danger in red at the top; **deliberate hold** to confirm (never a tap). -- ERC-7730: if a descriptor exists for the `verifyingContract`/call, render its human-readable fields. - If not, show an explicit **"Blind signing — Deckard can't decode this request. Only continue if you - fully trust the source."** caution (amber icon + risk word, per `DESIGN.md`), demoted-but-present. -- Origin shown as **unverified** (PRD-05 may upgrade it). Per `research §29`, never let a claimed name - substitute for the decoded effects. - -### `wallet_addEthereumChain` guard (`research §36`) - -Even pre-transport, codify the rule: a chain add/switch must sign only with the **user-submitted** -chainId, never one returned by a (possibly malicious) RPC; require an explicit confirm naming requester -+ target chain. Land the daemon-side invariant here; the UI lands with PRD-04. - -## Acceptance tests - -- `eth_sign_refused`: a raw-hash sign request is denied with `ETH_SIGN_REFUSED`, nothing signed. -- `typed_data_chainid_mismatch_denies`: EIP-712 with domain.chainId ≠ daemon chain → `CHAINID_MISMATCH`. -- `permit_flagged_danger`: a `permit`/Permit2 typed-data produces a `Decision::NeedsApproval` whose - pending view carries the danger flag (assert the flag, not just the approval). -- `delegation_refused`: an EIP-7702 authorization → `DELEGATION_REFUSED`. -- `message_never_broadcasts`: signing a message returns a signature, never a `tx_hash`; `Execute` on a - message request is rejected. -- `undecodable_typed_data_bounded`: an oversized/deeply-nested EIP-712 blob is rejected by the bounded - reader without OOM/panic (fuzz-style vector). -- Parity: `evaluate_message` gives identical verdicts in `MockSigner` and the daemon. -- Transcript hygiene: no message bytes/signature leak into any reason string (extend the existing - allowlist scan). - -## Definition of Done - -PRD-series DoD **plus**: new ⌘K commands for any user-initiated signing actions; `DESIGN.md` card spec -honored (verify visually per `just check` build + a screenshot in the PR); the EIP-7702 refusal and the -blind-sign fallback are documented in `THREAT-MODEL.md` (new "message signing" surface section). - -## Risks & fallbacks - -- **EIP-712 parsing is a parser-security surface.** Mitigate with the bounded reader + fuzz vectors; - keep the parser in `deckard-core` under its strict lints. -- **ERC-7730 coverage gap** (`research §35`): most contracts have no descriptor. The blind-sign - fallback is therefore the *common* path, not the exception — design it to be honestly alarming, not - normalized-away. -- **Scope creep into a full ABI decoder.** v1: decode the *high-risk shapes* (permit/Permit2/Seaport/ - owner-transfer) + ERC-7730 descriptors; everything else is explicit blind-sign. Don't build a - general decoder. - -## Sources - -`docs/research/10-dapp-connectivity.md §30–36`; eips.ethereum.org eip-712, eip-7702, erc-7730, -eip-3085/3326; MetaMask MIP-3; scamsniffer 2024 drainer report; existing `swap_order.rs`/`SignOrder`. diff --git a/docs/prd/03-curated-native-integrations.md b/docs/prd/03-curated-native-integrations.md deleted file mode 100644 index 12b1074..0000000 --- a/docs/prd/03-curated-native-integrations.md +++ /dev/null @@ -1,99 +0,0 @@ -# PRD-03 — Curated native dapp integrations (Phase 0 connectivity) - -> Phase 0 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The "curated/allowlisted dapps -> first" requirement, realized as **native Deckard surfaces** — no browser, no extension, no relay, no -> external proposer. This is the first user-visible value and the most secure interpretation of -> "connect to dapps." - -## Why this exists - -The maintainer chose **curated/allowlisted dapps first** (ADR driver #2). Taken to its logical, most -secure conclusion, the curated set doesn't need a generic *dapp-connection transport at all*: each -curated integration is a **native screen that builds a typed `Intent`/`SwapOrder` in Rust** and routes -it through the existing daemon → policy → clear-signing path — exactly how **Shield** (Railgun) and the -**CoW swap** path already work (`swap_order.rs`, `SignOrder`). This adds zero new attack surface, stays -fully on-brand ("calm sovereign control, not a casino"), works offline-first, and ships *before* the -Phase-2 connectivity machinery (PRD-04/05). - -## Goals - -- A small, vetted set of integrations as **native surfaces**, each producing a typed proposal the - daemon already (or via PRD-02) understands. Initial set: **token swap** (extend existing) and **one - bridge**. Each is a wallet action, reachable from ⌘K, with clear-signing on confirm. -- A **curated integration registry** in-repo (compile-time or signed config) listing the allowed - protocols, their contract addresses per chain, and the `IntentKind`/order they build — the - "allowlist" is the set of code paths we shipped, not a URL allowlist. -- Reuse `Policy.allow_swap_tokens` / `allow_to` fences; no new bypass. - -## Non-goals - -- Generic / open dapp access (PRD-04, the Deckard-native bridge). No `window.ethereum`, no external - transport here — these are native Rust code paths only. -- Per-origin permissions (PRD-05) — there is no untrusted origin; the integration *is* Deckard code. -- New signing primitives beyond what PRD-02 adds (a swap needs Permit2/EIP-712 → that's PRD-02's job; - this PRD consumes it). - -## Design - -### Integration shape - -Each curated integration is a module under the app (or a small `deckard-integrations` area if it grows) -that: -1. Fetches the quote/route from the protocol's API over the existing verified-read/RPC path (reads - only; never a signing path). -2. Builds a typed `Intent` (`Send`/`ContractCall`/`Shield`) or `SwapOrder` against **registry-pinned - contract addresses** for the active `chain_id`. -3. Calls `Propose`/`ProposeOrder` → renders the clear-signing card → on hold-confirm, - `Execute`/`SignOrder`. - -The daemon's `calldata_ok` shape check and `evaluate`/`evaluate_order` fences apply unchanged — a -native integration is just another well-behaved proposer using the *typed* builders (never raw -arbitrary calldata, per the `deckard-mcp` rule). - -### Curated registry - -- A versioned, in-repo list: `{ protocol, chains: { chain_id: { contracts… } }, kind }`. Compile-time - `const`/`include_str!` of a JSON checked into the repo (offline-first; no network fetch to learn what - is allowed). If it must update without a release, ship it as a **signed** config (verify a maintainer - signature before trusting — never an unsigned downloaded allowlist). -- Addresses are pinned and reviewed; a swap/bridge to an off-registry contract is impossible from this - surface (it would require a code change + review). - -### UI (`DESIGN.md`) - -- Swap already exists; extend per the **Amount input** + **Clear-signing review card** specs. -- Bridge: a new contextual action on the wallet (Send/Receive/Swap siblings), same review-card confirm, - same status-glyph activity rows. Bridges cross chains — the card must show source chain, destination - chain, destination address, and asset, danger-early on any mismatch. - -## Acceptance tests - -- `swap_builds_pinned_addresses`: the swap path only ever targets registry-pinned contracts for the - active chain; an attempt to target another address is rejected before propose. -- `bridge_proposal_clear_signs`: a bridge proposal renders source/dest chain + dest address on the card - and routes through the normal approval path. -- `off_registry_denied`: a constructed proposal to a non-registry contract via this surface is refused. -- Existing swap parity tests (`crates/deckard-signerd/tests/swap_parity.rs`) stay green. -- Registry signature check (if signed-config path chosen): a tampered/unsigned registry is rejected - loudly (mirror the `policy_store.rs` "loud fallback" discipline). - -## Definition of Done - -PRD-series DoD **plus**: each integration is a ⌘K `Command`; the curated registry and its trust model -(compile-time vs signed) are documented; the bridge card matches `DESIGN.md`; a screenshot in the PR. - -## Risks & fallbacks - -- **Protocol API as an input channel.** Quotes/routes from a protocol API are untrusted input — the - daemon still re-derives/validates the typed proposal against pinned addresses and the policy fence; - never sign what the API returns verbatim. -- **Bridge complexity** (two chains, longer settlement). v1: pick one well-understood bridge; show - honest pending/failed states (`DESIGN.md` required states). Don't ship a bridge whose effects we - can't clear-sign. -- **Scope creep toward "just open any dapp".** That is explicitly PRD-04 and gated on an audit; keep - this surface curated-code-only. - -## Sources - -`docs/adr/0001-dapp-connectivity-architecture.md` (Phase 0); existing `swap_order.rs`, `SignOrder`, -`swap_parity.rs`, Railgun Shield path (`docs/build/10-kohaku-shield.md`); `DESIGN.md`. diff --git a/docs/prd/04-deckard-native-bridge.md b/docs/prd/04-deckard-native-bridge.md deleted file mode 100644 index 9e1e2d1..0000000 --- a/docs/prd/04-deckard-native-bridge.md +++ /dev/null @@ -1,168 +0,0 @@ -# PRD-04 — Deckard-native bridge (universal dapp reach, owned end-to-end) - -> Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). Delivers "connect to **any** -> dapp" through a transport Deckard **fully owns** — our wire, our UX, local-first, **no third-party -> relay, no embedded browser, no store as a trust anchor**. This is the deliberate replacement for -> WalletConnect ([shelved](./x-walletconnect-shelved.md)). **Gated on PRD-01 (resolver auth), -> PRD-02 (clear-signing v2), PRD-05 (per-origin permissions), and an external audit** (`SECURITY.md`). - -## Why this exists - -The maintainer wants **universal reach** (users will want arbitrary dapps) **without** the "external -bad stuff": no bundled browser engine (rejected, ADR §1) and no WalletConnect (a centralized relay that -leaks metadata, a Project-ID dependency, and a QR/relay UX the maintainer rejects — `research §10–14`). - -The resolution: dapps already speak **EIP-1193**. So we reach all of them by injecting a *standard* -EIP-1193 + EIP-6963 provider (`research §7, 21–22`) through a **first-party connector** in the user's -own browser, and carry requests over a **wire Deckard defines** to a key-less proposer that speaks the -existing `deckard-contract` wire to `deckard-signerd`. Same proven pattern as `deckard-mcp` -(`docs/build/30-mcp-shape.md`) — a thin, key-less translator — but for dapps, and entirely local. - -The payoffs are exactly the vision: **no relay → no third-party metadata leak** (a real privacy win -over WalletConnect, on-brand offline-first); **native, instant approval cards → the UX we control** -(no QR dance, no relay latency); universal reach without becoming a browser. - -## Goals - -- A **key-less proposer process** (working name `deckard-bridged`) mirroring `deckard-mcp`: speaks the - owned wire to the connector and the `deckard-contract` wire to the daemon. Holds **no key**, **cannot - `resolve`** (PRD-01 enforces this even if it tried), submits only **typed** intents/messages via the - PRD-02 builders — never raw arbitrary calldata that skips a typed path. -- A **first-party browser connector** that injects EIP-1193 + EIP-6963 (`rdns: sh.deckard` or similar), - giving universal reach (every dapp works) without per-dapp integration. **Target Chromium *and* - Firefox from the start** (separate manifests; MV3 service-worker quirks on the Chromium side). -- **Distribution: self-distributed + signed first.** Ship a Deckard-signed connector we host ourselves - (Firefox supports self-distribution of signed extensions; sideload/dev paths on Chromium) so trust is - **our signature + the key-less design, not a store review**. Store listings, if used later, are - convenience only — never the trust anchor. -- A **Deckard-owned local wire** between connector and `deckard-bridged`. **The exact wire is a spike - decision** (PRD-04 spike, [`spikes/deckard-bridge-spike.md`](../../spikes/deckard-bridge-spike.md)): - the leading candidate is **native messaging** (stdio host, gated by `allowed_origins`/ - `allowed_extensions`, **not web-reachable**), preferred on security over a localhost RPC port — a - localhost port is reachable by any page via DNS-rebinding / cross-site WebSocket hijacking - (`research §2–4`, the Frame weakness). The spike proves both end-to-end on both browsers and records - the choice before the full build. -- Map dapp methods → Deckard surfaces: `eth_sendTransaction` → `Intent`; `personal_sign` / - `eth_signTypedData_v4` → `SignMessage` (PRD-02); `wallet_switchEthereumChain`/`addEthereumChain` → - guarded per PRD-02. **Refuse** `eth_sign` and (v1) EIP-7702 delegation. -- Per-origin scope enforced via PRD-05 (accounts × chains × methods). - -## Non-goals - -- WalletConnect (shelved) and embedded webview (rejected). -- **Mobile.** A browser-side connector is desktop-only. Mobile universal-reach is an explicit open - problem (ADR Consequences), NOT solved here. Keep `deckard-bridged`'s core protocol logic - platform-agnostic so a future mobile mechanism can reuse it, but do not build for mobile now - (driver #1: don't compromise desktop for it). -- Generic ABI decoding (PRD-02 scope: high-risk shapes + ERC-7730 + blind-sign fallback). - -## Design - -### Process topology (mirror `deckard-mcp`) - -``` -dapp ──EIP-1193──► Deckard connector ──owned wire (native messaging)──► deckard-bridged ──contract wire──► deckard-signerd - (any site) (first-party, (key-less, (holds key, - injects std provider) no resolve) policy gate) - │ - native clear-signing card ◄── GPUI app - (control channel, PRD-01) -``` - -`deckard-bridged` is to dapps what `deckard-mcp` is to LLM agents. Reuse the daemon socket, the -`Propose`/`Execute`/`Status` poll loop, and the native approval card unchanged. - -### The owned wire (settled by the PRD-04 spike) - -The spike ([`spikes/deckard-bridge-spike.md`](../../spikes/deckard-bridge-spike.md)) proves both -candidates end-to-end on Chromium **and** Firefox, then records the choice with rationale: - -- **Leading candidate: native messaging** (Chrome `connectNative` / Firefox `runtime.connectNative`). - The host is an OS-installed stdio binary gated by a manifest `allowed_origins`/`allowed_extensions` - (exact id, no wildcards) and runs as the user (`research §3, 12`). It is **not reachable from web - pages** — the decisive security advantage over a localhost port. Caveats the spike must exercise: MV3 - service-worker lifecycle (keep the port alive, reconnect on disconnect, `research §5`); host-manifest - registration lives in user-writable paths (`research §4`) — we install it ourselves. -- **Alternative measured by the spike: hardened localhost RPC** (Frame-style `ws://127.0.0.1`). - Web-reachable, so it would need origin-allowlist + token + Host-header validation just to approach - native-messaging's isolation (`research §2`). The spike documents whether any target browser/setup - forces it; default is native messaging unless the spike finds a blocker. - -### The first-party connector (trust is bounded, not store-based) - -- Minimal, open-source, **key-less**: it injects a standard provider and relays JSON-RPC to the host. - It holds no key, has no signing power, and **cannot reach the daemon's `Resolve`** (PRD-01). -- **Self-distributed + signed first, Chromium + Firefox.** We host a Deckard-signed connector - ourselves; trust is our signature + the key-less design, not a store review. Firefox supports - self-distribution of signed XPIs directly; Chromium normal installs lean on the Web Store, so the - spike records the Chromium self-/sideload-distribution path and its friction. Any store listing is - convenience, **never** the trust anchor. -- **"No store as a trust anchor" holds because trust is bounded by design.** The connector is a - recurring attack target (`research §6`: Great Suspender, Cyberhaven), but a fully compromised - connector can only **propose** — it cannot sign, self-approve, or exfiltrate a key, and every effect - is clear-signed (PRD-02) against an attacker-controllable origin (`research §29`). -- Inject via **EIP-6963** (announce with a stable `rdns`), with `window.ethereum` as legacy fallback, - so Deckard coexists with other wallets instead of fighting over the global (`research §7, 21–22`). - -### UX (the explicit anti-WalletConnect) - -- Connecting is a native flow: the dapp requests, Deckard raises a **native connect card** showing the - (unverified) origin + requested scope (PRD-05); approval is local and instant — no QR, no relay. -- Every signing request renders the PRD-02 clear-signing card. Origin shown as unverified unless - corroborated (PRD-05). Reuse `DESIGN.md` card anatomy, danger-early, hold-to-confirm. -- Sessions are listable and revocable from ⌘K + settings governance (next to "Pause all agents"). - -## Acceptance tests - -- **Spike first** ([`spikes/deckard-bridge-spike.md`](../../spikes/deckard-bridge-spike.md)): prove a - real dapp → connector → host → `deckard-bridged` → daemon round-trip for `eth_requestAccounts` + one - `personal_sign`, with the native card rendering, on **both Chromium and Firefox**, and settle the wire - (native messaging vs localhost) + the self-distribution path. Commit the spike report before the full - build. -- `bridged_is_keyless`: memory/fd scan of `deckard-bridged` finds no key (reuse the `deckard-mcp` - red-team harness, `docs/build/00-test-harness.md`). -- `bridged_cannot_resolve`: `deckard-bridged` attempting `Resolve` is refused by PRD-01's control-channel - gate (cross-PRD integration test). -- `wire_not_web_reachable`: assert the chosen wire (native messaging) exposes no listening TCP/WS port a - web page could reach; if a localhost fallback is ever used, a cross-origin page request without the - token/Host check is rejected. -- `method_out_of_scope_refused`: a dapp method/chain/account outside the PRD-05 per-origin grant is - refused; nothing reaches the daemon. -- `eth_sign_refused` / `delegation_refused`: dapp requests for `eth_sign` and EIP-7702 authorizations are - refused (defense-in-depth with PRD-02). -- `eip6963_announced`: the connector announces via EIP-6963 with the stable `rdns` and coexists with a - second injected provider. -- Transcript hygiene extended to `deckard-bridged` (no secret/RPC-token leakage). - -## Definition of Done - -PRD-series DoD (see [`README.md`](./README.md)) **plus**: -- ⌘K commands: connect, disconnect, list sessions, revoke session, STOP-all-sessions. -- The owned-wire choice (native messaging vs localhost) and the connector distribution/trust model are - documented; the "store is not a trust anchor; trust is bounded by key-less + clear-signing" argument - is written into `SECURITY.md` + `THREAT-MODEL.md` as a new "dapp bridge" surface. -- The desktop-only / mobile-open-question is recorded honestly. -- Off by default — the bridge is an opt-in network/browser feature in an offline-first wallet; it never - installs a host or opens a connection silently. -- New dependencies (if any) explicitly approved and reviewed against `deny.toml`/`Cargo.lock`. -- **External audit sign-off recorded** before shipping beyond testnet (`SECURITY.md`). - -## Risks & fallbacks - -- **First-party connector is a standing supply-chain target** (`research §6`). Contained by design - (key-less, no-resolve, clear-signed, bounded blast radius); keep it minimal and open-source; consider - reproducible builds + signature pinning between connector and host. -- **Native-messaging host registration is user-writable** (`research §4`) — any same-uid code can - overwrite the manifest. This is the same uid boundary the whole model already assumes; document it and - lean on PRD-01 (the host is key-less and can't self-approve regardless). -- **MV3 lifecycle flakiness** (`research §5`) — reconnect logic + heartbeat; covered by the spike. -- **We're inventing a wire.** Keep it small and typed; reuse `deckard-contract` shapes wherever possible - so the connector↔host protocol is a thin envelope, not a second contract. -- **Mobile remains unsolved** — acknowledged; revisit as a separate effort if/when mobile is funded. - -## Sources - -`docs/research/10-dapp-connectivity.md §1–7 (extension/bridge), §22–29 (boundary), §30–37 (signing & -scope)`; `docs/build/30-mcp-shape.md` (the proposer pattern to mirror); EIP-1193, EIP-6963; -developer.chrome.com native-messaging; github.blog localhost-dangers; the shelved WalletConnect -rationale ([`x-walletconnect-shelved.md`](./x-walletconnect-shelved.md)). diff --git a/docs/prd/05-per-origin-permissions-and-registry.md b/docs/prd/05-per-origin-permissions-and-registry.md deleted file mode 100644 index 3bbc614..0000000 --- a/docs/prd/05-per-origin-permissions-and-registry.md +++ /dev/null @@ -1,113 +0,0 @@ -# PRD-05 — Per-origin permissions, dapp registry & anti-phishing - -> Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The policy + trust layer that -> makes the Deckard-native bridge (PRD-04) safe: scope each connected origin (accounts × chains × -> methods), ship a curated registry + anti-phishing blocklist, and treat origin as -> attacker-controllable. Pairs with PRD-04; depends on PRD-02. - -## Why this exists - -Once a dapp can connect (PRD-04), the wallet must answer "what may *this* origin see and do?" — not -"what may any caller do?". Research (`research §37`) shows the standards: EIP-2255 -(`wallet_requestPermissions`/`getPermissions`, the `{invoker, parentCapability, caveats}` object) and -CAIP-25 session scoping (per-scope accounts × chains × methods). And `research §13, 29` shows origin is -**self-asserted and spoofable** — so scoping must be paired with attestation-as-a-hint + a shipped -blocklist, and the human must always approve on decoded effects, not a claimed name. - -## Goals - -- A **per-origin permission model**: a connected origin is granted a scoped session (which accounts, - which chains, which methods, optional caps) that the daemon enforces — distinct from the global - `Policy`. Persisted, revocable, and visible in the UI. -- A **curated dapp registry** (the "allowlist first" posture for generic connectivity): known-good - origins with metadata (name, verified domain, ERC-7730 descriptor references). Offline-first and - **signed** if updatable without a release. -- An **anti-phishing blocklist** (known-malicious domains), shipped and updatable, mirroring - `eth-phishing-detect` (`research §37`). -- Wire-level guards: `wallet_addEthereumChain`/`switchEthereumChain` per `research §36` (sign only with - user-submitted chainId; confirm requester + target chain). - -## Non-goals - -- The transport (PRD-04) and the signing decode (PRD-02). -- Building/curating ERC-7730 descriptors upstream — we *reference and consume* them (PRD-02 renders). -- Open access to arbitrary origins as a default — default posture is curated-allowlist; unknown origins - are allowed only with an explicit, friction-ful "unverified origin" confirmation (never silent). - -## Design - -### Per-origin policy (`crates/deckard-contract` + `deckard-signerd`) - -- Extend the policy layer with an **origin-scoped permission** type: `OriginGrant { origin, - accounts, chains, methods, caps?, expiry }`. Keep the global `Policy` as the outer fence — an origin - grant can only ever be *narrower* than the global policy (defense-in-depth; a permissive grant can't - widen the global caps/allowlist). -- The daemon evaluates an incoming dapp proposal against **both** the origin grant and the global - policy; the stricter wins. Add a pure `evaluate_origin(&proposal, &OriginGrant, &Policy)` next to the - existing decision functions (preserve the mock⇄daemon parity charter). -- Grants are created at connect-time (the human approves the requested scope, EIP-2255/CAIP-25 style) - and are **revocable** — surface revoke in the agent/governance settings alongside the existing - "Pause all agents" kill switch (`DESIGN.md`). - -### Origin attestation (treat as a hint, `research §29`) - -- The daemon receives an origin string from the (untrusted) connector/proposer. It is displayed as - **unverified** unless corroborated by (a) a curated-registry match and/or (b) any first-party - attestation the bridge can establish (PRD-04). There is no third-party relay attestation (we shelved - WalletConnect's Verify API along with WalletConnect) — so origin trust leans on the registry + - blocklist + the always-clear-signed effects, never a remote checkmark. -- The card shows attestation state (verified-domain / unverified / mismatch / known-scam) using - `DESIGN.md` caution affordances — but the **decoded effects** remain the ground truth the human holds - to confirm. Never gate solely on a green checkmark. - -### Curated registry & blocklist (offline-first, signed) - -- **Registry**: in-repo signed JSON of curated origins → metadata + ERC-7730 descriptor refs. Ships - with the build; if updatable out-of-band, verify a maintainer signature before trust (mirror the - `policy_store.rs` loud-fallback discipline — a tampered/unsigned list is refused loudly, never - silently widening trust). -- **Blocklist**: known-malicious domains; a match is a hard, loud block (red), not a soft warning. - Updatable on the same signed-config mechanism. Default-deny on a blocklist match even if the user - tries to proceed (this is the one place we override user intent — a known drainer domain). - -### ERC-7730 descriptor sourcing - -- The registry references which ERC-7730 descriptors apply to a curated origin's contracts so PRD-02's - card can render human-readable intent; uncovered contracts fall back to PRD-02's explicit blind-sign - warning. - -## Acceptance tests - -- `origin_grant_cannot_widen_global`: an origin grant requesting more than the global `Policy` allows is - clamped to the global fence (stricter wins); assert the effective decision. -- `out_of_scope_method_denied`: a method/chain/account outside the origin grant is denied. -- `blocklist_hard_blocks`: a blocklisted origin is refused even on explicit user "proceed". -- `unsigned_registry_refused`: a tampered/unsigned registry/blocklist is rejected loudly; the wallet - falls back to the safe built-in (curated-empty = nothing auto-trusted). -- `unverified_origin_requires_friction`: an unknown (not-curated, not-blocked) origin connect requires - the explicit unverified-origin confirmation path, not a silent allow. -- `addchain_uses_user_chainid`: a `wallet_addEthereumChain` flow signs only with the user-submitted - chainId; an RPC-returned chainId is never trusted. -- Parity: `evaluate_origin` identical in mock + daemon. - -## Definition of Done - -PRD-series DoD **plus**: revoke-origin and list-connected-origins are ⌘K commands and appear in -settings governance; registry/blocklist trust model documented in `SECURITY.md`; the -"origin-is-untrusted, effects-are-ground-truth" rule documented in `THREAT-MODEL.md`; UI matches -`DESIGN.md` (caution affordances, danger-early, no green-check-as-safety). - -## Risks & fallbacks - -- **Curated registry maintenance burden.** Start tiny (the same protocols PRD-03 integrates natively); - grow by reviewed PR. The registry being small is fine — it's the safe default. -- **Blocklist staleness** (`research §37` counts are directional). It's a backstop, not the primary - defense; the clear-signing card is. Update cadence documented; signed. -- **Origin spoofing** (`research §13`): contained by never substituting a name for decoded effects, by - attestation-as-hint, and by the blocklist. - -## Sources - -`docs/research/10-dapp-connectivity.md §13, 29, 36–37`; eips.ethereum.org eip-2255, eip-3085/3326; -chainagnostic.org CAIP-25; github.com/MetaMask/eth-phishing-detect; existing `policy.rs`, -`policy_store.rs` (loud-fallback discipline). diff --git a/docs/prd/README.md b/docs/prd/README.md deleted file mode 100644 index 4e18c32..0000000 --- a/docs/prd/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Dapp connectivity — PRD series - -These PRDs execute [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). Each is written to be -picked up by an independent agent: motivation, scope, implementation guidance mapped to real crates/ -files, and a Definition of Done tied to the project's gates (`docs/AGENTIC-ENGINEERING.md`). - -**Definition of Done — applies to every PRD below** (paste command output as evidence; never claim -done while red): - -1. `cargo fmt --all --check` clean. -2. `just check` green — clippy `-D warnings` on BOTH default and `--features tray`. -3. `cargo test --workspace` green. -4. No new/changed dependencies (`Cargo.toml`/`Cargo.lock`) unless explicitly approved in the PRD. -5. `deckard-core` crate-level lints respected (no `unwrap`/`expect`/`panic!`/raw indexing in non-test - code; `#![forbid(unsafe_code)]`). New untrusted-byte parsing goes through a bounded `Reader`. -6. No secret ever logged or `Debug`-printed (seeds/keys/passphrases/viewing keys stay in `Zeroizing`). -7. Every new user-facing action has a ⌘K `Command` (`palette_commands.rs` + `Shell::run_palette_command`). -8. Any visual/UI surface matches `DESIGN.md` (clear-signing card anatomy, amber=human/cyan=agent, - danger-early, hold-to-confirm). - -## Dependency graph - -``` -PRD-01 Resolver auth ─────────────┐ - ├─► PRD-04 Deckard-native bridge ─► (Phase 2, post-audit) -PRD-02 Clear-signing v2 ──────────┤ (universal reach, owned wire) - │ └─► PRD-05 Per-origin permissions + registry - └─► PRD-03 Curated native integrations (Phase 0, ships first) -``` - -| PRD | Title | Phase | Blocks on | Ships | -|-----|-------|-------|-----------|-------| -| [01](./01-resolver-authentication.md) | Resolver authentication (capability-gated `Resolve`) | 1a | — | independently; closes residual-risk #1 | -| [02](./02-clear-signing-and-message-intents.md) | Clear-signing v2 + message-signing intents | 1b | — | independently; needed by PRD-03 & PRD-04 | -| [03](./03-curated-native-integrations.md) | Curated native dapp integrations | 0 | PRD-02 (for permit/EIP-712) | first user-visible value | -| [04](./04-deckard-native-bridge.md) | Deckard-native bridge (universal reach, owned wire) | 2 | PRD-01, PRD-02, PRD-05 | post-audit | -| [05](./05-per-origin-permissions-and-registry.md) | Per-origin permissions, registry & anti-phishing | 2 | PRD-02 | with PRD-04 | -| [x](./x-walletconnect-shelved.md) | ~~WalletConnect transport~~ | — | — | **SHELVED / rejected** (rationale recorded) | - -**Recommended execution order:** PRD-01 and PRD-02 in parallel (foundational, independent) → PRD-03 -(first shippable value) → PRD-05 → PRD-04 (gated on an external audit per `SECURITY.md`). - -**Connectivity model (ADR 0001, second-pass requirements):** Deckard pursues **universal dapp reach** -but **owns the transport end-to-end** — no embedded browser, no WalletConnect relay, no store as a -trust anchor. Reach comes from injecting a standard EIP-1193/6963 provider via a first-party, -key-less connector over a Deckard-owned local wire (native messaging). WalletConnect is shelved with -rationale; the embedded webview is rejected. diff --git a/docs/prd/x-walletconnect-shelved.md b/docs/prd/x-walletconnect-shelved.md deleted file mode 100644 index a162d31..0000000 --- a/docs/prd/x-walletconnect-shelved.md +++ /dev/null @@ -1,153 +0,0 @@ -# SHELVED — WalletConnect v2 transport (rejected alternative) - -> **Status: REJECTED / SHELVED (2026-06-14).** Not on the roadmap. Superseded by the **Deckard-native -> bridge** ([PRD-04](./04-deckard-native-bridge.md)), which delivers universal dapp reach over a -> transport Deckard owns end-to-end. Retained only so the decision is recorded and not re-litigated. -> See [ADR 0001 §Alternatives](../adr/0001-dapp-connectivity-architecture.md). -> -> **Why shelved:** -> 1. **Centralized relay + Project-ID dependency** (`research §10–11`) — leaks IP/timing/topic metadata -> to a third-party (Reown) relay and requires a Project ID with usage analytics. Against Deckard's -> offline-first, privacy-focused identity. -> 2. **UX the maintainer explicitly rejects** — QR-pairing / relay round-trips / session churn. Deckard -> wants native, local, instant approval cards, which the owned bridge provides. -> 3. **No maintained Rust wallet-side SDK** (`research §14`) — the Sign protocol is an in-house build -> either way, so "adopt a standard to save work" is weak. -> 4. Its one genuine advantage — **mobile reach** — does not outweigh 1–3. Mobile universal-reach is -> left as an explicit open problem (ADR 0001 Consequences), not solved via WC. -> -> The text below is the original (pre-shelving) proposal, kept verbatim for the record. - ---- - -# PRD-04 — WalletConnect v2 transport (`deckard-wcd`) — ORIGINAL, SHELVED - -> Phase 2 of [ADR 0001](../adr/0001-dapp-connectivity-architecture.md). The **primary generic -> dapp-connectivity transport**: reaches desktop today and mobile later, no browser extension to ship -> or get hijacked. **Gated on PRD-01 (resolver auth), PRD-02 (clear-signing v2), PRD-05 (per-origin -> permissions), and an external security audit** (`SECURITY.md`). Do not start before its blockers. - -## Why this exists - -When Deckard wants to connect to dapps *it didn't integrate natively* (PRD-03), it needs a generic -transport. Research (`research §8–15`) found WalletConnect v2 dominates the alternatives for Deckard: - -- **Reaches mobile** (driver #1) — the only one of {extension, embedded webview, WalletConnect} that - does. An eventual mobile app reuses the same transport; no rewrite. -- **No extension artifact** to maintain in two hostile stores or get supply-chain-hijacked - (`research §6`). -- **E2E encrypted** (ChaCha20-Poly1305 / X25519); the relay cannot read sign/tx payloads - (`research §9`). -- **Protocol-level scope** via CAIP-25 namespaces (chains × methods × accounts) — maps cleanly onto - per-origin policy (PRD-05). - -The costs are real and this PRD must confront them: a **centralized Reown relay + Project-ID -dependency** (privacy/offline-first tension, `research §10–11`) and **no maintained Rust wallet-side -SDK** — only the low-level relay client; the Sign protocol must be built in-house (`research §14`). - -## Goals - -- A **key-less proposer process, `deckard-wcd`**, mirroring `deckard-mcp`: it speaks WalletConnect to - dapps and the existing `deckard-contract` wire to `deckard-signerd`. It holds **no key**, **cannot - `resolve`** (PRD-01 guarantees that even if it tried), and submits only **typed** intents/messages - through the PRD-02 builders — never raw arbitrary calldata that skips a typed path. -- Implement WC v2 pairing (`wc:` URI), sessions, and **CAIP-25 scope negotiation** restricting a - connected dapp to approved chains × methods × accounts (PRD-05 owns the policy; this owns the wire). -- Map WC method calls → Deckard intents: `eth_sendTransaction` → `Intent`; `personal_sign` / - `eth_signTypedData_v4` → `SignMessage` (PRD-02); `wallet_switchEthereumChain`/`addEthereumChain` → - guarded per PRD-02. Refuse `eth_sign` and (v1) delegation. -- **Verify-API origin attestation** surfaced on the card as MATCH/UNVERIFIED/MISMATCH/THREAT — never as - the sole defense (`research §13, 29`); the card still clear-signs real effects. -- **Relay-privacy posture**: document and implement what a privacy-focused wallet must do (below). - -## Non-goals - -- A browser extension (separate optional future PRD; explicitly secondary per ADR). -- Generic ABI decoding (PRD-02 scope: high-risk shapes + ERC-7730 + blind-sign fallback). -- Mobile app itself (driver #1: don't compromise desktop for it) — but **do not pick a design that - forecloses mobile** (keep `deckard-wcd`'s core protocol logic platform-agnostic). - -## Design - -### Process topology (reuse the `deckard-mcp` shape — `docs/build/30-mcp-shape.md`) - -``` -dapp ──WalletConnect (relay, E2E)──► deckard-wcd ──deckard-contract wire──► deckard-signerd - (key-less, (holds key, policy gate) - no resolve) │ - native clear-signing card ◄─ GPUI app - (control channel, PRD-01) -``` - -`deckard-wcd` is to dapps what `deckard-mcp` is to LLM agents: a thin, key-less translator. Reuse the -same daemon socket, the same `Propose`/`Execute`/`Status` poll loop, and the same native approval card. - -### Build vs borrow the WC Sign layer (decide first; record the decision) - -No maintained Rust wallet SDK exists (`research §14`). Options, in preference order: -1. **Build the Sign protocol in Rust** on `WalletConnectRust` (relay client + RPC types). Most work; - keeps everything native and dependency-light; full control of the crypto + scoping. **No new - heavyweight deps without approval** (DoD #4) — the relay client + an X25519/ChaCha20 stack (some - already in-tree via the keystore) need an explicit dependency review. -2. A separate sidecar in another language only if (1) is infeasible — but that reintroduces a non-Rust - surface; avoid unless forced. - -This PRD's first deliverable is a **spike** (mirror `spikes/`): pin `WalletConnectRust`'s exact surface, -prove a pairing + one `personal_sign` round-trip against a test dapp, and write the build/borrow -recommendation before committing the full implementation. - -### Relay privacy (the offline-first tension, `research §10–11`) - -- Route relay egress through the **same network path Deckard already uses** (so it inherits any - proxy/VPN/Tor the user configures); document that the relay sees IP + timing + topic metadata. -- Rotate topics per session; don't reuse pairing topics. -- Expose a **custom relay URL** setting (for future self-host / testing), defaulting to the public - relay, clearly labeled. Note the Project-ID analytics dependency honestly in-app and in docs. -- WalletConnect is **opt-in and off by default** — it is a network feature in an offline-first wallet; - the user turns it on, sees the relay-dependency notice, and it never silently phones home. - -### Scope & methods - -- Negotiate CAIP-25 with **required namespaces empty/minimal** (per Reown guidance, `research §12`) and - drive real scope from optional namespaces + Deckard's per-origin policy (PRD-05). -- Method allowlist per session; an out-of-scope method request is refused with a typed error and shown - to the user. - -## Acceptance tests - -- Spike report committed: pairing + `personal_sign` round-trip proven; build/borrow recommendation. -- `wcd_is_keyless`: memory/fd scan of `deckard-wcd` finds no key (reuse the `deckard-mcp` red-team - harness, `docs/build/00-test-harness.md`). -- `wcd_cannot_resolve`: `deckard-wcd` attempting `Resolve` is refused by PRD-01's control-channel gate - (cross-PRD integration test). -- `method_out_of_scope_refused`: a dapp requesting a method outside the negotiated CAIP-25 scope is - refused; nothing reaches the daemon. -- `sign_request_clear_signs`: a `personal_sign`/EIP-712 from a dapp renders the PRD-02 card with origin - attestation state; off-chain danger flags fire. -- `relay_opt_in_default_off`: with WC disabled, no relay connection is opened (assert no socket/egress). -- Transcript hygiene extended to `deckard-wcd` (no secret/RPC-token leakage). - -## Definition of Done - -PRD-series DoD **plus**: ⌘K commands for connect/disconnect/list-sessions/STOP-all-sessions; the relay -privacy posture documented in `SECURITY.md` + `THREAT-MODEL.md` (new "WalletConnect transport" -surface); off-by-default verified; **an external audit sign-off recorded** before this ships beyond -testnet (per `SECURITY.md`). New deps (if any) explicitly approved and added to `deny.toml`/`Cargo.lock` -review. - -## Risks & fallbacks - -- **In-house Sign protocol is substantial.** Fallback: ship PRD-03 (native integrations) as the user - value while this bakes; WC is not on the critical path to first value. -- **Relay centralization/availability** (`research §11`): the custom-relay-URL setting + opt-in posture - contain it; track WC's decentralized-relay progress before relying on self-host. -- **Verify API is not bulletproof** (`research §13`): treat as one signal; the clear-signing card + - per-origin policy (PRD-05) + blocklist are the real defenses. -- **Spoofable dapp metadata** (`research §13`): never let the claimed name substitute for decoded - effects (`research §29`). - -## Sources - -`docs/research/10-dapp-connectivity.md §8–15, 29`; specs.walletconnect.com (pairing-uri, crypto-keys, -sign/namespaces, core/verify); docs.reown.com/cloud/relay; github.com/WalletConnect/WalletConnectRust; -`docs/build/30-mcp-shape.md` (the proposer pattern to mirror). diff --git a/docs/research/10-dapp-connectivity.md b/docs/research/10-dapp-connectivity.md index f2ad537..286ffab 100644 --- a/docs/research/10-dapp-connectivity.md +++ b/docs/research/10-dapp-connectivity.md @@ -4,7 +4,7 @@ > later) whose key lives in a process-isolated signer daemon behind a policy gate and a native > clear-signing card — let third-party dapps interact with it? This is the cited evidence base > behind [`docs/adr/0001-dapp-connectivity-architecture.md`](../adr/0001-dapp-connectivity-architecture.md) -> and the `docs/prd/*` series. Five parallel research angles, claims ranked by confidence, +> and the PRD work items tracked in GitHub issues (epic #44). Five parallel research angles, claims ranked by confidence, > primary sources linked. Where a claim is our own reasoning over the sources it is marked > **(inference)**. diff --git a/spikes/deckard-bridge-spike.md b/spikes/deckard-bridge-spike.md deleted file mode 100644 index 15599aa..0000000 --- a/spikes/deckard-bridge-spike.md +++ /dev/null @@ -1,109 +0,0 @@ -# Spike prompt: the Deckard-native dapp bridge (PRD-04 #1) - -> Feed this whole file to a fresh coding agent. It is self-contained. Goal: prove (or disprove) that a -> **first-party, key-less browser connector** can give Deckard universal dapp reach over a wire Deckard -> **owns end-to-end** — no third-party relay, no embedded browser, no store as a trust anchor — and -> settle the two open choices: **(a) the local wire** (native messaging vs hardened localhost) and -> **(b) the self-distribution path** on both Chromium and Firefox. Standalone spike — do **not** touch -> the app crates (`crates/`) or ship anything user-facing. - -Executes the first deliverable of [`docs/prd/04-deckard-native-bridge.md`](../docs/prd/04-deckard-native-bridge.md). -Read it, [ADR 0001](../docs/adr/0001-dapp-connectivity-architecture.md), and -[`docs/research/10-dapp-connectivity.md`](../docs/research/10-dapp-connectivity.md) first. The research -section references below (`§N`) point at claims in that file. - -## Context you can trust (verified in research — don't re-litigate) - -- **Dapps already speak EIP-1193**, so injecting a *standard* provider (announced via **EIP-6963**, not - fighting over `window.ethereum`) reaches all of them with no per-dapp work (`§7, 21–22`). -- **Native messaging is the leading wire**: an OS-installed stdio host gated by a manifest - `allowed_origins` (Chrome, exact `chrome-extension://`, no wildcards) / `allowed_extensions` - (Firefox, the gecko id), running as the user, **not reachable from web pages** (`§3, 12, 15`). - Manifest locations differ per browser/OS and live in user-writable paths (`§4, 15`). -- **A localhost RPC port (Frame's `ws://127.0.0.1:1248`) is web-reachable** — same-origin policy does - not stop a page from calling localhost, WebSockets aren't CORS-gated at handshake, and DNS-rebinding - defeats naive Host checks. If used at all it needs origin-allowlist + token + Host validation, and - even then it's weaker than native messaging (`§1, 2`). -- **MV3 service workers are ephemeral**; a `connectNative` port keeps the worker alive but has had - cross-version edge cases — you must reconnect on `onDisconnect` (`§5`). -- **Trust is bounded by design, not by the store.** The connector is key-less: even fully compromised it - can only *propose*; it can never sign, self-approve, or exfiltrate a key (the daemon owns the key; the - resolver capability is PRD-01; effects are clear-signed, PRD-02). This is *why* self-signed - distribution outside a store is acceptable (`§6, 22, 29`). -- The existing **`deckard-mcp`** is the proposer pattern to mirror: a key-less translator speaking the - `deckard-contract` wire to `deckard-signerd` (`docs/build/30-mcp-shape.md`). The mock daemon in - `deckard-contract` (`MockSigner`) answers the socket API without a real key. - -## The two questions the spike must settle - -1. **Wire:** does **native messaging** carry a full EIP-1193 request/response round-trip cleanly on - **both Chromium and Firefox**, with stable reconnection under MV3 SW eviction? If yes, it wins and - localhost is not needed. Only if a target browser/setup *blocks* native messaging do we fall back to - a hardened localhost wire — in which case prove the origin-allowlist + token + Host-check defenses. -2. **Distribution:** can we **self-distribute a Deckard-signed connector** on both browsers without - relying on a store as the trust anchor? Document the exact path + friction: - - Firefox: self-hosted signed XPI (AMO-signed but self-distributed) — confirm it installs. - - Chromium: normal installs lean on the Web Store; document the self-/sideload path (unpacked/dev - mode, enterprise policy, `.crx` + `update_url`) and its real-world friction for an end user. - -## Tasks (do in order) - -1. **Stand up a mock host + mock daemon.** A small Rust binary (the spike's `deckard-bridged` stand-in) - that: (a) speaks the browser **native-messaging stdio framing** (32-bit native-endian length prefix + - UTF-8 JSON, `§12`) on one side; (b) speaks the `deckard-contract` wire to the **`MockSigner`** (no - real key) on the other. Map `eth_requestAccounts` → `Address`, `personal_sign` → a (mock) message - sign, `eth_sendTransaction` → `Propose`/`Status`/`Execute`. Standalone crate under - `spikes/deckard-bridge/` with its own `[workspace]`; `.gitignore` `target/` + `Cargo.lock`. Depend on - `deckard-contract` by path for the wire types; **no real key, no real signing, no broadcasting.** -2. **Build a minimal connector** (Chromium MV3 + Firefox MV2/MV3 as needed; separate manifests). It - injects an EIP-1193 provider, announces it via **EIP-6963** (`rdns` e.g. `sh.deckard`), and relays - page JSON-RPC → background → native-messaging port → host. Handle MV3 SW lifecycle: reconnect on - `onDisconnect`, no lost requests. -3. **Drive a real dapp end to end.** Point a simple test page (or a real read-only dapp) at the injected - provider and complete: `eth_requestAccounts` (returns the mock address) and one `personal_sign` - (returns a mock signature). Confirm the request reaches the host and the mock daemon, on **Chromium - and Firefox**. -4. **Stress the MV3 path.** Force service-worker eviction (idle it out / `chrome://serviceworker-internals`) - mid-session and confirm the next request reconnects and succeeds (`§5`). -5. **Prove the web-unreachability of native messaging.** From an ordinary web page, attempt to reach the - host directly (there should be no port to reach). Then, for contrast, if you also stand up the - localhost fallback, show a cross-origin page *can* reach it without the token/Host defenses — and that - the defenses block it. This is the security evidence for the wire choice. -6. **Self-distribution dry run.** Produce a signed Firefox XPI and install it from a self-hosted URL - (not AMO listing). Produce the Chromium artifact and document the install path + friction. Record - exact steps so PRD-04 can reproduce. - -## Constraints - -- Standalone: `spikes/deckard-bridge/` (Rust host + mock) with its own `[workspace]`; the connector is - plain JS/TS, no build pipeline beyond what's needed. Do **not** edit `crates/` or the app. -- **Key-less throughout.** The host/connector never hold a key; reads/proposes only against `MockSigner`. - No real network broadcast. -- Mirror `deckard-mcp`'s no-secret-in-transcript discipline: no key/passphrase/token in any log or - message (a quick grep assertion is enough for the spike). - -## Success criteria (what "done" means) - -- A clear **YES/NO** on: *"Does native messaging carry a full EIP-1193 round-trip cleanly on both - Chromium and Firefox, with reliable reconnection under MV3 eviction?"* — with the evidence. -- A clear **wire recommendation** (native messaging vs localhost) with the security argument (task 5). -- A clear **self-distribution recommendation** with the exact Firefox + Chromium paths and their - friction (task 6). -- A runnable spike: connector + host + mock daemon completing `eth_requestAccounts` + `personal_sign`. -- A short report feeding PRD-04's "owned wire" + "distribution" sections. - -## Report format (return this) - -``` - - NATIVE MESSAGING / LOCALHOST REQUIRED / BLOCKED - - - - - - - - - -```