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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions