Files
alktls/tasks/handshake-tests.md
T
glm-5.3-flash 4a4fae64af task 2: handshake-level suites — pin, fail-closed, RFC 7250 paths executed (U-3)
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.
2026-09-11 08:17:28 +00:00

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
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)

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_closedremote_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.