- 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
8.2 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fix-accept-any-cert-verifier-posture | S-1 remediation — AcceptAnyCertVerifier no-pop posture (doc + ADR/OQ + permanent probe) | completed | narrow | medium | project | implementation |
|
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:
- 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. - No ADR/OQ records which layer owns proof-of-possession on the server path.
- 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
- 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
FingerprintPinVerifierimplements — the crate has the code, just not on this type).
- 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-sideAcceptAnyCertVerifierdoes not. - 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) additiveVerifyPresentedCertVerifiersibling (request-and-verify — same nine-scheme list, permissiveverify_client_cert, signature methods delegate torustls::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. - 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 andpeer_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: rustlsServerConnection/ClientConnectionpair over a duplex,complete_ioon 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
AcceptAnyCertVerifierand states the no-pop consequence in one reading - The N-1 companion line exists on
FingerprintPinVerifier - OQ-TLS-09 recorded with status
openand 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-depsgreencargo test --all-featuresgreen (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
- The probe used a tokio duplex pair +
TlsConnector/TlsAcceptor(tokio-rustls,tcp-gated) rather than rawcomplete_io— same shape, cleaner await-based flow. The attacker client uses a test-local accept-anythingServerCertVerifierand a fixedResolvesClientCertpresenting the victim's bytes with the attacker's signer,only_raw_public_keys() == falsein both variants (the cert-type negotiation stays X.509-typed, exactly the review-probe shape; verified against rustls 0.23.44'svalidate_client_cert_type_extension— an offer of only[RawPublicKey]would failIncorrectCertificateTypeExtensionagainstrequires_raw_public_keys() == false, so the SPKI is presented under the default X.509 offer and passes through unparsed). - N-4 (the
requires_raw_public_keys()doc note) is deliberately NOT in this task — it belongs todocs-pin-c1-c4-n3-n4. While verifying the probe shape, the review's N-4 parenthetical ("alknet's own client resolver offers both types") was found inaccurate: rustls 0.23.41 AND 0.23.44 sendclient_certificate_types = [RawPublicKey]whenever the resolver'sonly_raw_public_keys()is true — there is no "offers both types" behavior in either version. Task 6 should write the N-4 note from the rustls sources, not from the review's parenthetical. - Docs synced beyond the type docs:
docs/architecture/server.md§AcceptAnyCertVerifieranddocs/architecture/client.md§FingerprintPinVerifiernow carry the S-1 posture + the N-1 cross-reference.
Summary
Landed (this task):
src/server.rs—AcceptAnyCertVerifierdoc rewritten: a "# The presented signature is NOT verified (no proof-of-possession)" section stating the spoofable-fingerprint consequence, the two safe patterns (challenge-response / a verifying verifier), the OQ-TLS-09 pointer, and the pinning-test reference.src/client.rs—FingerprintPinVerifierdoc: the "stolen-but-stale fingerprint" phrasing replaced (the cert is presented fresh each handshake; the signature check defeats use of a stolen/observed cert by a party without the private key) plus the cross-reference: this verifier checks pop; the server-sideAcceptAnyCertVerifierdoes not.docs/architecture/open-questions.md— OQ-TLS-09 recorded, status open, the three options (a/b/c), the pinning-test constraint, and the deferral note (the decision needs the auth-layer design or an API call before the first consumer).tests/impersonation_posture.rs(tcp-gated) — the impersonation probe made permanent, both variants: X.509 victim cert + attacker key, and RFC 7250 victim SPKI + attacker key; each asserts the handshake completes, application data flows, and the server-extracted fingerprint equals the victim's.
Verification: 68 default / 77 all-features tests green
(+2 new); clippy -D warnings clean (default + all-features); fmt
clean; cargo doc --no-deps warning-free.