ADR-008: server-path possession verification — the verifying verifier is the default (OQ-TLS-09 resolved)
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.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
---
|
||||
status: accepted
|
||||
last_updated: 2026-09-11
|
||||
---
|
||||
|
||||
# ADR-008: Server-path proof-of-possession — the verifying verifier is the default
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-09-11). Resolves OQ-TLS-09.
|
||||
|
||||
## Context
|
||||
|
||||
Review 001 §S-1 (executed): `AcceptAnyCertVerifier` — then the default
|
||||
client-cert verifier on every `TlsServerConfig` path — never checked
|
||||
the client's CertificateVerify signature. Any party that observes a
|
||||
peer's public cert bytes (X.509) or SPKI (RFC 7250) could complete a
|
||||
handshake *as that peer*: the handshake completed, `peer_certificates()`
|
||||
yielded the victim's cert, and the extracted fingerprint — the
|
||||
identity every downstream auth decision consumes — was the victim's.
|
||||
The auth layer could not detect the spoofing: the fingerprint it was
|
||||
handed *was* the victim's.
|
||||
|
||||
The public cert/SPKI bytes are public **by design** — a peer must
|
||||
publish its identity to be dialable (branch-2 endpoints publish the
|
||||
same Ed25519 public key an SSH server would). So the observed-by-anyone
|
||||
attack surface was every peer identity in the stack, and the only
|
||||
proposed mitigation (challenge-response owned by the auth layer, the
|
||||
rewrite's option (a)) did not exist anywhere in the alk* codebase. The
|
||||
spoil was fully known and the fix fully specified — mirroring the
|
||||
signature routing the client-side `FingerprintPinVerifier` already
|
||||
implements — so "wait for a consumer" (the OQ's deferral framing)
|
||||
repeated the circular reasoning OQ-TLS-10 fell into: the component
|
||||
that must *enable* the consumer was deferring to the consumer it
|
||||
enables. The consumer designs are known (TCP/QUIC servers with X.509
|
||||
or raw-key identities and identity-bearing clients — alkhttp,
|
||||
webtransport, the rewrite's endpoints); possession verification is
|
||||
needed by every one of them that authenticates by fingerprint.
|
||||
|
||||
## Decision
|
||||
|
||||
`VerifyPresentedCertVerifier` is the **default** client-cert verifier
|
||||
on every `TlsServerConfig` path — X509, RawKey, SelfSigned, and ACME
|
||||
alike (the verifier install is crate-side rustls in `new_acme`, not
|
||||
rustls-acme's, so the ACME path flips identically). Its posture:
|
||||
**request, don't require, verify possession.**
|
||||
|
||||
- `offer_client_auth() == true`, `client_auth_mandatory() == false`,
|
||||
`root_hint_subjects() == &[]` — identical request-not-require shape
|
||||
to `AcceptAnyCertVerifier` (no-cert clients unaffected).
|
||||
- `verify_client_cert` accepts any presented bytes — possession is
|
||||
proven by the CertificateVerify, not the cert's provenance;
|
||||
self-signed X.509 chains and bare RFC 7250 SPKIs remain valid
|
||||
presentation.
|
||||
- `verify_tls13_signature` / `verify_tls12_signature` route by
|
||||
presented cert kind: an Ed25519 SPKI goes through
|
||||
`verify_tls13_signature_with_raw_key` (both TLS versions), an X.509
|
||||
chain through the standard `verify_tls{12,13}_signature` — the same
|
||||
routing the client-side `FingerprintPinVerifier` implements, and the
|
||||
same functions rustls' webpki verifiers use.
|
||||
- `supported_verify_schemes()` returns the nine-scheme list verbatim
|
||||
(shared with `AcceptAnyCertVerifier`; the exact-list pin covers
|
||||
both).
|
||||
- `requires_raw_public_keys()` stays `false` — both cert types
|
||||
negotiate (ADR-007).
|
||||
|
||||
`AcceptAnyCertVerifier` stays public as the explicit no-pop escape
|
||||
hatch for a deployment that deliberately wants the old posture
|
||||
(handshake-speed over strictness, an auth layer that owns
|
||||
challenge-response). It is no longer installed by any crate path.
|
||||
|
||||
Mechanism note (rustls 0.23.44, `server/tls13.rs`): when a client
|
||||
presents a cert, rustls calls `verify_client_cert` then
|
||||
`verify_tls13_signature(construct_client_verify_message(..), cert[0],
|
||||
sig)` — the signature covers the transcript hash bound to the
|
||||
`"TLS 1.3, client CertificateVerify"` constant, so the check is a real
|
||||
proof-of-possession of the presented public key, not a replayable
|
||||
blob. A client presenting no cert skips both (the `!mandatory`
|
||||
branch). The attacker's failure mode is a signature under the
|
||||
attacker's key against the victim's public key: `BadSignature`
|
||||
(raw-key) or `UnsupportedSignatureAlgorithmForPublicKeyContext`
|
||||
(X.509 kind mismatch) — pinned by `tests/impersonation_posture.rs`.
|
||||
|
||||
## What this buys — and what it deliberately does not
|
||||
|
||||
**Bought:** the extracted fingerprint is now authenticated as *"the
|
||||
connecting party holds the private key for the presented public
|
||||
identity."* The impersonation attack (victim's public bytes + attacker
|
||||
signer) fails the handshake on every default path, both cert types.
|
||||
This is the S-1 resolution for branches 1 and 2 (X.509 and raw-key
|
||||
TCP/QUIC endpoints with identity-bearing clients).
|
||||
|
||||
**Not bought (deliberately, unchanged):** no CA verification and no
|
||||
name verification — a self-signed chain remains valid presentation,
|
||||
and who a fingerprint maps to (the peer table, scopes, tokens) remains
|
||||
the auth layer's concern (ADR-005). The verifier proves *possession*;
|
||||
the auth layer still decides *trust*. This is exactly the split the
|
||||
crate's scope boundary prescribes (ADR-002).
|
||||
|
||||
## Behavior-preservation invariants under the change
|
||||
|
||||
- The nine-scheme list, request-not-require shape, non-empty root
|
||||
store, 0-RTT, provider, and ALPN handling are untouched.
|
||||
- Fail-closed: unchanged — and the new default adds a rejection where
|
||||
there was silent acceptance (the spoof), the direction that never
|
||||
breaks a legitimate composition.
|
||||
- Legit compositions verified end-to-end (duplex handshakes,
|
||||
`tests/handshake_behavior.rs`): raw-key client pin ↔ raw-key server
|
||||
(suite 4), X.509 client ↔ X.509 server (suite 4b), raw-key client ↔
|
||||
X.509 server (suite 3b) — all extract the client fingerprint under
|
||||
the verifying default. No-cert clients (browsers) unaffected.
|
||||
- QUIC parity: the CertificateVerify mechanism is handshake-level,
|
||||
identical under `for_noq()`.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- S-1 closed out of the box: no deployment can accidentally run the
|
||||
spoofable posture; the escape hatch must be explicitly installed and
|
||||
its posture is test-pinned.
|
||||
- The auth layer's fingerprint resolution can now treat the fingerprint
|
||||
as a possession-authenticated claim — the option-(c)
|
||||
challenge-response design becomes optional defense-in-depth rather
|
||||
than a prerequisite.
|
||||
- Zero-consumer moment used correctly: flipping the default *after*
|
||||
first consumers would be a guaranteed breaking republish.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- A legitimate client whose signer does not match its presented cert
|
||||
(a misconfigured `CertifiedKey`) now fails the handshake where it
|
||||
previously succeeded silently — the failure is the correct diagnosis
|
||||
(there is no such legitimate composition).
|
||||
- One more public type on the API surface (additive; the freeze allows
|
||||
additions).
|
||||
|
||||
## References
|
||||
|
||||
- OQ-TLS-09 (`docs/architecture/open-questions.md`) — resolved by
|
||||
this ADR
|
||||
- `tests/impersonation_posture.rs` — both postures pinned (default
|
||||
rejects, escape hatch accepts), both cert types
|
||||
- `tests/handshake_behavior.rs` — legit-composition pins (suites 1,
|
||||
3b, 4, 4b)
|
||||
- review 001 §S-1 — the discovery probe
|
||||
- ADR-007 (cert-type negotiation), ADR-002 (scope boundary), ADR-005
|
||||
(auth layer owns peer resolution)
|
||||
- alknet ADR-034 — the request-but-don't-require shape this ADR
|
||||
refines (deviation recorded per AGENTS.md convention 10)
|
||||
- rustls 0.23.44 `server/tls13.rs` (the CertificateVerify flow),
|
||||
`webpki/verify.rs` (`verify_tls13_signature_with_raw_key`)
|
||||
Reference in New Issue
Block a user