feat(server): mTLS client-cert agent identity, remove agentcore.c handshake (VS-63) #86

Merged
David merged 1 commit from fix/VS-63-remove-agent-handshake into main 2026-06-28 01:51:50 +02:00
Owner

Completes the last open acceptance criterion of VS-63 (AC5: remove the agentcore.c handshake). ACs 1-4 (agent CA, enrollment token, CSR-signing endpoint) shipped in #83; AC5 was deferred because it was coupled to the agent mTLS listener, which has since landed in VS-62 (#84). This makes the verified mTLS client cert the sole agent identity and deletes the handshake.

What changed

agent_identity::AgentCertAcceptor (new) wraps the rustls acceptor on the agent listener: after the TLS handshake it reads the verified leaf certificate off the negotiated connection and injects it into request extensions as AgentClientCert. The listener's WebPkiClientVerifier (VS-62) already guarantees that leaf chains to the dedicated agent CA; axum-server's default connect-info only plumbs the peer SocketAddr, so this wrapper is what surfaces the cert to the handler.

agent_ws_handler derives the node id from the leaf CommonName (the value the agent CA stamps from the CSR subject, per AgentCa::sign_csr) and rejects any agent connection that arrives without a verified client cert.

agent::handle_session no longer runs a handshake. Identity comes from the cert; the agent's first frame is AuthInfo, which now carries registration metadata only (group id, capabilities, hostname, platform, version) since the connection is already authenticated by mTLS. On AuthInfo the node is registered and marked online. Subsequent post-auth frames dispatch exactly as before.

agent_handshake.rs is deleted (the AuthRequest / AuthVerify / ServerId / ServerIdHint server handling plus its 997 lines of tests). The wire structs stay in vervain-agent-proto until the agent half (VA-67) drops them.

Contract for VA-67

The post-mTLS registration contract the agent must speak: connect over mTLS with the enrolled client cert (CN = node//<hash>), then send a single AuthInfo frame. No challenge-response. This unblocks VA-67, which was correctly blocked on this server half being undelivered.

Tests

  • agent_identity unit tests: CommonName extraction for valid, empty, and garbage certs.
  • New end-to-end agent_mtls test: drives a real WebSocket over mTLS with an agent-CA-issued client cert, sends only AuthInfo, and asserts the node row is registered under the cert's CN with the reported group id.
  • Full just pre-commit green (fmt, clippy -D warnings, build, tests).

Follow-up

The now-inert settings.tls.insecureSkipHashCheck config knob and the agent_skip_hash_check fields were left in place to keep this change bounded; their removal (plus the orphaned vervain_agent_handshake_failed_total metric) is tracked in VS-66.

🤖 Generated with Claude Code

Completes the last open acceptance criterion of VS-63 (AC5: remove the `agentcore.c` handshake). ACs 1-4 (agent CA, enrollment token, CSR-signing endpoint) shipped in #83; AC5 was deferred because it was coupled to the agent mTLS listener, which has since landed in VS-62 (#84). This makes the verified mTLS client cert the sole agent identity and deletes the handshake. ## What changed `agent_identity::AgentCertAcceptor` (new) wraps the rustls acceptor on the agent listener: after the TLS handshake it reads the verified leaf certificate off the negotiated connection and injects it into request extensions as `AgentClientCert`. The listener's `WebPkiClientVerifier` (VS-62) already guarantees that leaf chains to the dedicated agent CA; axum-server's default connect-info only plumbs the peer `SocketAddr`, so this wrapper is what surfaces the cert to the handler. `agent_ws_handler` derives the node id from the leaf CommonName (the value the agent CA stamps from the CSR subject, per `AgentCa::sign_csr`) and rejects any agent connection that arrives without a verified client cert. `agent::handle_session` no longer runs a handshake. Identity comes from the cert; the agent's first frame is `AuthInfo`, which now carries registration metadata only (group id, capabilities, hostname, platform, version) since the connection is already authenticated by mTLS. On `AuthInfo` the node is registered and marked online. Subsequent post-auth frames dispatch exactly as before. `agent_handshake.rs` is deleted (the `AuthRequest` / `AuthVerify` / `ServerId` / `ServerIdHint` server handling plus its 997 lines of tests). The wire structs stay in `vervain-agent-proto` until the agent half (VA-67) drops them. ## Contract for VA-67 The post-mTLS registration contract the agent must speak: connect over mTLS with the enrolled client cert (CN = `node//<hash>`), then send a single `AuthInfo` frame. No challenge-response. This unblocks VA-67, which was correctly blocked on this server half being undelivered. ## Tests - `agent_identity` unit tests: CommonName extraction for valid, empty, and garbage certs. - New end-to-end `agent_mtls` test: drives a real WebSocket over mTLS with an agent-CA-issued client cert, sends only `AuthInfo`, and asserts the node row is registered under the cert's CN with the reported group id. - Full `just pre-commit` green (fmt, clippy `-D warnings`, build, tests). ## Follow-up The now-inert `settings.tls.insecureSkipHashCheck` config knob and the `agent_skip_hash_check` fields were left in place to keep this change bounded; their removal (plus the orphaned `vervain_agent_handshake_failed_total` metric) is tracked in VS-66. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(server): mTLS client-cert agent identity, remove agentcore.c handshake (VS-63 AC5)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m31s
Create release / Create release from merged PR (pull_request) Has been skipped
28ad8930e5
Completes VS-63's last acceptance criterion: with VS-62's dedicated agent mTLS listener now landed, agent identity is the verified client certificate and the `agentcore.c`-derived challenge-response handshake is gone.

The agent listener's `WebPkiClientVerifier` already proves every client cert chains to the dedicated agent CA, but axum-server's default connect-info plumbs only the peer `SocketAddr`. New `agent_identity::AgentCertAcceptor` wraps the rustls acceptor, reads the verified leaf off the negotiated connection, and injects it into request extensions as `AgentClientCert`; `agent_ws_handler` extracts the node id from the leaf CommonName (the value the agent CA stamps from the CSR subject) and rejects any agent connection without a verified cert.

`agent::handle_session` no longer drives a handshake. Identity arrives from the cert; the agent's first frame is `AuthInfo`, which now carries registration metadata only (group id, capabilities, hostname, platform, version) since the connection is already authenticated by mTLS. On `AuthInfo` the node is registered and marked online; subsequent frames dispatch unchanged.

Deletes `agent_handshake.rs` (`AuthRequest` / `AuthVerify` / `ServerId` / `ServerIdHint` server handling and its 997 lines of tests). The proto wire structs stay in `vervain-agent-proto` until the agent half (VA-67) drops them.

Tests: `agent_identity` unit tests cover CN extraction (valid, empty, garbage). A new end-to-end `agent_mtls` test drives a real WebSocket over mTLS with an agent-CA-issued client cert, sends only `AuthInfo`, and asserts the node row is registered under the cert's CN and the reported group id.

The now-inert `settings.tls.insecureSkipHashCheck` knob and `agent_skip_hash_check` fields are left in place to bound this change; their removal is tracked in VS-66.

#VS-63

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
David merged commit 740e56dddb into main 2026-06-28 01:51:50 +02:00
David deleted branch fix/VS-63-remove-agent-handshake 2026-06-28 01:51:51 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!86
No description provided.