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:
glm-5.3-flash committed 2026-09-10 22:00:57 +00:00
1 parent d7db6b17a6
commit e86b8ba1b5
7 files changed
+414 -21

No files matched your search

+11 -3
View File
@@ -231,9 +231,17 @@ impl rustls::client::ResolvesClientCert for NoClientCertResolver {
/// CA verification and no name verification, only the fingerprint pin.
///
/// Handshake signatures are still verified (using the aws-lc-rs default
/// signature verification algorithms) so that a stolen-but-stale fingerprint
/// can't be replayed with a forged signature: the presenter must prove
/// possession of the private key corresponding to the pinned public key.
/// signature verification algorithms): the presenter must prove possession
/// of the private key corresponding to the pinned public key, so the pin
/// cannot be satisfied by replaying the cert's public bytes under a
/// different key. The cert is presented fresh in every handshake — the
/// threat the signature check addresses is a stolen or observed
/// *certificate* being used by a party that does not hold the matching
/// private key.
///
/// This verifier checks proof-of-possession. The **server-side**
/// `AcceptAnyCertVerifier` does not — see that type's docs (S-1 in
/// `docs/reviews/001-implementation-review.md`, OQ-TLS-09).
pub struct FingerprintPinVerifier {
fingerprint: String,
supported: rustls::crypto::WebPkiSupportedAlgorithms,
+37 -1
View File
@@ -249,11 +249,47 @@ pub fn generate_self_signed_cert() -> Result<SelfSignedCert, TlsError> {
/// (`IdentityProvider::resolve_from_fingerprint`) is the auth layer's
/// concern, outside this crate (alktls ADR-005).
///
/// # The presented signature is NOT verified (no proof-of-possession)
///
/// The client's CertificateVerify signature is never checked against the
/// presented cert's public key: `verify_client_cert` accepts any cert and
/// the `verify_tls12_signature`/`verify_tls13_signature` methods assert
/// validity unconditionally. Consequently **the fingerprint this verifier
/// lets the server extract is attacker-suppliable**: any party that
/// observes a peer's public cert bytes (X.509) or SPKI (RFC 7250) can
/// complete a handshake presenting those bytes under its own key, and
/// `peer_certificates()` yields the victim's cert — every downstream
/// fingerprint decision (scopes, tokens, resumption) is then made against
/// a spoofed identity. The auth layer's peer table cannot detect this: the
/// fingerprint it is handed *is* the victim's. Until the caller enforces
/// possession, treat the extracted fingerprint as an unauthenticated
/// claim, not proof of identity.
///
/// Two patterns make the identity safe — the auth layer owns either:
///
/// 1. **Challenge-response over the established channel**, bound to the
/// presented public key: possession is proven post-handshake, so a
/// stolen/observed fingerprint cannot be replayed without the key.
/// 2. **A verifier that does verify the CertificateVerify** against the
/// presented cert — the same
/// `verify_tls13_signature_with_raw_key` / `verify_tls12_signature`
/// routing the client-side `FingerprintPinVerifier` implements. The
/// crate has the routing code, just not on this type (a tracked
/// question — see OQ-TLS-09 in `docs/architecture/open-questions.md`).
///
/// This behavior is pinned end-to-end by
/// `tests/impersonation_posture.rs`: a handshake with the victim's cert
/// bytes + an attacker signer completes and the server extracts the
/// victim's fingerprint. A change that adds proof-of-possession (or an
/// upstream rustls change that enforces it) must fail that test and
/// update this doc + OQ-TLS-09 together.
///
/// **Server-side only.** This must not be reused as a client-side
/// `ServerCertVerifier` — client-side verification is alknet ADR-034's
/// selection matrix (see the client module): CA verification for unknown
/// X.509 remotes, fingerprint pinning for known peers, fail closed for
/// unknown raw keys.
/// unknown raw keys. Unlike the client-side pin verifier, this type has
/// **no proof-of-possession check** (see above).
pub struct AcceptAnyCertVerifier;
impl std::fmt::Debug for AcceptAnyCertVerifier {