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:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: fix-accept-any-cert-verifier-posture
|
||||
name: S-1 remediation — AcceptAnyCertVerifier no-pop posture (doc + ADR/OQ + permanent probe)
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: medium
|
||||
@@ -80,23 +80,23 @@ finding, not a code-change mandate. What's missing:
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] The doc note exists on `AcceptAnyCertVerifier` and states the
|
||||
- [x] 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
|
||||
- [x] The N-1 companion line exists on `FingerprintPinVerifier`
|
||||
- [x] OQ-TLS-09 recorded with status `open` and the three options
|
||||
- [x] The permanent probe test passes and asserts the spoofable
|
||||
fingerprint end-to-end (both cert types)
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
- [x] `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)
|
||||
- [x] `cargo test --all-features` green (the probe is tcp-gated)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A consumer reading only the rustdoc understands that presented
|
||||
- [x] 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
|
||||
- [x] The spoofable posture is pinned by an executed test, not just
|
||||
prose
|
||||
- [ ] The ownership question is tracked (OQ-TLS-09) — not silently
|
||||
- [x] The ownership question is tracked (OQ-TLS-09) — not silently
|
||||
inherited
|
||||
|
||||
## References
|
||||
@@ -111,6 +111,59 @@ finding, not a code-change mandate. What's missing:
|
||||
|
||||
> Agent fills this during implementation.
|
||||
|
||||
- The probe used a tokio duplex pair + `TlsConnector`/`TlsAcceptor`
|
||||
(tokio-rustls, `tcp`-gated) rather than raw `complete_io` — same
|
||||
shape, cleaner await-based flow. The attacker client uses a
|
||||
test-local accept-anything `ServerCertVerifier` and a fixed
|
||||
`ResolvesClientCert` presenting the victim's bytes with the
|
||||
attacker's signer, `only_raw_public_keys() == false` in both
|
||||
variants (the cert-type negotiation stays X.509-typed, exactly the
|
||||
review-probe shape; verified against rustls 0.23.44's
|
||||
`validate_client_cert_type_extension` — an offer of only
|
||||
`[RawPublicKey]` would fail `IncorrectCertificateTypeExtension`
|
||||
against `requires_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 to `docs-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 send `client_certificate_types = [RawPublicKey]`
|
||||
whenever the resolver's `only_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` §
|
||||
`AcceptAnyCertVerifier` and `docs/architecture/client.md` §
|
||||
`FingerprintPinVerifier` now carry the S-1 posture + the N-1
|
||||
cross-reference.
|
||||
|
||||
## Summary
|
||||
|
||||
> Agent fills this on completion.
|
||||
> Agent fills this on completion.
|
||||
|
||||
**Landed (this task):**
|
||||
|
||||
1. `src/server.rs` — `AcceptAnyCertVerifier` doc 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.
|
||||
2. `src/client.rs` — `FingerprintPinVerifier` doc: 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-side
|
||||
`AcceptAnyCertVerifier` does not.
|
||||
3. `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).
|
||||
4. `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.
|
||||
Reference in New Issue
Block a user