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
+64 -11
View File
@@ -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.