Close review 001 §S-1: the default client-cert verifier never checked
the client's CertificateVerify, so anyone holding a peer's *public*
cert/SPKI bytes (public by design — peers publish them to be dialable)
could complete a handshake as that peer, and the auth layer could not
detect it. The consumer designs are known (X.509 and raw-key TCP/QUIC
endpoints with identity-bearing clients), so implementing now — the
zero-consumer moment — avoids the guaranteed breaking republish of
flipping the default later.
- VerifyPresentedCertVerifier (new): request, don't require, verify
possession — permissive verify_client_cert (self-signed chains and
bare SPKIs stay valid presentation) + CertificateVerify routing by
presented cert kind (Ed25519 SPKI -> verify_tls13_signature_with_
raw_key both TLS versions; X.509 -> standard route), the same
routing FingerprintPinVerifier implements. Nine-scheme list
verbatim (shared fn, exact-list pin covers both).
- Default on every TlsServerConfig path — X509 / RawKey / SelfSigned
/ ACME (the verifier install is crate-side rustls in new_acme, not
rustls-acme's).
- AcceptAnyCertVerifier stays public as the explicit no-pop escape
hatch, no longer installed by any crate path.
- tests/impersonation_posture.rs: four pins — default rejects the
attacker (X.509: UnsupportedSignatureAlgorithmForPublicKeyContext;
raw-key: BadSignature), escape hatch still accepts + extracts the
victim's fingerprint (both cert types).
- tests/handshake_behavior.rs: suites 4/4b — possession-checked legit
clients (raw-key pin vs raw-key server; X.509 client vs X.509
server) complete and the server extracts the fingerprint; suite 3b
doc updated.
- Docs: ADR-008 written; OQ-TLS-09 -> resolved (option (b));
ADR-007 §Limits deferral retired to not-planned; server.md/client.md
invariants/README/overview synced.
Verification: cargo test 81 / --features tcp 95 / --all-features 104
green; clippy -D warnings clean (default + all-features); fmt clean;
cargo doc warning-free.
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.
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.
- review 001: §Status carries the remediation status (S-1 + N-1 landed
in e86b8ba, OQ-TLS-09 open, probe permanent) and the N-4 correction
summary; §S-1 remediation-task line updated; §N-1 marked remediated;
§N-4 gains the corrected mechanism chain (rustls 0.23.41/0.23.44 send
[RawPublicKey] iff the resolver's only_raw_public_keys() is true —
no "offers both types" behavior exists; a raw-key client fails
IncorrectCertificateTypeExtension against this verifier; the S-1
probe presents the SPKI under the default X.509 offer instead)
- task fix-accept-any-cert-verifier-posture: drop the placeholder
fill-in lines (Notes/Summary carry the actual record)
- task docs-pin-c1-c4-n3-n4: N-4 work item rewritten from the
corrected chain so the implementing agent does not re-derive it;
references point at the rustls source lines and the probe's
resolver choice
- AcceptAnyCertVerifier doc: the presented CertificateVerify signature
is not verified (no proof-of-possession) — the server-extracted
fingerprint is attacker-suppliable from observed public cert/SPKI
bytes; states the two safe patterns (auth-layer challenge-response /
a verifying verifier) and points at OQ-TLS-09
- FingerprintPinVerifier doc (N-1): fixed the "stolen-but-stale
fingerprint" phrasing (the cert is presented fresh each handshake;
the signature check defeats a stolen/observed cert used by a party
without the private key) and added the server-verifier cross-reference
- OQ-TLS-09 recorded (open, high): which layer owns server-path
proof-of-possession — three options; deferral noted (needs the
auth-layer design or an API call before the first consumer)
- tests/impersonation_posture.rs (tcp-gated): the S-1 probe made
permanent, both variants — X.509 victim cert + attacker key and RFC
7250 victim SPKI + attacker key complete the handshake, application
data flows, and the server extracts the victim's fingerprint; any
future pop change must fail/update this test with the doc + OQ
- server.md / client.md synced with the same posture
- task note: the review's N-4 parenthetical ("alknet's client resolver
offers both types") is inaccurate — rustls 0.23.41/0.23.44 offer
[RawPublicKey] iff the resolver's only_raw_public_keys() is true;
task 6 should write N-4 from the rustls sources
Verified: cargo test 68 default / 77 all-features (+2) green; clippy
-D warnings clean (default + all-features); fmt clean; cargo doc
--no-deps warning-free
Checklist (all six PASS):
1. API surface == ADR-004 — every accessor signature verified verbatim
2. TlsError == ADR-002 — six variants, #[non_exhaustive], typed sources,
AcmeConfig holds exactly the two config-mismatch cases
3. Invariants: all five server invariants + client 0-RTT half +
fail-closed structure, each with a passing behavioral test at unit
and integration level
4. Deltas vs extraction: all ADR-pinned; two surfaced divergences
recorded as ADR amendments — zero un-pinned divergences remain
5. Feature hygiene: default = [] lean, tokio subset (no full),
doc comments on public API, no inline // comments, no panics
6. Docs sync: ADR-002 + ADR-003 amendment notes; overview/server/client
Draft → Reviewed; README carries the API-freeze lifecycle note
5 findings, all low severity, all resolved forward (table in task Notes)
Verification: cargo test (81), cargo test --all-features (92),
clippy -D warnings, fmt --check, doc --no-deps,
publish --dry-run — all green. API FROZEN for the alknet rewrite.
port-identity-types (src/identity.rs):
- TlsIdentity (four variants), Ed25519SecretKey, AcmeDirectory ported
verbatim from alknet-core config.rs; OQ-TLS-02 + server-only docs
on the variants; 9 in-module tests incl. Debug-no-leak
- dep decision: rand_core 0.6 (+getrandom) with ed25519-dalek
rand_core feature, NOT rand (lockfile rand 0.10/rand_core 0.10
traits are incompatible with ed25519-dalek 2.2's CryptoRngCore);
rand stays out of the tree entirely
port-fingerprint (src/fingerprint.rs):
- fingerprint_from_cert_der, extract_ed25519_raw_key_from_spki,
DerParser ported verbatim; production code sha2 + manual DER
(+hex for the normalized formats)
- 16 tests: 7 ported + 9 new malformed-DER edges (the extraction
had none despite the invariant naming them)
- empty-input behavior: matches extraction's actual code (always
Some via the SHA-256 fallback); doc records the deviation from
the stale None claim
port-pem-signing (src/pem.rs, src/signing.rs):
- load_cert_chain/load_private_key remapped to TlsError::CertLoad
per ADR-002; InvalidData no-key path kept
- Ed25519SigningKey rewired to crate::identity::Ed25519SecretKey
(the one intentional change); rcgen PEM round-trip test added
lib.rs re-export block: fingerprint + pem + signing + identity lines
landed; server/client/credentials pending their port tasks
Verification: cargo test (36), cargo test --all-features (37),
clippy -D warnings (default+all-features), fmt --check, feature
checks (noq/tcp/acme) — all green
- crate-init: skeleton, feature gates (ADR-003), TlsError (ADR-002)
- three parallel foundation ports: identity types, fingerprint, pem+signing
- port-server / port-client in parallel (each consumes the foundation;
client also carries credentials.rs per ADR-005 co-location)
- integration-suite: invariant pins + feature-matrix + seam round-trips
- review-impl: spec-conformance gate — the API-freeze point before the
alknet rewrite consumes the crate
- validated: taskgraph topo (5 generations), no cycles, critical path
len 5, risk-path 1.45; medium risk concentrated on the two broad
ports + the suite (each a diff against a fixed extraction source,
not open-ended work)
- rust-version 1.85 -> 1.88 (ecosystem-wide resolution from the audit
sessions; matches noq 1.2's floor, sits below iroh 1.91)
- Cargo.lock: time 0.3.36 pin removed (existed only to keep the 1.85
claim satisfiable); tree re-floated to current
- phase-0: MSRV thread marked RESOLVED; OQ-TLS-08 narrowed to the
quinn->noq feature-rename half; lesson recorded (rust-version is
passive metadata — claims must be compile-checked, not assumed)
Verified: cargo test, test --all-features, clippy -D warnings,
fmt --check, rustup run 1.88 cargo check