Skip to content

fix(agent): per-request signing authorization (#354)#356

Merged
bordumb merged 2 commits into
mainfrom
fix/agent-354-per-request-auth
Jun 24, 2026
Merged

fix(agent): per-request signing authorization (#354)#356
bordumb merged 2 commits into
mainfrom
fix/agent-354-per-request-auth

Conversation

@bordumb

@bordumb bordumb commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Addresses #354 (partial — lands the enforceable mechanism + tested policy; the platform approval prompt that flips enforcement to on is the remaining follow-up).

Problem

The signing agent (the SSH-agent used for git commit signing) held unlocked keys and would sign for any same-user process that reached its socket during the unlock window — no per-request authorization, no biometric. The existing peer-UID check gates other users, not another process you run.

What this PR does (TDD)

  • RED → GREEN: agent_refuses_to_sign_when_authorizer_denies failed against current code (the agent signs for anyone); now passes via a new gate.
  • SignAuthorizer hook consulted in AgentSession::sign before every signature — deny ⇒ refuse.
  • PerCallerAuthorizer (the chosen policy): approves a peer once per connecting process and pins it, so the legitimate caller isn't re-prompted per signature while a different process triggers a fresh approval. Tested (per_caller_pins_approved_and_reprompts_a_different_process). Keyed by (uid, pid); macOS has no peer pid → falls back to time-bucketed re-auth.
  • Wired live on the socket path: PeerAuthorizedAgent reads the peer (uid, pid) and injects the authorizer per session. Authorizer types re-exported as public API for host injection.

Impact (why it's a policy, not a hard prompt)

  • CI: unaffected — release/artifact signing uses auths sign (direct keychain decrypt), never the agent.
  • Autonomous / MCP: keep an explicit non-interactive policy (their authority is the delegation/scope, not a human touch).
  • Developer: per-caller approval (not per-signature), so a 30-commit rebase from one git process = one approval, not 30.

Honest limitation

The shipped default is permissive (AllowAllSigning) — signing behavior is unchanged. Real enforcement requires the platform approval prompt (macOS LocalAuthentication / Touch ID, or a Linux prompt) injected as the PerCallerAuthorizer approval Fn at the CLI boundary. That platform piece is deliberately a follow-up rather than faked.

Tests

All 14 agent tests green (2 new + 12 existing); auths-core compiles clean. Full guard (clippy --workspace, fmt-all) not yet run — left to CI.

The signing agent held unlocked keys and would sign for any same-user
process that reached its socket during the unlock window. Add a
per-request SignAuthorizer the agent consults before every signature,
and a PerCallerAuthorizer that approves a peer once per connecting
process and pins it (Linux keys by uid+pid; macOS has no peer pid and
falls back to a time-bucketed re-auth). The socket path now reads the
peer (uid, pid) and injects the authorizer into each session.

The shipped default stays permissive (AllowAllSigning) so signing
behavior is unchanged; enforcement turns on when the host injects an
approval prompt. Adds a failing-then-passing test that a denied peer
gets no signature, plus a per-caller pinning test.

Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd
Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd
Auths-Anchor-Seq: 1
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
auths Ready Ready Preview, Comment Jun 24, 2026 7:32pm

@github-actions

Copy link
Copy Markdown

Auths Commit Verification

Commit Status Details
a050a6c3 ❌ Failed No signature found

Result: ❌ 0/1 commits verified


How to fix

Commit a050a6c3 has no Auths signature (no Auths-Id/Auths-Device trailer).

1. Install auths

macOS: brew install auths
Linux: Download from releases

2. One-time setup (creates your identity and configures Git)

auths init

3. Sign this branch and push

auths sign origin/main..HEAD
git push --force-with-lease

For CI to verify the signer, commit an identity bundle:

auths id export-bundle --alias main --output .auths/ci-bundle.json --max-age-secs 31536000

Quickstart →

…al for the interactive agent (#354)

Thread the SignAuthorizer through the agent serve path
(start_agent_listener_with_handle) as injected policy instead of a hardcoded
permissive default. The CLI now selects the policy by context: an interactive
(TTY) foreground agent gates each new connecting process behind a terminal
approval prompt and pins it (PerCallerAuthorizer); a daemonized / headless
agent stays permissive, since there is no human present to approve. CI is
unaffected (it signs via the direct keychain path, not the agent).

The macOS Touch ID / Linux GUI prompt for the daemonized case is the remaining
backend; the LocalAuthentication infra to build it on already exists.

Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd
Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd
Auths-Anchor-Seq: 1
@github-actions

Copy link
Copy Markdown

Auths Commit Verification

Commit Status Details
e5dcb1e7 ❌ Failed No signature found
a050a6c3 ❌ Failed No signature found

Result: ❌ 0/2 commits verified


How to fix

Commit e5dcb1e7 has no Auths signature (no Auths-Id/Auths-Device trailer).

1. Install auths

macOS: brew install auths
Linux: Download from releases

2. One-time setup (creates your identity and configures Git)

auths init

3. Sign this branch and push

auths sign origin/main..HEAD
git push --force-with-lease

For CI to verify the signer, commit an identity bundle:

auths id export-bundle --alias main --output .auths/ci-bundle.json --max-age-secs 31536000

Quickstart →

@bordumb

bordumb commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Update (e5dcb1e7): enforcement is now wired, not just the mechanism.

  • The SignAuthorizer is dependency-injected through the agent serve path (start_agent_listener_with_handle) instead of a hardcoded permissive default.
  • The CLI selects the policy by context: an interactive (TTY) foreground agent gates each new connecting process behind a terminal approval prompt and pins it (PerCallerAuthorizer); a daemonized / headless agent stays permissive (no human to approve).
  • CI is unaffected — it signs via the direct keychain path (auths sign), never the agent.

Verified: full chain (cli → sdk → core) compiles clean; 7/7 agent tests pass including the socket integration test now routed through the injected authorizer.

Remaining for full enforcement on the common (daemonized) deployment: the macOS Touch ID / Linux GUI approval backend. The infra exists to build it on (build.rs links LocalAuthentication; the Swift bridge already drives LAContext for Secure-Enclave signing) — it's a thin approve backend swapped in for the daemon case. Note: that dialog can't be verified from a headless CI environment; it needs on-device confirmation.

@bordumb bordumb self-assigned this Jun 24, 2026
@bordumb bordumb marked this pull request as ready for review June 24, 2026 22:27
@bordumb bordumb merged commit bc4760a into main Jun 24, 2026
23 of 24 checks passed
@bordumb bordumb deleted the fix/agent-354-per-request-auth branch June 24, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant