Files
alktls/tasks/handshake-tests.md
T
glm-5.3-flash 49d4432247 ADR-007: RFC 7250 cert-type negotiation — the offer follows the identity (OQ-TLS-10 resolved)
Resolve the cert-type negotiation gap (review 001 §U-3, OQ-TLS-10) by
deviation from alknet: the gap was a defect in the prior art (alknet's
code never delivered its spec's raw-key-over-TCP promise — ADR-082
"works for both QUIC and TCP+TLS"), not behavior to preserve.

- FingerprintPinVerifier::requires_raw_public_keys() derives from the
  pin format: ed25519: -> true (offer [RawPublicKey]), SHA256: ->
  false (X.509 offer). Crate pin client now completes against the
  crate raw-key server; SHA256: pins negotiate unchanged.
- RawKeyClientCertResolver presents the SPKI under the X.509 offer
  (only_raw_public_keys() == false): a raw-only client offer can only
  negotiate against a requires_raw server verifier, and
  AcceptAnyCertVerifier correctly stays on the default (accepts both
  cert types). The server extracts the ed25519: fingerprint from the
  SPKI bytes either way.
- Fail-closed preserved and strengthened: an ed25519: pin against an
  X.509 server now aborts at negotiation (suite 2b), never a
  downgrade; no API change (no public signature affected; the fix is
  invisible to consumers apart from working handshakes).
- tests/handshake_behavior.rs: suite 3 now runs crate-native (no
  custom iroh-shaped verifier), new negotiation fail-closed suite,
  suite 3b inverted to end-to-end success; invariant_pins.rs
  resolver-offer assertions flipped; unused imports dropped.
- Docs: ADR-007 written; OQ-TLS-10 -> resolved-by-deviation;
  client.md/server.md/overview/README/task postscript synced
  (incl. the strict-foreign-server limit in ADR-007 §Limits).

Verification: cargo test 81 / --features tcp 94 / --all-features
105 green; clippy -D warnings clean (default + all-features); fmt
clean; cargo doc warning-free.
2026-09-11 10:26:28 +00:00

175 lines
8.1 KiB
Markdown

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