task 1: S-1 remediation — no-pop posture doc + OQ-TLS-09 + permanent impersonation pin

- 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
This commit is contained in:
2026-09-10 22:00:57 +00:00
parent d7db6b17a6
commit e86b8ba1b5
7 changed files with 414 additions and 21 deletions
+6 -3
View File
@@ -89,9 +89,12 @@ verification — only the pin.
Handshake signatures are still verified (TLS 1.2/1.3, aws-lc-rs
algorithms; Ed25519 SPKI certs route through
`verify_tls13_signature_with_raw_key`): a stolen fingerprint cannot
be replayed with a forged signature — the presenter must prove
possession of the corresponding private key.
`verify_tls13_signature_with_raw_key`): the presenter must prove
possession of the corresponding private key, so a stolen or observed
certificate cannot be used by a party that does not hold the matching
key. This verifier checks proof-of-possession; the server-side
`AcceptAnyCertVerifier` does not (see
[server.md](server.md), OQ-TLS-09).
## The root-store fallback (alknet ADR-088 §5)
+42 -2
View File
@@ -21,6 +21,7 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
| OQ-TLS-06 | ACME task shutdown surface | resolved (detached-only for v1) | low |
| OQ-TLS-07 | iroh key surface | **resolved** (ADR-005, byte access pinned) | low |
| OQ-TLS-08 | `quinn``noq` feature rename | **resolved** (ADR-003) | high |
| OQ-TLS-09 | Server-path proof-of-possession | **open** | high |
## Identity & types
@@ -104,6 +105,44 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
[ADR-003](decisions/003-noq-replaces-quinn.md).
- **Cross-references**: ADR-002, ADR-003, ADR-004
### OQ-TLS-09: Which layer owns proof-of-possession on the server path?
- **Origin**: docs/reviews/001-implementation-review.md §S-1 (the
impersonation probe: a handshake with the victim's cert bytes + an
attacker signer completes, and the server extracts the victim's
fingerprint)
- **Status**: open (recorded 2026-09-10)
- **Priority**: high
- **Question**: `AcceptAnyCertVerifier` never checks the client's
CertificateVerify signature — the presented identity is spoofable by
anyone holding the public cert/SPKI bytes, and the auth layer cannot
detect it (the fingerprint it is handed *is* the victim's). alknet
ADR-034 inherited the request-but-don't-require shape without
recording this property. Who enforces possession?
- **Options**:
- **(a) Keep request-but-don't-require; the auth layer owns
challenge-response** over the established channel, bound to the
presented public key. No crate change; the S-1 doc note on
`AcceptAnyCertVerifier` is the honest description until then.
- **(b) Add a `VerifyPresentedCertVerifier` sibling**
(request-and-verify): same nine-scheme list, permissive
`verify_client_cert`, signature methods delegate to
`rustls::crypto::verify_tls{12,13}_signature(_with_raw_key)` — the
routing the client-side `FingerprintPinVerifier` already
implements. Additive; must land before the first consumer (an API
shape decision).
- **(c) Make the auth layer's fingerprint resolution
possession-checked** (a possession proof accompanies each
fingerprint resolution). Cross-crate; the auth layer does not exist
yet.
- **Constraints**: the spoofable posture is pinned by
`tests/impersonation_posture.rs` (both cert types) — any decision
must fail or update that test together with the
`AcceptAnyCertVerifier` doc note.
- **Cross-references**: src/server.rs (`AcceptAnyCertVerifier`),
src/client.rs (`FingerprintPinVerifier`),
docs/reviews/001-implementation-review.md §S-1, alknet ADR-034
## Quality / process
### OQ-TLS-05: Test surface for the invariants
@@ -133,5 +172,6 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
## Deferred / Blocked
(none — all promoted OQs are resolved; new OQs added during review
land here with their deferral half per `docs/sdd_process.md`)
- OQ-TLS-09 (server-path proof-of-possession): open by design — the
decision needs the rewrite's auth-layer design in hand (option (c))
or an API-shape call before the first consumer (option (b)).
+19 -1
View File
@@ -79,9 +79,27 @@ fingerprint is matched against peer records by the auth layer
the TLS crate hands over the fingerprint string; peer resolution is
not a TLS concern (ADR-005).
**The presented signature is not verified (no proof-of-possession)**
review 001's S-1: the client's CertificateVerify signature is never
checked against the presented cert's public key, so any party holding
a peer's public cert bytes (X.509) or SPKI (RFC 7250) can complete a
handshake *as that peer*, and the server hands the auth layer the
victim's fingerprint. The auth layer cannot detect this — the
fingerprint it is handed *is* the victim's. Until the caller enforces
possession, treat the extracted fingerprint as an unauthenticated
claim, not proof of identity. Two patterns make it safe (the auth
layer owns either): challenge-response over the established channel
bound to the presented public key, or a verifier that verifies the
CertificateVerify (the routing `FingerprintPinVerifier` implements —
tracked as OQ-TLS-09; option (b) there is additive but must land
before the first consumer). The spoofable posture is pinned by
`tests/impersonation_posture.rs` (both cert types) — a change here
must fail or update that test together with this doc.
Server-side only: this must not be reused as a client-side
`ServerCertVerifier` — client-side verification is alknet ADR-034's
selection matrix (see [client.md](client.md)).
selection matrix (see [client.md](client.md)), and unlike the
client-side pin verifier this type has no proof-of-possession check.
## `RawKeyCertResolver`