[AAASM-3569] 🔒 (aa-sdk-client): SDK↔runtime IPC authentication & anti-impersonation#1216
Conversation
Move libc to a cfg(unix) target table so geteuid() is available on macOS/BSD as well as Linux, backing the IPC socket peer-UID check. Refs AAASM-3579. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure peer_uid_is_allowed() policy + current_runtime_uid() (geteuid), isolated and unit-tested so the platform-specific code is testable without opening a real socket. Refs AAASM-3579. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read SO_PEERCRED/getpeereid via UnixStream::peer_cred in the accept loop and drop any connection whose process UID does not match the runtime's euid, before spawning any handler — defence-in-depth over the 0600 socket perms against local impersonation/forged events. Closes AAASM-3579. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply umask(0o077) around UnixListener::bind so the socket inode is owner-only from creation — closes the bind→chmod TOCTOU window. Add agent_id to IpcServerConfig and debug-assert the bound path is scoped to it. Keep set_permissions(0o600) as belt-and-suspenders. Refs AAASM-3581. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Asserts the socket file mode is exactly 0o600 immediately after bind and the path contains the configured agent id. Closes AAASM-3581. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HandshakeChallenge{nonce} + HandshakeProof{agent_did, public_key,
signature} for the local SDK↔runtime UDS session handshake; wired into
build.rs codegen and the lib.rs module tree.
Refs AAASM-3583.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
IpcFrame::HandshakeProof (inbound) + IpcResponse::HandshakeChallenge (outbound) wrapping the assembly.ipc.v1 protos for the per-session UDS handshake. Refs AAASM-3583. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ests Add TAG_HANDSHAKE_PROOF=5 (inbound) / TAG_HANDSHAKE_CHALLENGE=5 (outbound), extend read_frame/write_response for the new handshake frames (existing tags unchanged), and add round-trip tests: HandshakeProof decodes through read_frame; HandshakeChallenge encodes through write_response with the correct tag. Closes AAASM-3583. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dependencies for verifying the SDK's Ed25519 proof of key possession and generating per-session challenge nonces. Refs AAASM-3585. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
expected_verifying_key() derives the agent's key from AA_AGENT_ID the same way the SDK does; generate_nonce() yields a fresh 32-byte nonce; verify_proof() fails closed unless the proof's public_key matches the expected key and the signature verifies over the exact nonce. Unit tests cover valid/forged/wrong-nonce/wrong-key/bad-length proofs. Refs AAASM-3585. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before any frame is dispatched, the runtime challenges each accepted connection with a fresh nonce (perform_handshake) and verifies the SDK's signed proof against the key derived from AA_AGENT_ID. The handshake runs inside the per-connection task (never the accept loop), bounded by a 5s timeout; the response router and active-connection counter are wired only after a valid proof, so an unauthenticated or non-handshaking peer is dropped without dispatch. Existing dispatch tests updated to perform the SDK-side handshake. Refs AAASM-3585. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retain the SigningKey and add sign(message) -> [u8;64] so the SDK can prove key possession over the runtime's handshake nonce. Tests assert the signature verifies under the public key and is deterministic. Refs AAASM-3587. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rite) read_handshake_challenge() returns the runtime's nonce; write_handshake_proof() sends the signed proof. Tags mirror aa-runtime (TAG_HANDSHAKE_CHALLENGE/PROOF=5). Refs AAASM-3587. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…osed ipc_loop now reads the runtime's nonce challenge and sends a signed HandshakeProof (perform_handshake/build_handshake_proof) before any heartbeat/event; a failed handshake aborts the loop rather than leaking traffic onto an unauthenticated channel. spawn_ipc_thread/ipc_loop take the agent_id to derive the signing key. Mock-server tests + the lifecycle e2e updated to perform the server-side handshake. Closes AAASM-3587. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three black-box cases on the gated IPC server: (1) a peer that sends an EventReport without handshaking is never dispatched; (2) a peer whose handshake signature is corrupted is rejected and never dispatched; (3) a peer with a valid handshake has its event dispatched. Closes AAASM-3589. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field 14: a raw Ed25519 signature over the registering did:key proving the caller holds the private key for public_key, gating credential_token issuance (AAASM-3591). Refs AAASM-3591. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cargo.lock update for the aa-runtime handshake-verification dependencies added in this branch. Refs AAASM-3585. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register now verifies an Ed25519 possession_proof over the registering did:key against public_key (verify_possession_proof) and returns Unauthenticated when it is missing/forged/wrong-length — closing the "hit unauthenticated :50051 and mint a credential_token" path. Unit tests cover accept/missing/forged/wrong-challenge/wrong-length. Coordinates with AAASM-3416; minimal gate a future interceptor layers on. Refs AAASM-3591. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exhaustive RegisterRequest literal in the serialization test now includes the new possession_proof field (AAASM-3591). Refs AAASM-3591. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_register_request signs the registering did:key with the agent's keypair and sets possession_proof, so the gateway admits the registration and mints a credential_token. Refs AAASM-3591. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Success-path Register requests now sign their did:key to pass the new possession gate; the parent-unknown test gets a valid proof so it still reaches the InvalidArgument parent-lookup. Invalid-public-key and non-did:key tests left unproofed — they fail earlier by design. Closes AAASM-3591. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runtime↔gateway e2e harness needs the production AgentKeypair to sign the AAASM-3585 session handshake nonce; pull aa-sdk-client into dev-deps so the test can reuse the real signer instead of duplicating Ed25519 crypto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…teway e2e
AAASM-3585 made the SDK↔runtime IPC require a signed per-session handshake
before the runtime serves any PolicyQuery: on connect the runtime sends a
HandshakeChallenge{nonce} and expects a HandshakeProof signed with the
agent's deterministic Ed25519 key. This test connected and sent a PolicyQuery
without handshaking, so the runtime's first frame was the challenge (tag 5),
not a PolicyResponse (tag 1), and the assertion at line 191 failed.
Make check_tool complete the handshake first — read the challenge, sign the
nonce with the production aa_sdk_client::AgentKeypair (seed = SHA-256(agent_id),
identical to the runtime's expected key), and send the proof — mirroring the
real SDK exactly. The runtime's security behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI fix:
|
Review — AAASM-3569 SDK↔runtime IPC authentication & anti-impersonationVerdict: READY to merge (pending the required Pioneer approval — no code/CI blocker). CIAuthoritative The four red checks are not code failures / not gating:
No fix pushed (would be wrong to "fix" an infra flake with code). Scope coverage — all 8 subtasks verified
Commit history is granular and bisectable (23 commits, one logical unit each).
|
The codec round-trip test built a HandshakeChallenge from a hard-coded literal nonce (vec![3u8; 32]), which CodeQL flags as a hard-coded value used as a nonce. Generate a fresh random nonce via the production handshake::generate_nonce() and assert the decoded value round-trips against it. Production nonce generation is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nces The mock-server handshake tests need a CSPRNG to generate fresh per-test challenge nonces instead of embedding hard-coded literals (CodeQL hard-coded-nonce alert). getrandom 0.2 mirrors the runtime's production nonce source and is already resolved in the lockfile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mock-server handshake helpers in ipc.rs and lifecycle_e2e.rs built HandshakeChallenge from hard-coded literal nonces (vec![42u8; 32] / vec![9u8; 32]), which CodeQL flags as hard-coded values used as a nonce. Fill a fresh 32-byte nonce from getrandom each run; the proof is still signed over and verified against the random nonce, so the assertions are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cleared the CodeQL hard-coded-nonce alertThe CodeQL "critical" finding ("This hard-coded value is used as a nonce", ×6) was not in production. Production nonce generation is already correct and was left untouched:
The 6 flagged flows all originated from hard-coded literal nonces in test/codec code added by this PR. Fixed at the 3 literal sources (which CodeQL expands into ~6 sink flows via clone + sign + verify):
Each nonce is still signed and verified against the same (now random) value, so no assertion was weakened. Local validation (touched crates): CodeQL should report 0 new alerts on re-analysis. — Claude Code |
…ar CodeQL hard-coded-crypto) CodeQL's "Hard-coded cryptographic value" (Critical) flagged the `[0u8; NONCE_LEN]` buffer literal at handshake.rs:36 as a hard-coded value flowing into a nonce: getrandom's in-place `&mut` fill is not recognized as a sanitizer, so the zero-init literal stayed in the taint path. Produce the 32 bytes directly from `rand::random` (a ChaCha-based, OS-seeded thread CSPRNG that returns the value), so no constant literal enters the nonce data-flow. Runtime randomness is unchanged/strengthened; NONCE_LEN preserved. Drops the now-unused direct getrandom dep (rand 0.8 was already a transitive dep via async-nats, so the dep graph is unchanged; cargo deny stays clean). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeQL "Hard-coded cryptographic value" (Critical) — real source fixedThe previous attempt randomized the test nonces, but the Critical alert's source is production code: CodeQL's taint model does not recognize Fix (commit pub fn generate_nonce() -> [u8; NONCE_LEN] {
rand::random::<[u8; NONCE_LEN]>()
}
Local validation: I'm not asserting the CodeQL check is green — leaving the CI re-analysis on the new head to confirm. — Claude Code |
CodeQL CRITICAL "hard-coded value used as a nonce" at ipc.rs:369/387 — the test helpers used `[0u8; 32]`/`vec![0u8; 32]` + getrandom in-place fill, whose buffer literal CodeQL flags as flowing into the signed nonce (it doesn't model getrandom's &mut fill as sanitizing). Switch to value-returning rand::random() so no literal is in the data-flow. Same fix as the #1216 production handshake. No behavior change. Refs AAASM-3666.
Description
Closes Story AAASM-3569 (under Epic AAASM-3567): authenticates the local SDK↔aa-runtime UDS channel and the gateway credential_token mint, closing the local-impersonation and forged-event vectors.
Three controls, layered:
SO_PEERCRED/getpeereidviaUnixStream::peer_credin the accept loop and drops any connection whose process UID ≠ the runtime's euid, before any handler is spawned.umask(0o077)aroundbindremoves the bind→chmod TOCTOU window; the socket path is scoped toAA_AGENT_ID.assembly.ipc.v1proto (HandshakeChallenge/HandshakeProof) + new wire tags (=5). On accept the runtime sends a fresh 32-byte nonce and requires the SDK's Ed25519 signature over it, verified against the key deterministically derived fromAA_AGENT_ID(reusingAgentKeypair). The handshake runs in the per-connection task (never blocks the accept loop), is bounded by a 5s timeout, and the response router / active-conn counter are wired only after a valid proof. The SDK completes the handshake before any heartbeat/event, fail-closed.Registernow requires an Ed25519possession_proofover the registering did:key, verified againstpublic_key, beforegenerate_credential_token(). Missing/forged/wrong-length →Unauthenticated. The SDK signs and sends it. Coordinates with AAASM-3416 (broad per-endpoint authn, still To Do) — this is the minimal credential_token possession gate that a future authn interceptor layers on top of, not a replacement.Type of Change
Breaking Changes
RegisterRequestgains a requiredpossession_prooffield (proto field 14): a gRPCRegistercall without a valid proof now returnsUnauthenticatedinstead of minting a token.aa-sdk-client::ipc::spawn_ipc_thread/ipc_loopnow take anagent_id: Stringto derive the handshake signing key. The cross-repo FFI shims (python/node/go SDKs) consumeaa-sdk-clientby pinned SHA and will adopt these on their own cadence.Related Issues
Testing
Local validation (CI verifies authoritatively):
cargo nextest run -p aa-sdk-client -p aa-runtime -p aa-gateway -p conformance→ 1575 passed, 2 skipped.AgentKeypair::sign.cargo clippy --all-targets --all-features -- -D warningsclean on touched crates;cargo fmt --checkclean;cargo build --workspaceclean.Platform note:
SO_PEERCREDis Linux-specific; the peer-UID read uses tokio's portablepeer_cred()(getpeereid on macOS/BSD), so the path compiles and runs on all Unix targets. Tests were authored and run on macOS; the exact UID-mismatch reject is best validated under Linux CI (a same-host different-UID peer is hard to stage in a unit test) — the helper that decides it is unit-tested directly.Checklist
cargo fmt,cargo clippy)🤖 Generated with Claude Code