Skip to content

PRD-01: Resolver authentication (capability-gated Resolve) #45

Description

@hellno

Part of #44 (ADR 0001). Phase 1a. Closes THREAT-MODEL.md residual-risk #1. Foundational and independent — has standalone security value with zero dapps, and is a hard prerequisite for PRD-04.

Why

crates/deckard-signerd/src/auth.rs authorizes on same_uid() alone, so Resolve{approved:true} is honored from any same-uid process — the textbook confused-deputy gap (research §22–24: uid is ambient authority; peer-cred proves who connected, not which role). Every hardware/companion signer (Trezor, Ledger, GridPlus, Frame) separates request (promiscuous endpoint) from approval (a surface the requester can't drive). Deckard must manufacture that split in software before any browser-reachable proposer exists.

Goals

  • The GPUI app is the sole principal that can send Resolve.
  • Approval authority is an unforgeable capability the daemon hands only to the app it controls.
  • The public proposer socket keeps accepting proposals/reads but rejects Resolve with a typed error.
  • STOP/RevokeAll stays reachable on every channel (it only reduces authority).

Non-goals

Multi-principal daemon; salted request-ids (residual #6); keystore/guardrail/STOP-latency changes; dapp-origin auth (that's PRD-05).

Design

Capability channel (supervise.rs already has the app spawn+supervise the daemon — the app is the parent):

  1. socketpair() inherited by the daemon child (recommended) — app creates an AF_UNIX pair pre-spawn, passes one end to the child, keeps the other; Resolve accepted only there. Portable Linux+macOS (research §26).
  2. SCM_RIGHTS fd-pass after connect — daemon passes back a dedicated control fd; Resolve only on it (research §25, 27).
  • Fallback (weaker, document if used): per-launch 0600 cookie read by the app.

Daemon (deckard-signerd): keep same_uid() for the public socket (defense-in-depth/logging); route Resolve only from the control channel; a Resolve on the public socket → typed denial (deny_reasons::RESOLVE_NOT_AUTHORIZED, URL-redacted, no payload echo). Keep STOP on every channel. Don't re-trust peer creds across the connection lifetime (staleness, research §24).

App (deckard-app): establish the control channel at spawn (supervise.rs/shell.rs); thread its handle to where Resolve is sent after a completed hold-to-confirm (shell.rs:142 area). Hold-to-confirm contract unchanged — only the channel changes.

Contract: prefer no wire change (channel, not frame, carries authority). If a cookie fallback is used, add a typed field with redacted Debug.

Cross-platform: Linux SO_PEERCRED/socketpair/SCM_RIGHTS; macOS LOCAL_PEERCRED (no pid; pid via LOCAL_PEERPID) — fd-capability is the portable primitive. Confine unsafe fd handling to the app crate with a scoped, documented #[allow(unsafe_code)] (match the eth.rs pattern); keep fd plumbing out of deckard-core. Use already-present nix/tokio — no new deps.

Acceptance tests (default cargo test, not #[ignore])

DoD

Shared DoD (see #44) plus: THREAT-MODEL.md residual #1 moves to "Mitigated" with the mechanism named; a decision note records which capability mechanism was chosen; the four tests run by default.

Risks

fd inheritance across Command::spawn is platform-fiddly → SCM_RIGHTS, then cookie. Handle the supervise respawn path so a restarted app re-handshakes with no window where Resolve is ungated.

Sources: research §22–29; THREAT-MODEL.md (residual #1); man7 unix(7); cloudflare SCM_RIGHTS; capability-myths-demolished.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions