Summary
The signing agent in auths-core holds unlocked private-key material and serves signature requests over a
Unix-domain socket. Connection authorization is by peer UID only. This correctly rejects other users,
but it means any process running as the same user can connect during the unlock window and obtain
signatures over arbitrary payloads — and there is no per-signature re-authentication (no biometric /
Secure-Enclave user-presence check, no approval prompt). A single unlock grants silent, blanket signing to
every local process the user runs, for the duration of the window.
Where (code)
Caller authorization — crates/auths-core/src/agent/session.rs:299:
fn peer_is_authorized(peer_uid: u32, owner_uid: u32) -> bool {
peer_uid == owner_uid
}
This is the only caller gate (it is fed by an SO_PEERCRED / getpeereid check on the connecting socket). It
does not distinguish processes of the same user.
Unlock window — crates/auths-core/src/agent/handle.rs:15 and :19:
pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30 * 60); // sliding; resets on each sign
pub const DEFAULT_MAX_UNLOCK_TTL: Duration = Duration::from_secs(8 * 60 * 60); // absolute cap
During this window the agent signs any request from a same-UID connection. The signing path
(AgentSession::sign in crates/auths-core/src/agent/session.rs) performs no per-request re-authentication
beyond confirming the agent is unlocked.
Threat / scenario
- The user unlocks the agent once (passphrase / initial approval).
- Any other process running as that user — a malicious dependency, a compromised build tool, a script —
connects to the agent's Unix socket.
- It sends a sign request with attacker-chosen bytes and receives a valid signature attributable to the
user's identity.
- This succeeds silently for up to the unlock window (default 30 min sliding idle, 8 h absolute), with no
prompt or biometric.
Already in place (do not regress)
- Socket directory is
0o700, socket file 0o600, with fail-closed handling of pre-existing loose
permissions (crates/auths-core/src/api/runtime.rs, functions harden_socket_dir / harden_socket_file).
- Cross-user connections are rejected (the UID check above).
- Locking clears in-memory keys; both idle and absolute caps exist and are tested.
The gap is specifically: same-user callers + no per-use confirmation.
Proposed remediation (pick / combine)
- Per-signature (or per-new-peer) user approval. Require an explicit user action — OS notification /
TUI prompt / Touch ID — to approve each signature, or at least the first request from each newly
connecting process. (ssh-agent's confirm mode and password-manager per-request approval are the model.)
- Per-signature biometric / Secure-Enclave user-presence. When the key is SE-backed, require a Touch ID
/ enclave user-presence check per signature (or per N signatures), not only at initial unlock.
- Caller pinning / allowlist. Record the first authorized peer (pid + executable path / code-signing
identity) and require explicit approval when a different same-user process connects.
- Tighten defaults. Shorten the 8 h absolute cap and/or add a max-signatures-per-unlock counter.
Full defense against same-UID malware is not achievable without OS-level isolation; the realistic goal is
that obtaining a signature requires user interaction, so one unlock does not equal silent blanket signing.
Acceptance criteria
- A same-user process connecting mid-window cannot obtain a signature without the chosen approval/biometric
step.
- A test under
crates/auths-core/tests/cases/ that drives the real socket and asserts a second connection
cannot sign without that step (mirroring the existing agent_socket.rs socket tests).
- Existing cross-user rejection and lock-clears-keys tests still pass.
Severity
High. The agent exists to hold signing capability; "one unlock → silent blanket signing for any local
process you run" is the realistic local-compromise path.
Summary
The signing agent in
auths-coreholds unlocked private-key material and serves signature requests over aUnix-domain socket. Connection authorization is by peer UID only. This correctly rejects other users,
but it means any process running as the same user can connect during the unlock window and obtain
signatures over arbitrary payloads — and there is no per-signature re-authentication (no biometric /
Secure-Enclave user-presence check, no approval prompt). A single unlock grants silent, blanket signing to
every local process the user runs, for the duration of the window.
Where (code)
Caller authorization —
crates/auths-core/src/agent/session.rs:299:This is the only caller gate (it is fed by an SO_PEERCRED /
getpeereidcheck on the connecting socket). Itdoes not distinguish processes of the same user.
Unlock window —
crates/auths-core/src/agent/handle.rs:15and:19:During this window the agent signs any request from a same-UID connection. The signing path
(
AgentSession::signincrates/auths-core/src/agent/session.rs) performs no per-request re-authenticationbeyond confirming the agent is unlocked.
Threat / scenario
connects to the agent's Unix socket.
user's identity.
prompt or biometric.
Already in place (do not regress)
0o700, socket file0o600, with fail-closed handling of pre-existing loosepermissions (
crates/auths-core/src/api/runtime.rs, functionsharden_socket_dir/harden_socket_file).The gap is specifically: same-user callers + no per-use confirmation.
Proposed remediation (pick / combine)
TUI prompt / Touch ID — to approve each signature, or at least the first request from each newly
connecting process. (ssh-agent's
confirmmode and password-manager per-request approval are the model.)/ enclave user-presence check per signature (or per N signatures), not only at initial unlock.
identity) and require explicit approval when a different same-user process connects.
Full defense against same-UID malware is not achievable without OS-level isolation; the realistic goal is
that obtaining a signature requires user interaction, so one unlock does not equal silent blanket signing.
Acceptance criteria
step.
crates/auths-core/tests/cases/that drives the real socket and asserts a second connectioncannot sign without that step (mirroring the existing
agent_socket.rssocket tests).Severity
High. The agent exists to hold signing capability; "one unlock → silent blanket signing for any local
process you run" is the realistic local-compromise path.