Skip to content

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

Closed

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

  1. hellno commented on Jun 14, 2026

    @hellno
    OwnerAuthor

    Delivered in PR #55 (merged 2026-06-14).

    Capability-gated Resolve via an AF_UNIX socketpair: the GPUI app (parent) mints the pair before spawning the daemon and hands the child one end by fd inheritance (DECKARD_RESOLVE_FD), keeping the other as a ControlChannel. The daemon honors Resolve ONLY on the inherited control end (Channel::Control); a Resolve on the public proposer socket is refused with the typed resolve_not_authorized denial; STOP (Lock/RevokeAll) stays reachable on every channel; each respawn re-handshakes and fails closed mid-restart.

    THREAT-MODEL.md residual-risk #1 → Mitigated; ADR 0001 PRD-01 open question resolved (socketpair fd-inheritance). Tests green: resolver_auth (public-socket Resolve rejected, control-channel accepted, STOP-on-public still works, red-team second-proposer-cannot-self-approve) — re-verified 2026-06-14. Closing PRD-01.

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