record S-1/N-1 remediation status + the N-4 parenthetical correction
- 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
This commit is contained in:
@@ -8,6 +8,31 @@ logged, N-2 retracted).** Findings filed from the `review-impl`
|
||||
post-port review (2026-09-10), an independent pass on top of the
|
||||
already-green phase gate (`tasks/review-impl.md`, the same day).
|
||||
|
||||
**Remediation status (2026-09-11): S-1 (+N-1) remediated** — the
|
||||
no-pop doc note is on `AcceptAnyCertVerifier`, the N-1 companion fix is
|
||||
on `FingerprintPinVerifier`, OQ-TLS-09 records the ownership question
|
||||
(open), and the impersonation probe is permanent
|
||||
(`tests/impersonation_posture.rs`, tcp-gated, both cert types) —
|
||||
commit `e86b8ba`, task
|
||||
`tasks/fix-accept-any-cert-verifier-posture.md` (completed). The
|
||||
remaining findings are tracked in the six remediation tasks from the
|
||||
001-decomposition (commit `d7db6b1`).
|
||||
|
||||
**One correction to this review, found during the S-1 remediation:** the
|
||||
N-4 parenthetical "alknet's own client resolver offers both types" is
|
||||
**inaccurate** — rustls 0.23.41 and 0.23.44 both send
|
||||
`client_certificate_types = [RawPublicKey]` iff the *resolver's*
|
||||
`only_raw_public_keys()` is true (client/hs.rs:331-334 in 0.23.41,
|
||||
:355-358 in 0.23.44); there is no "offers both types" behavior in
|
||||
either version. The raw-key impersonation probe works because the
|
||||
attacker presents the SPKI under the *default* X.509 offer
|
||||
(`only_raw_public_keys() == false`), which the server accepts unparsed
|
||||
(`(false, _, true)` arm of `process_cert_type_extension` — server/hs.rs
|
||||
0.23.44:274-288). A rustls-driven raw-key client resolver (offering
|
||||
`[RawPublicKey]` only) would fail `IncorrectCertificateTypeExtension`
|
||||
against this verifier — see N-4 below for the corrected mechanism, and
|
||||
write the N-4 doc note from the rustls sources, not the parenthetical.
|
||||
|
||||
This review's focus (per the user's direction): the crate stands on
|
||||
its own as the publication target — correctness, security, and test
|
||||
coverage — with the alknet sources consulted only as behavior-preservation
|
||||
@@ -240,10 +265,16 @@ vulnerable *now*.
|
||||
endpoint model wants mandatory pop, that's a cheap, additive
|
||||
companion type — decide before the first consumer.
|
||||
|
||||
**Remediation task:** none exists yet; if adopted, land as
|
||||
`tasks/fix-accept-any-cert-verifier-posture.md` alongside the ADR/OQ
|
||||
above. (This review does not change code — the finding is doc + ADR
|
||||
work.)
|
||||
**Remediation task:** ~~none exists yet;~~ **landed** —
|
||||
`tasks/fix-accept-any-cert-verifier-posture.md` (completed, commit
|
||||
`e86b8ba`): the S-1 doc note on `AcceptAnyCertVerifier`, the N-1
|
||||
companion fix, OQ-TLS-09 (open, three options a/b/c), and the
|
||||
impersonation probe permanent as `tests/impersonation_posture.rs`
|
||||
(tcp-gated, both cert types — handshake completes, app data flows, the
|
||||
server extracts the victim's fingerprint; a future pop change must
|
||||
fail/update that test with the doc + OQ together). server.md and
|
||||
client.md carry the same posture. This review does not change code —
|
||||
the finding is doc + ADR work.
|
||||
|
||||
## C-1 [minor, correctness] — `generate_self_signed_cert()` produces a cert valid 1975→4096 (rcgen's default), i.e. effectively never-expiring, and no doc states it
|
||||
|
||||
@@ -468,6 +499,12 @@ the server-side verifier (S-1). One clarifying line ("this verifier
|
||||
checks proof-of-possession; the *server-side* AcceptAnyCertVerifier
|
||||
does not — see that type's docs") closes the loop.
|
||||
|
||||
**Remediated (commit `e86b8ba`)**: the phrasing is 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) and the
|
||||
cross-reference to `AcceptAnyCertVerifier` exists on the type, in
|
||||
client.md, and server.md.
|
||||
|
||||
## N-2 — NON-FINDING (retracted): the client config's `dangerous()` verifier path was suspected of allowing an unverified-CA downgrade — it does not
|
||||
|
||||
Filed as a suspect ("`with_custom_certificate_verifier` + `None`
|
||||
@@ -522,6 +559,39 @@ explicitly: **a raw-key client must include `X509` in its
|
||||
verifier rejects it.** (alknet's own client resolver offers both
|
||||
types, which is why the extracted code works in production.)
|
||||
|
||||
**Correction to the parenthetical above (found during the S-1
|
||||
remediation, 2026-09-11):** "alknet's own client resolver offers both
|
||||
types" is **inaccurate**. rustls 0.23.41 and 0.23.44 both send
|
||||
`client_certificate_types = [RawPublicKey]` (only) whenever the
|
||||
*resolver's* `only_raw_public_keys()` is true (client/hs.rs:331-334 in
|
||||
0.23.41, :355-358 in 0.23.44) — no version offers both types. The
|
||||
consequence chain, verified from the rustls sources:
|
||||
|
||||
1. A rustls-driven client with a raw-key resolver (e.g.
|
||||
`RawKeyClientCertResolver` with an Ed25519 SPKI,
|
||||
`only_raw_public_keys() == true`) offers `[RawPublicKey]` only.
|
||||
2. Against `AcceptAnyCertVerifier`
|
||||
(`requires_raw_public_keys() == false`), the `(false, true, false)`
|
||||
arm of `process_cert_type_extension` (server/hs.rs:274-288) fires →
|
||||
`IncorrectCertificateTypeExtension` → **handshake failure**.
|
||||
3. So a raw-key *client* cannot handshake against this verifier at
|
||||
all (fail-closed, not a downgrade — but a real interop trap the
|
||||
doc note must state).
|
||||
4. The S-1 raw-key impersonation probe sidesteps this: the attacker
|
||||
presents the victim's SPKI under the *default* X.509 offer
|
||||
(`only_raw_public_keys() == false` → no cert-type extension →
|
||||
`(false, false, false)` → default X.509), and the server passes the
|
||||
SPKI bytes through unparsed (a custom `verify_client_cert` receives
|
||||
opaque bytes) — which is why the probe shape pins
|
||||
`only_raw_public_keys() == false` on its fixed resolver.
|
||||
|
||||
The doc note for task 6 (`docs-pin-c1-c4-n3-n4`) should be written
|
||||
from this chain (and the rustls sources), not from the review's
|
||||
original parenthetical: the accurate statement is that
|
||||
`requires_raw_public_keys()` must stay `false` for X.509-or-raw
|
||||
*server* offers, and that a raw-key *client* resolver makes rustls
|
||||
offer `[RawPublicKey]` only, which this verifier rejects.
|
||||
|
||||
## N-5 — NON-FINDING (verified): the dropped `zeroize::ZeroizeOnDrop` marker impl does not change memory-erasure behavior
|
||||
|
||||
The port review recorded the omission as accepted
|
||||
|
||||
Reference in New Issue
Block a user