--- id: handshake-tests name: Handshake-level test suite — pin, fail-closed, raw-key path executed (U-3) status: completed depends_on: [] scope: moderate risk: medium impact: component level: implementation tags: [tests, tcp, review-001, u3] --- ## Description Every existing test asserts config *construction*; the crate's actual TLS behavior (server accepts, pin verifies, fail-closed manifests) is never executed — no handshake-level test exists. These are the highest-value missing tests in the crate: they turn "fail closed" and "pin" from documentation into executed behavior. The ADR-006 boundary (handshakes belong to transport crates) is not violated by a duplex-pair handshake: no external transport dep, just tokio + tokio-rustls under the existing `tcp` feature (dev/test only — the feature gate already exists for `for_tcp_tls`). The four handshake-level gaps (review 001 §U-3): 1. **The pin path end-to-end**: client config with `FingerprintPinVerifier` handshaking a server whose presentation matches the pin → ok; mismatched pin → fails. 2. **The fail-closed path**: `remote_identity: None` + raw-key server → handshake fails (the structural claim, executed). 3. **The RFC 7250 raw-key server path end-to-end**: `RawKeyCertResolver` + client → handshake completes with the raw-key cert type negotiated (currently only `only_raw_public_keys()` is asserted). 4. **S-1's impersonation probe made permanent** — covered by `fix-accept-any-cert-verifier-posture` (kept separate: different remediation owner, doc+ADR+probe). ## Work 1. Add `tests/handshake_behavior.rs` (`#![cfg(feature = "tcp")]`): tokio duplex pair + `for_tcp_tls()` acceptor / `into_rustls_config()` connector — or raw `ServerConnection`/`ClientConnection` with `complete_io` (the review-probe shape; fewer moving parts). 2. Suite (1): server = `TlsIdentity::RawKey(sk)`, client pin = `fingerprint_from_cert_der(spki)` → handshake ok + app-data round-trip; wrong pin → error. 3. Suite (2): server = RawKey, credentials without `remote_identity` → handshake fails (`UnknownCertificateType`/alert — assert failure, not the specific error text unless rustls pins it). 4. Suite (3): raw-key server + raw-key client (RFC 7250 both sides): handshake ok; server `peer_certificates()` is an SPKI; client presented X509+RawPublicKey cert types per N-4's correction (the client resolver already offers the right shape — see N-4). 5. Keep every test within the crate's public API + rustls types; no new dependencies. ## Verification - [ ] All three executed handshake tests pass under `cargo test --features tcp` (and `--all-features`) - [ ] The fail-closed test actually fails the handshake (assert error, not success) - [ ] `cargo test` (default) still green — the file is feature-gated and contributes nothing without `tcp` - [ ] `cargo clippy --all-targets --all-features -- -D warnings`, `cargo fmt --check` green ## Acceptance Criteria - [ ] Pin / fail-closed / raw-key are executed behaviors in the suite, not just structural assertions - [ ] No new dependencies; the default build stays lean ## References - docs/reviews/001-implementation-review.md §U-3, §N-4 (the cert-type negotiation correction — read before writing suite (3)) - src/client.rs (`FingerprintPinVerifier`, `select_server_verifier`), src/server.rs (`RawKeyCertResolver`) - ADR-006 (the handshake scope boundary — and why the duplex-pair shape stays inside it) - tasks/integration-suite.md (the existing suite this extends) ## Postscript (2026-09-11 — the OQ-TLS-10 resolution) The open gap above is now **resolved by ADR-007** (deviation from alknet; OQ-TLS-10 → resolved). The two gap premises this task pinned are no longer true: - Suite 1's original premise (crate pin client ↔ raw-key server → ok) is now **true**: `FingerprintPinVerifier::requires_raw_public_keys()` derives from the pin format (`ed25519:` → `true`), so the crate's own pin client completes against the crate's raw-key server — `raw_key_server_path_completes_with_crate_pin_client` (no custom verifier needed anymore). - Suite 3b's premise is now **inverted**: a raw-key client identity presents its SPKI under the X.509 offer and `AcceptAnyCertVerifier` accepts it end-to-end (`raw_key_client_presents_spki_and_server_extracts_fingerprint`). - New pin: `ed25519_pin_against_x509_server_fails_closed_at_negotiation` — a pin-format/cert-kind mismatch aborts at negotiation, never a downgrade. Rationale and limits (incl. the strict-foreign-server caveat) in ADR-007; client.md/server.md notes flipped accordingly. ## Notes > **Major finding during implementation (recorded as OQ-TLS-10):** the > task's suite premises 1 and 3 did not hold as written. A temporary > probe (the review's methodology, deleted after the run) found: > > - **A crate-built pin client cannot complete a handshake against a > crate-built raw-key server** (HandshakeFailure). Mechanism, > verified in the rustls 0.23.44 sources > (`server/hs.rs::process_cert_type_extension`, > `client/hs.rs::process_cert_type_extension`): the raw-key server > resolver requires the client to offer > `server_certificate_types = [RawPublicKey]`, which rustls sends > only when the client verifier overrides > `requires_raw_public_keys() == true`. `FingerprintPinVerifier` > keeps the trait default `false` (as does > `AcceptAnyCertVerifier`). iroh's verifier overrides `true` on both > sides (`iroh/src/tls/verifier.rs`) — the working prior art. The > gap is inherited from alknet (no override there either) — > behavior-preserving, but the raw-key-over-TCP interop the > extraction implies does not exist yet. > - **Suite 3's premise adjusted**: the raw-key server path completes > only with an iroh-shaped verifier (`requires_raw_public_keys() > == true`); the suite pins that working shape (with > `NoClientCertResolver` client auth) and asserts the presented > SPKI carries the raw Ed25519 key. > - **Suite 1's premise adjusted**: the crate pin client ↔ raw-key > server shape was replaced by the pin client ↔ X.509 server > (`SHA256:` pin, which the crate's verifier can negotiate), pin > match + mismatch. The executed fail-closed (suite 2) is > mechanically the same HandshakeFailure (raw-key server needs a > raw-key offer the CA-path client never makes) — the alert is > asserted, the no-downgrade outcome is what's pinned. > - **N-4's trap executed as suite 3b**: raw-key client resolver ↔ > `AcceptAnyCertVerifier` fails (client offers > `[RawPublicKey]` → `IncorrectCertificateTypeExtension`). > > OQ-TLS-10 recorded (open, options a/b/c, deferral rationale); > client.md and server.md carry the interop notes. ## Summary **Landed as `tests/handshake_behavior.rs`** (`tcp`-gated, 5 suites; no new deps — tokio duplex + tokio-rustls under the existing feature): 1. `pin_match_completes_and_server_extracts_client_fingerprint` — X.509 server, client pins the matching `SHA256:` fingerprint → handshake completes, app data round-trips, server extracts the client cert's fingerprint (request-but-don't-require, executed). 2. `pin_mismatch_fails_the_handshake` — wrong pin → handshake error carrying the pin verifier's mismatch message. 3. `unknown_raw_key_remote_fails_closed` — `remote_identity: None` + raw-key server → HandshakeFailure (fail closed, executed). 4. `raw_key_server_path_completes_with_requires_raw_verifier` — the RFC 7250 server path end-to-end with the iroh-shaped client verifier: handshake completes, no client cert presented, and the presented server cert is verified to be the SPKI carrying the raw Ed25519 public key. 5. `raw_key_client_resolver_fails_against_accept_any_cert_verifier` — N-4's interop trap executed (raw-key client cert types rejected). Plus **OQ-TLS-10** (open — the cert-type negotiation gap above), server.md/client.md synced, frontmatter → completed. **Verification:** 81 default / 91 tcp / 99 all-features tests green (+5 new handshake suites), clippy `-D warnings` clean (default + all-features), fmt clean, `cargo doc` warning-free, `taskgraph validate` 14 tasks.