review-001 decomposition: 6 remediation tasks from verified findings
Verify every review-001 finding before decomposition: - S-1 re-confirmed by fresh executable probe (X.509 + raw-key impersonation both complete the handshake with the victim's fingerprint extracted server-side) - U-2/U-3 uncovered-line inventory re-derived from cargo llvm-cov (--all-features); matches Part C exactly - rcgen 1975/4096 defaults, rustls cert-type negotiation arm, packaging list, doc texts: all verified against sources Tasks (all verified, none speculative): - fix-accept-any-cert-verifier-posture (S-1 + N-1 + OQ-TLS-09 + probe) - handshake-tests (U-3 suites 1-3; suite 4 lives in the S-1 task) - coverage-cheap-closes (U-2's seven groups) - acme-event-loop-test (U-1; depends on coverage-cheap-closes for the tracing-capture pattern) - config-validation-and-trivia (C-2 ALPN dedup, C-3 empty-domains, N-6 excludes, N-7 https doc line) - docs-pin-c1-c4-n3-n4 (C-1, C-4, N-3 decision note, N-4 negotiation note) Graph: acme-event-loop-test is generation 2; the rest run in generation 1. taskgraph validate: 14 tasks, no cycles. Verification: cargo test (default + --all-features) green before and after; probe file deleted after its run.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
---
|
||||
id: fix-accept-any-cert-verifier-posture
|
||||
name: S-1 remediation — AcceptAnyCertVerifier no-pop posture (doc + ADR/OQ + permanent probe)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: project
|
||||
level: implementation
|
||||
tags: [security, docs, review-001, s1]
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `AcceptAnyCertVerifier` (src/server.rs:257-318) never checks the
|
||||
client's CertificateVerify signature — no proof-of-possession — so any
|
||||
party that sees a peer's public cert/SPKI can complete a handshake *as
|
||||
that peer*, and the server hands the auth layer the victim's fingerprint.
|
||||
Confirmed by executable probe during review 001 and **re-verified by a
|
||||
fresh probe in the 001-decomposition session** (both X.509 and RFC 7250
|
||||
raw-key variants complete the handshake with the victim's cert + the
|
||||
attacker's signing key; `peer_certificates()` yields the victim's cert
|
||||
and the server-extracted fingerprint equals the victim's).
|
||||
|
||||
The verifier behavior itself is the alknet-inherited design (alknet
|
||||
ADR-034) and is behavior-preserving — this is a **documentation/posture**
|
||||
finding, not a code-change mandate. What's missing:
|
||||
|
||||
1. The load-bearing doc on `AcceptAnyCertVerifier` (src/server.rs:242-256)
|
||||
states "does not verify the presented cert against a CA" but nowhere
|
||||
states the signature is unverified. To a consumer that reads as the
|
||||
*only* gap, when the actual consequence is: **presented identity is
|
||||
spoofable by anyone with the public bytes**.
|
||||
2. No ADR/OQ records which layer owns proof-of-possession on the server
|
||||
path.
|
||||
3. The spoofable posture has no permanent test pinning it — a refactor
|
||||
that *adds* pop (or upstream changes the verifier contract) would
|
||||
land green, silently changing the documented behavior.
|
||||
|
||||
## Work
|
||||
|
||||
1. **Doc on `AcceptAnyCertVerifier`**: state explicitly that the
|
||||
client's CertificateVerify signature is not verified (no
|
||||
proof-of-possession), that consequently the fingerprint the server
|
||||
extracts is attacker-suppliable from observed peer fingerprints, and
|
||||
point at the two safe patterns the auth layer can own:
|
||||
- challenge-response over the established channel bound to the
|
||||
cert's public key, or
|
||||
- a server-side verifier that *does* verify the CertificateVerify
|
||||
(the same routing the client-side `FingerprintPinVerifier`
|
||||
implements — the crate has the code, just not on this type).
|
||||
2. **N-1 companion line on `FingerprintPinVerifier`**
|
||||
(src/client.rs:232-236): fix the "stolen-but-stale fingerprint"
|
||||
phrasing (the cert is presented fresh each time — the real threat is
|
||||
a stolen *private key*) and add the cross-reference: this verifier
|
||||
checks proof-of-possession; the server-side `AcceptAnyCertVerifier`
|
||||
does not.
|
||||
3. **Record OQ-TLS-09** in `docs/architecture/open-questions.md`:
|
||||
which layer owns proof-of-possession on the server path? Options:
|
||||
(a) keep request-but-don't-require + auth-layer
|
||||
challenge-response; (b) additive `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)`);
|
||||
(c) make the auth layer's fingerprint resolution possession-checked.
|
||||
Until decided, the S-1 doc note is the honest description. If
|
||||
option (b) is chosen, it must land before the first consumer.
|
||||
4. **Permanent behavior-pin test** (tests/, tcp-feature-gated): the
|
||||
impersonation probe as a documented test — server config via
|
||||
`build_rustls_server_config`, a client presenting the victim's cert
|
||||
bytes with an attacker signer completes the handshake and
|
||||
`peer_certificates()` yields the victim's fingerprint. Assert the
|
||||
spoofable posture in both directions (X.509 + raw-key SPKI). If a
|
||||
future change adds pop, this test fails and forces the doc/ADR
|
||||
update. Probe shape: rustls `ServerConnection`/`ClientConnection`
|
||||
pair over a duplex, `complete_io` on each — the
|
||||
001-decomposition session's probe (deleted after its run) is the
|
||||
reference; a client-side test-local accept-anything verifier keeps
|
||||
the probe independent of the pin path.
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] The doc note exists on `AcceptAnyCertVerifier` and states the
|
||||
no-pop consequence in one reading
|
||||
- [ ] The N-1 companion line exists on `FingerprintPinVerifier`
|
||||
- [ ] OQ-TLS-09 recorded with status `open` and the three options
|
||||
- [ ] The permanent probe test passes and asserts the spoofable
|
||||
fingerprint end-to-end (both cert types)
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check`, `cargo doc --no-deps` green
|
||||
- [ ] `cargo test --all-features` green (the probe is tcp-gated)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A consumer reading only the rustdoc understands that presented
|
||||
identity is spoofable without caller-enforced possession
|
||||
- [ ] The spoofable posture is pinned by an executed test, not just
|
||||
prose
|
||||
- [ ] The ownership question is tracked (OQ-TLS-09) — not silently
|
||||
inherited
|
||||
|
||||
## References
|
||||
|
||||
- docs/reviews/001-implementation-review.md §S-1 (the finding) and §N-1
|
||||
- src/server.rs:242-318 (`AcceptAnyCertVerifier`), src/client.rs:225-323
|
||||
(`FingerprintPinVerifier` — the pop-complete routing to mirror)
|
||||
- alknet ADR-034 (the inherited request-but-don't-require shape)
|
||||
- docs/architecture/open-questions.md (where OQ-TLS-09 lands)
|
||||
|
||||
## Notes
|
||||
|
||||
> Agent fills this during implementation.
|
||||
|
||||
## Summary
|
||||
|
||||
> Agent fills this on completion.
|
||||
Reference in New Issue
Block a user