tests/handshake_behavior.rs (tcp-gated, tokio duplex + tokio-rustls, no new deps) turns the fail-closed / pin / raw-key language into executed behavior: - pin match: X.509 server + SHA256 pin -> handshake completes, app data round-trips, server extracts the client cert fingerprint - pin mismatch: wrong pin -> handshake error (the pin IS the anchor) - fail closed: remote_identity None + raw-key server -> HandshakeFailure - raw-key server path end-to-end: completes with the iroh-shaped client verifier (requires_raw_public_keys == true); presented cert asserted to be the SPKI carrying the raw Ed25519 key - N-4's interop trap executed: raw-key client resolver vs AcceptAnyCertVerifier -> IncorrectCertificateTypeExtension alert Major finding, recorded as OQ-TLS-10 (open): a crate-built pin client cannot reach a crate-built raw-key server over rustls TCP+TLS — the raw-key resolver requires the client to offer [RawPublicKey] server cert types, sent only when the client verifier overrides requires_raw_public_keys() == true. FingerprintPinVerifier keeps the trait default false (AcceptAnyCertVerifier too); iroh's verifier overrides true on both sides. Gap inherited from alknet (behavior-preserving); pinned both ways by the suite. client.md / server.md carry the interop notes; task file updated (premise adjustments documented in Notes, summary filled). Verification: 81 default / 91 tcp / 99 all-features tests green (+5 new), clippy -D warnings clean both configs, fmt clean, cargo doc warning-free, taskgraph validate 14 tasks.
7.0 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| handshake-tests | Handshake-level test suite — pin, fail-closed, raw-key path executed (U-3) | completed | moderate | medium | component | implementation |
|
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):
- The pin path end-to-end: client config with
FingerprintPinVerifierhandshaking a server whose presentation matches the pin → ok; mismatched pin → fails. - The fail-closed path:
remote_identity: None+ raw-key server → handshake fails (the structural claim, executed). - 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).
- client → handshake completes with the raw-key cert type negotiated
(currently only
- S-1's impersonation probe made permanent — covered by
fix-accept-any-cert-verifier-posture(kept separate: different remediation owner, doc+ADR+probe).
Work
- Add
tests/handshake_behavior.rs(#![cfg(feature = "tcp")]): tokio duplex pair +for_tcp_tls()acceptor /into_rustls_config()connector — or rawServerConnection/ClientConnectionwithcomplete_io(the review-probe shape; fewer moving parts). - Suite (1): server =
TlsIdentity::RawKey(sk), client pin =fingerprint_from_cert_der(spki)→ handshake ok + app-data round-trip; wrong pin → error. - Suite (2): server = RawKey, credentials without
remote_identity→ handshake fails (UnknownCertificateType/alert — assert failure, not the specific error text unless rustls pins it). - 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). - 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 withouttcpcargo clippy --all-targets --all-features -- -D warnings,cargo fmt --checkgreen
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)
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 offerserver_certificate_types = [RawPublicKey], which rustls sends only when the client verifier overridesrequires_raw_public_keys() == true.FingerprintPinVerifierkeeps the trait defaultfalse(as doesAcceptAnyCertVerifier). iroh's verifier overridestrueon 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 (withNoClientCertResolverclient 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 ↔
AcceptAnyCertVerifierfails (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):
pin_match_completes_and_server_extracts_client_fingerprint— X.509 server, client pins the matchingSHA256:fingerprint → handshake completes, app data round-trips, server extracts the client cert's fingerprint (request-but-don't-require, executed).pin_mismatch_fails_the_handshake— wrong pin → handshake error carrying the pin verifier's mismatch message.unknown_raw_key_remote_fails_closed—remote_identity: None+ raw-key server → HandshakeFailure (fail closed, executed).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.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.