ADR-008: server-path possession verification — the verifying verifier is the default (OQ-TLS-09 resolved)

Close review 001 §S-1: the default client-cert verifier never checked
the client's CertificateVerify, so anyone holding a peer's *public*
cert/SPKI bytes (public by design — peers publish them to be dialable)
could complete a handshake as that peer, and the auth layer could not
detect it. The consumer designs are known (X.509 and raw-key TCP/QUIC
endpoints with identity-bearing clients), so implementing now — the
zero-consumer moment — avoids the guaranteed breaking republish of
flipping the default later.

- VerifyPresentedCertVerifier (new): request, don't require, verify
  possession — permissive verify_client_cert (self-signed chains and
  bare SPKIs stay valid presentation) + CertificateVerify routing by
  presented cert kind (Ed25519 SPKI -> verify_tls13_signature_with_
  raw_key both TLS versions; X.509 -> standard route), the same
  routing FingerprintPinVerifier implements. Nine-scheme list
  verbatim (shared fn, exact-list pin covers both).
- Default on every TlsServerConfig path — X509 / RawKey / SelfSigned
  / ACME (the verifier install is crate-side rustls in new_acme, not
  rustls-acme's).
- AcceptAnyCertVerifier stays public as the explicit no-pop escape
  hatch, no longer installed by any crate path.
- tests/impersonation_posture.rs: four pins — default rejects the
  attacker (X.509: UnsupportedSignatureAlgorithmForPublicKeyContext;
  raw-key: BadSignature), escape hatch still accepts + extracts the
  victim's fingerprint (both cert types).
- tests/handshake_behavior.rs: suites 4/4b — possession-checked legit
  clients (raw-key pin vs raw-key server; X.509 client vs X.509
  server) complete and the server extracts the fingerprint; suite 3b
  doc updated.
- Docs: ADR-008 written; OQ-TLS-09 -> resolved (option (b));
  ADR-007 §Limits deferral retired to not-planned; server.md/client.md
  invariants/README/overview synced.

Verification: cargo test 81 / --features tcp 95 / --all-features 104
green; clippy -D warnings clean (default + all-features); fmt clean;
cargo doc warning-free.
This commit is contained in:
glm-5.3-flash committed 2026-09-11 11:12:48 +00:00
1 parent 49d4432247
commit ac440f3a9a
12 files changed
+622 -172

No files matched your search

+1
View File
@@ -34,6 +34,7 @@ unresolved.
| [005](decisions/005-config-types-move-into-alktls.md) | Accepted | Identity + credentials + fingerprint types move into alktls; auth layer stays out |
| [006](decisions/006-module-layout-and-tests.md) | Accepted | Eight-module layout; seed tests + integration invariant pins |
| [007](decisions/007-cert-type-negotiation.md) | Accepted | RFC 7250 cert-type negotiation: the offer follows the identity (deviation from alknet; resolves OQ-TLS-10) |
| [008](decisions/008-server-path-possession-verification.md) | Accepted | Server-path proof-of-possession: the verifying verifier is the default on every path (resolves OQ-TLS-09) |
## Lifecycle
+11 -10
View File
@@ -88,15 +88,14 @@ offer from the pin format — an `ed25519:<hex>` pin offers
`server_certificate_types = [RawPublicKey]` and completes against a
crate-built raw-key server (`raw_key_server_path_completes_with_crate_pin_client`);
a `SHA256:<hex>` pin keeps the X.509 offer. A raw-key *client*
identity presents its SPKI under the X.509 offer and
`AcceptAnyCertVerifier` accepts it end-to-end
(`raw_key_client_presents_spki_and_server_extracts_fingerprint`). A
pin-format/cert-kind mismatch fails closed at negotiation, never a
identity presents its SPKI under the X.509 offer and the server's
default verifier possession-verifies it end-to-end
(`raw_key_client_presents_spki_and_server_extracts_fingerprint`,
`raw_key_client_vs_raw_key_server_default_verifier_checks_possession`).
A pin-format/cert-kind mismatch fails closed at negotiation, never a
downgrade (`ed25519_pin_against_x509_server_fails_closed_at_negotiation`).
Raw-key peers that ride iroh/noq use those transports' own TLS and are
unaffected. The strict-server limit (a foreign server demanding
raw-only client certs still rejects an X.509-offer presentation) is
recorded in ADR-007 §Limits.
unaffected.
## `FingerprintPinVerifier`
@@ -111,9 +110,11 @@ algorithms; Ed25519 SPKI certs route through
`verify_tls13_signature_with_raw_key`): the presenter must prove
possession of the corresponding private key, so a stolen or observed
certificate cannot be used by a party that does not hold the matching
key. This verifier checks proof-of-possession; the server-side
`AcceptAnyCertVerifier` does not (see
[server.md](server.md), OQ-TLS-09).
key. The same possession rule now holds server-side:
`VerifyPresentedCertVerifier` is the default client-cert verifier on
every server path (ADR-008, OQ-TLS-09); `AcceptAnyCertVerifier`
remains the explicit no-pop escape hatch (see
[server.md](server.md)).
The cert-type offer follows the pin format (ADR-007):
`requires_raw_public_keys()` returns `true` for `ed25519:` pins
@@ -127,13 +127,16 @@ recorded rather than silent; this ADR is the record.
## Limits (honest scope)
The SPKI-under-X.509 presentation makes a raw-key *client* presentable
to **this crate's** `AcceptAnyCertVerifier` (and to any permissive
verifier). A foreign strict RFC 7250 server that *demands*
`client_certificate_types = [RawPublicKey]` (`requires_raw_public_keys() == true` server-side) will still reject an X.509-offer
presentation. If the rewrite needs that, the additive route is a
raw-key-only server-verifier sibling (OQ-TLS-10 option (c)), which
pairs with OQ-TLS-09's verify-presenting verifier (option (b)) — both
remain additive and are not needed for the crate's own compositions.
to **this crate's** server verifiers (both the default
`VerifyPresentedCertVerifier` and the `AcceptAnyCertVerifier` escape
hatch) and to any permissive verifier. A foreign strict RFC 7250
server that *demands* `client_certificate_types = [RawPublicKey]`
(`requires_raw_public_keys() == true` server-side) will still reject
an X.509-offer presentation. No known alk* deployment needs that (all
branch designs use permissive servers — clients may present X.509, an
SPKI, or nothing); the additive route if one ever appears is a
raw-key-only server-verifier sibling. **Not planned** — recorded here
so the next reader doesn't mistake it for pending work.
## Consequences
@@ -0,0 +1,152 @@
---
status: accepted
last_updated: 2026-09-11
---
# ADR-008: Server-path proof-of-possession — the verifying verifier is the default
## Status
Accepted (2026-09-11). Resolves OQ-TLS-09.
## Context
Review 001 §S-1 (executed): `AcceptAnyCertVerifier` — then the default
client-cert verifier on every `TlsServerConfig` path — never checked
the client's CertificateVerify signature. Any party that observes a
peer's public cert bytes (X.509) or SPKI (RFC 7250) could complete a
handshake *as that peer*: the handshake completed, `peer_certificates()`
yielded the victim's cert, and the extracted fingerprint — the
identity every downstream auth decision consumes — was the victim's.
The auth layer could not detect the spoofing: the fingerprint it was
handed *was* the victim's.
The public cert/SPKI bytes are public **by design** — a peer must
publish its identity to be dialable (branch-2 endpoints publish the
same Ed25519 public key an SSH server would). So the observed-by-anyone
attack surface was every peer identity in the stack, and the only
proposed mitigation (challenge-response owned by the auth layer, the
rewrite's option (a)) did not exist anywhere in the alk* codebase. The
spoil was fully known and the fix fully specified — mirroring the
signature routing the client-side `FingerprintPinVerifier` already
implements — so "wait for a consumer" (the OQ's deferral framing)
repeated the circular reasoning OQ-TLS-10 fell into: the component
that must *enable* the consumer was deferring to the consumer it
enables. The consumer designs are known (TCP/QUIC servers with X.509
or raw-key identities and identity-bearing clients — alkhttp,
webtransport, the rewrite's endpoints); possession verification is
needed by every one of them that authenticates by fingerprint.
## Decision
`VerifyPresentedCertVerifier` is the **default** client-cert verifier
on every `TlsServerConfig` path — X509, RawKey, SelfSigned, and ACME
alike (the verifier install is crate-side rustls in `new_acme`, not
rustls-acme's, so the ACME path flips identically). Its posture:
**request, don't require, verify possession.**
- `offer_client_auth() == true`, `client_auth_mandatory() == false`,
`root_hint_subjects() == &[]` — identical request-not-require shape
to `AcceptAnyCertVerifier` (no-cert clients unaffected).
- `verify_client_cert` accepts any presented bytes — possession is
proven by the CertificateVerify, not the cert's provenance;
self-signed X.509 chains and bare RFC 7250 SPKIs remain valid
presentation.
- `verify_tls13_signature` / `verify_tls12_signature` route by
presented cert kind: an Ed25519 SPKI goes through
`verify_tls13_signature_with_raw_key` (both TLS versions), an X.509
chain through the standard `verify_tls{12,13}_signature` — the same
routing the client-side `FingerprintPinVerifier` implements, and the
same functions rustls' webpki verifiers use.
- `supported_verify_schemes()` returns the nine-scheme list verbatim
(shared with `AcceptAnyCertVerifier`; the exact-list pin covers
both).
- `requires_raw_public_keys()` stays `false` — both cert types
negotiate (ADR-007).
`AcceptAnyCertVerifier` stays public as the explicit no-pop escape
hatch for a deployment that deliberately wants the old posture
(handshake-speed over strictness, an auth layer that owns
challenge-response). It is no longer installed by any crate path.
Mechanism note (rustls 0.23.44, `server/tls13.rs`): when a client
presents a cert, rustls calls `verify_client_cert` then
`verify_tls13_signature(construct_client_verify_message(..), cert[0],
sig)` — the signature covers the transcript hash bound to the
`"TLS 1.3, client CertificateVerify"` constant, so the check is a real
proof-of-possession of the presented public key, not a replayable
blob. A client presenting no cert skips both (the `!mandatory`
branch). The attacker's failure mode is a signature under the
attacker's key against the victim's public key: `BadSignature`
(raw-key) or `UnsupportedSignatureAlgorithmForPublicKeyContext`
(X.509 kind mismatch) — pinned by `tests/impersonation_posture.rs`.
## What this buys — and what it deliberately does not
**Bought:** the extracted fingerprint is now authenticated as *"the
connecting party holds the private key for the presented public
identity."* The impersonation attack (victim's public bytes + attacker
signer) fails the handshake on every default path, both cert types.
This is the S-1 resolution for branches 1 and 2 (X.509 and raw-key
TCP/QUIC endpoints with identity-bearing clients).
**Not bought (deliberately, unchanged):** no CA verification and no
name verification — a self-signed chain remains valid presentation,
and who a fingerprint maps to (the peer table, scopes, tokens) remains
the auth layer's concern (ADR-005). The verifier proves *possession*;
the auth layer still decides *trust*. This is exactly the split the
crate's scope boundary prescribes (ADR-002).
## Behavior-preservation invariants under the change
- The nine-scheme list, request-not-require shape, non-empty root
store, 0-RTT, provider, and ALPN handling are untouched.
- Fail-closed: unchanged — and the new default adds a rejection where
there was silent acceptance (the spoof), the direction that never
breaks a legitimate composition.
- Legit compositions verified end-to-end (duplex handshakes,
`tests/handshake_behavior.rs`): raw-key client pin ↔ raw-key server
(suite 4), X.509 client ↔ X.509 server (suite 4b), raw-key client ↔
X.509 server (suite 3b) — all extract the client fingerprint under
the verifying default. No-cert clients (browsers) unaffected.
- QUIC parity: the CertificateVerify mechanism is handshake-level,
identical under `for_noq()`.
## Consequences
**Positive:**
- S-1 closed out of the box: no deployment can accidentally run the
spoofable posture; the escape hatch must be explicitly installed and
its posture is test-pinned.
- The auth layer's fingerprint resolution can now treat the fingerprint
as a possession-authenticated claim — the option-(c)
challenge-response design becomes optional defense-in-depth rather
than a prerequisite.
- Zero-consumer moment used correctly: flipping the default *after*
first consumers would be a guaranteed breaking republish.
**Negative:**
- A legitimate client whose signer does not match its presented cert
(a misconfigured `CertifiedKey`) now fails the handshake where it
previously succeeded silently — the failure is the correct diagnosis
(there is no such legitimate composition).
- One more public type on the API surface (additive; the freeze allows
additions).
## References
- OQ-TLS-09 (`docs/architecture/open-questions.md`) — resolved by
this ADR
- `tests/impersonation_posture.rs` — both postures pinned (default
rejects, escape hatch accepts), both cert types
- `tests/handshake_behavior.rs` — legit-composition pins (suites 1,
3b, 4, 4b)
- review 001 §S-1 — the discovery probe
- ADR-007 (cert-type negotiation), ADR-002 (scope boundary), ADR-005
(auth layer owns peer resolution)
- alknet ADR-034 — the request-but-don't-require shape this ADR
refines (deviation recorded per AGENTS.md convention 10)
- rustls 0.23.44 `server/tls13.rs` (the CertificateVerify flow),
`webpki/verify.rs` (`verify_tls13_signature_with_raw_key`)
+22 -35
View File
@@ -21,7 +21,7 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
| OQ-TLS-06 | ACME task shutdown surface | resolved (detached-only for v1) | low |
| OQ-TLS-07 | iroh key surface | **resolved** (ADR-005, byte access pinned) | low |
| OQ-TLS-08 | `quinn` → `noq` feature rename | **resolved** (ADR-003) | high |
| OQ-TLS-09 | Server-path proof-of-possession | **open** | high |
| OQ-TLS-09 | Server-path proof-of-possession | **resolved** (ADR-008) | high |
| OQ-TLS-10 | RFC 7250 over TCP: cert-type negotiation gap | **resolved** (ADR-007) | high |
## Identity & types
@@ -112,37 +112,23 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
impersonation probe: a handshake with the victim's cert bytes + an
attacker signer completes, and the server extracts the victim's
fingerprint)
- **Status**: open (recorded 2026-09-10)
- **Priority**: high
- **Question**: `AcceptAnyCertVerifier` never checks the client's
CertificateVerify signature — the presented identity is spoofable by
anyone holding the public cert/SPKI bytes, and the auth layer cannot
detect it (the fingerprint it is handed *is* the victim's). alknet
ADR-034 inherited the request-but-don't-require shape without
recording this property. Who enforces possession?
- **Options**:
- **(a) Keep request-but-don't-require; the auth layer owns
challenge-response** over the established channel, bound to the
presented public key. No crate change; the S-1 doc note on
`AcceptAnyCertVerifier` is the honest description until then.
- **(b) Add a `VerifyPresentedCertVerifier` sibling**
(request-and-verify): same nine-scheme list, permissive
`verify_client_cert`, signature methods delegate to
`rustls::crypto::verify_tls{12,13}_signature(_with_raw_key)` — the
routing the client-side `FingerprintPinVerifier` already
implements. Additive; must land before the first consumer (an API
shape decision).
- **(c) Make the auth layer's fingerprint resolution
possession-checked** (a possession proof accompanies each
fingerprint resolution). Cross-crate; the auth layer does not exist
yet.
- **Constraints**: the spoofable posture is pinned by
`tests/impersonation_posture.rs` (both cert types) — any decision
must fail or update that test together with the
`AcceptAnyCertVerifier` doc note.
- **Cross-references**: src/server.rs (`AcceptAnyCertVerifier`),
src/client.rs (`FingerprintPinVerifier`),
docs/reviews/001-implementation-review.md §S-1, alknet ADR-034
- **Status**: resolved (2026-09-11) — option (b),
[ADR-008](decisions/008-server-path-possession-verification.md).
[`VerifyPresentedCertVerifier`](decisions/008-server-path-possession-verification.md)
is the default client-cert verifier on every `TlsServerConfig` path
(including the ACME path — the verifier install is crate-side
rustls, not rustls-acme's): request, don't require, **verify the
CertificateVerify against the presented cert's public key**. The
extracted fingerprint is possession-checked (attacker-supplied
presentations fail the handshake —
`tests/impersonation_posture.rs` pins both directions); who a
fingerprint maps to remains the auth layer's concern (ADR-005).
`AcceptAnyCertVerifier` stays public as the explicit no-pop escape
hatch, its posture pinned by the same test file.
- **Cross-references**: src/server.rs (`VerifyPresentedCertVerifier`,
`AcceptAnyCertVerifier`), src/client.rs (`FingerprintPinVerifier` —
the routing being mirrored), ADR-008, review 001 §S-1, alknet
ADR-034
### OQ-TLS-10: How do RFC 7250 raw-key peers negotiate over rustls-driven TCP+TLS?
@@ -205,6 +191,7 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
## Deferred / Blocked
- OQ-TLS-09 (server-path proof-of-possession): open by design — the
decision needs the rewrite's auth-layer design in hand (option (c))
or an API-shape call before the first consumer (option (b)).
None — all recorded OQs are resolved. Future questions (e.g. the
raw-key-only server-verifier sibling if a foreign strict RFC 7250
server interop ever appears — see ADR-007 §Limits) get new OQ entries
rather than reopening these.
+1
View File
@@ -79,6 +79,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
| [005](decisions/005-config-types-move-into-alktls.md) | Config types move into alktls | Identity + credentials + fingerprint move in; auth layer stays out |
| [006](decisions/006-module-layout-and-tests.md) | Module layout and test surface | Eight modules; in-module seed tests + integration invariant pins |
| [007](decisions/007-cert-type-negotiation.md) | RFC 7250 cert-type negotiation | The cert-type offer follows the identity/pin format — raw-key-over-TCP works (deviation from alknet; resolves OQ-TLS-10) |
| [008](decisions/008-server-path-possession-verification.md) | Server-path possession verification | The verifying verifier is the default on every server path; `AcceptAnyCertVerifier` is the explicit escape hatch (resolves OQ-TLS-09) |
## Open Questions
+42 -39
View File
@@ -59,47 +59,46 @@ test (ADR-006):
- **`rustls::crypto::aws_lc_rs::default_provider()`** as the crypto
provider on all paths (alknet ADR-084). Never `ring`, never the
process-default provider, without a new ADR.
- **`AcceptAnyCertVerifier::supported_verify_schemes()`** returns
- **The nine-scheme `supported_verify_schemes()`** (shared by
`VerifyPresentedCertVerifier` and `AcceptAnyCertVerifier`) returns
ED25519 + ECDSA P-256/P-384 + RSA PSS (SHA256/384/512) + RSA PKCS1
(SHA256/384/512) — nine schemes, verbatim, pinned by an exact-list
integration test.
- **The default client-cert verifier verifies possession**
(ADR-008): `VerifyPresentedCertVerifier` is installed on every
path; the CertificateVerify is checked against the presented
cert's public key. `AcceptAnyCertVerifier` exists only as the
explicit escape hatch.
- **`acme-tls/1` ALPN append** for the ACME path only, done by the
crate, not the caller (alknet ADR-027 §7).
- **Non-empty root store** — the client CA path merges `webpki-roots`
when the platform store is empty (see [client.md](client.md)).
## `AcceptAnyCertVerifier`
## `VerifyPresentedCertVerifier` (the default) and `AcceptAnyCertVerifier` (the escape hatch)
The server-side client-cert verifier: **request-but-don't-require**.
It asks for a client cert (X.509 or RFC 7250 raw key) so the caller
can extract the fingerprint via `peer_identity()`, but does not
require one and does not verify the presented cert against a CA. The
fingerprint is matched against peer records by the auth layer
(`IdentityProvider::resolve_from_fingerprint`) *outside* this crate —
the TLS crate hands over the fingerprint string; peer resolution is
not a TLS concern (ADR-005).
The server-side client-cert verifier: **request, don't require, verify
possession** (ADR-008, resolving OQ-TLS-09).
[`VerifyPresentedCertVerifier`](decisions/008-server-path-possession-verification.md)
is the default on every `TlsServerConfig` path (X509 / RawKey /
SelfSigned / ACME). It asks for a client cert (X.509 or RFC 7250 raw
key) so the caller can extract the fingerprint via `peer_identity()`,
does not require one, and does not verify the presented cert against a
CA — self-signed chains and bare SPKIs are valid presentation. The
client's **CertificateVerify signature is verified** against the
presented cert's public key (Ed25519 SPKIs route through
`verify_tls13_signature_with_raw_key`; X.509 through the standard
route): the extracted fingerprint is possession-checked — presenting a
victim's public bytes under an attacker's key fails the handshake.
Who the fingerprint maps to remains the auth layer's concern
(ADR-005).
**The presented signature is not verified (no proof-of-possession)** —
review 001's S-1: the client's CertificateVerify signature is never
checked against the presented cert's public key, so any party holding
a peer's public cert bytes (X.509) or SPKI (RFC 7250) can complete a
handshake *as that peer*, and the server hands the auth layer the
victim's fingerprint. The auth layer 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 it safe (the auth
layer owns either): challenge-response over the established channel
bound to the presented public key, or a verifier that verifies the
CertificateVerify (the routing `FingerprintPinVerifier` implements —
tracked as OQ-TLS-09; option (b) there is additive but must land
before the first consumer). The spoofable posture is pinned by
`tests/impersonation_posture.rs` (both cert types) — a change here
must fail or update that test together with this doc.
Server-side only: this must not be reused as a client-side
`ServerCertVerifier` — client-side verification is alknet ADR-034's
selection matrix (see [client.md](client.md)), and unlike the
client-side pin verifier this type has no proof-of-possession check.
**The escape hatch**: `AcceptAnyCertVerifier` is the documented no-pop
verifier — same request-not-require shape, no CertificateVerify check,
so the fingerprint it extracts is attacker-suppliable (S-1). Install
it explicitly only when a deployment deliberately wants that posture.
Both postures are pinned by `tests/impersonation_posture.rs` (default
rejects the attacker, escape hatch accepts — both cert types); a
change must update that test together with this doc.
## `RawKeyCertResolver`
@@ -117,10 +116,12 @@ sends only when the client verifier overrides
(the offer follows the pin format), so a crate pin client completes
against this raw-key server
(`raw_key_server_path_completes_with_crate_pin_client`). A raw-key
*client* identity presents its SPKI under the X.509 offer, which
`AcceptAnyCertVerifier` (`requires_raw_public_keys() == false` —
correctly; it accepts both cert types) accepts end-to-end
(`raw_key_client_presents_spki_and_server_extracts_fingerprint`).
*client* identity presents its SPKI under the X.509 offer, which the
verifiers (`requires_raw_public_keys() == false` — correctly; both
accept X.509-or-raw cert types) accept end-to-end, the default
possession-verifying the presentation
(`raw_key_client_presents_spki_and_server_extracts_fingerprint`,
`raw_key_client_vs_raw_key_server_default_verifier_checks_possession`).
Raw-key peers riding iroh/noq are unaffected (their TLS stacks own
their negotiation).
@@ -169,9 +170,10 @@ Lifecycle semantics:
filters per endpoint type — alknet ADR-086 §3); the crate appends
only `acme-tls/1` on the ACME path.
- No handshake: verifier selection and handshake outcomes on the
*server* side are `AcceptAnyCertVerifier` + the caller's
fingerprint extraction; a rejected handshake is the transport's
error, not `TlsError`.
*server* side are the default `VerifyPresentedCertVerifier` (or the
explicitly-installed escape hatch) + the caller's fingerprint
extraction; a rejected handshake is the transport's error, not
`TlsError`.
- No peer resolution: the extracted fingerprint string goes to the
caller; `PeerEntry`/`AuthPolicy` live in the auth layer.
@@ -183,4 +185,5 @@ Lifecycle semantics:
spec this doc mirrors
- ADR-001 (invariants), ADR-002 (`TlsError`), ADR-003 (`for_noq`),
ADR-004 (accessors), ADR-005 (identity types), ADR-006 (modules,
tests)
tests), ADR-007 (cert-type negotiation), ADR-008 (possession
verification)
+1 -1
View File
@@ -45,7 +45,7 @@ pub use credentials::{ConnectionCredentials, RemoteIdentity};
pub use server::{
build_rustls_server_config, generate_self_signed_cert, AcceptAnyCertVerifier,
RawKeyCertResolver, SelfSignedCert, TlsServerConfig,
RawKeyCertResolver, SelfSignedCert, TlsServerConfig, VerifyPresentedCertVerifier,
};
#[derive(Debug, thiserror::Error)]
+162 -36
View File
@@ -1,7 +1,8 @@
//! Server-side TLS configuration: [`TlsServerConfig`],
//! [`build_rustls_server_config`], [`RawKeyCertResolver`],
//! [`AcceptAnyCertVerifier`], [`SelfSignedCert`] /
//! [`generate_self_signed_cert`], and the ACME path (feature `acme`).
//! [`VerifyPresentedCertVerifier`], [`AcceptAnyCertVerifier`],
//! [`SelfSignedCert`] / [`generate_self_signed_cert`], and the ACME path
//! (feature `acme`).
use std::sync::Arc;
@@ -12,6 +13,8 @@ use tracing::{debug, error, warn};
use crate::identity::AcmeDirectory;
use crate::identity::{Ed25519SecretKey, TlsIdentity};
use crate::signing::Ed25519SigningKey;
use crate::fingerprint::extract_ed25519_raw_key_from_spki;
use crate::TlsError;
/// Server-side TLS configuration, transport-agnostic. Built once from a
@@ -78,9 +81,9 @@ impl TlsServerConfig {
let resolver = state.resolver();
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let mut config = rustls::ServerConfig::builder_with_provider(provider)
let mut config = rustls::ServerConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()?
.with_client_cert_verifier(Arc::new(AcceptAnyCertVerifier))
.with_client_cert_verifier(Arc::new(VerifyPresentedCertVerifier::new(&provider)))
.with_cert_resolver(resolver);
config.max_early_data_size = u32::MAX;
@@ -173,7 +176,8 @@ pub fn build_rustls_server_config(
alpns: &[Vec<u8>],
) -> Result<rustls::ServerConfig, TlsError> {
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let client_verifier = Arc::new(AcceptAnyCertVerifier);
let client_verifier: Arc<dyn rustls::server::danger::ClientCertVerifier> =
Arc::new(VerifyPresentedCertVerifier::new(&provider));
match tls_identity {
TlsIdentity::X509 { cert, key } => {
let cert_chain = crate::pem::load_cert_chain(cert)?;
@@ -239,6 +243,147 @@ pub fn generate_self_signed_cert() -> Result<SelfSignedCert, TlsError> {
})
}
/// Server-side client-cert verifier that verifies the presented
/// identity's proof-of-possession: **request, don't require, verify
/// possession**. The default client-cert verifier on every
/// [`TlsServerConfig`] path (ADR-008, resolving OQ-TLS-09).
///
/// Like [`AcceptAnyCertVerifier`], it asks for a client cert (X.509 or
/// RFC 7250 raw key) so the endpoint can extract the fingerprint via
/// `peer_identity()`, does not require one, and does not verify the
/// presented cert against a CA — a self-signed X.509 chain or a bare
/// RFC 7250 SPKI is acceptable presentation. Unlike
/// `AcceptAnyCertVerifier`, the client's **CertificateVerify signature
/// is verified** against the presented cert's public key:
///
/// - An Ed25519 SPKI presentation routes to
/// `verify_tls13_signature_with_raw_key` (and its TLS 1.2
/// equivalent routing) — the same routing the client-side
/// `FingerprintPinVerifier` uses.
/// - An X.509 presentation routes to the standard
/// `verify_tls12_signature` / `verify_tls13_signature`.
///
/// A presented cert is therefore only usable by the party holding the
/// matching private key: presenting a victim's public cert/SPKI bytes
/// under an attacker's key fails the CertificateVerify check (the
/// impersonation posture pinned by `tests/impersonation_posture.rs`
/// under the permissive escape hatch). The extracted fingerprint is
/// authenticated as "this party holds the private key for the
/// presented public identity" — a real proof-of-possession, though
/// still *not* a name/CA check (self-signed chains remain valid
/// presentation; who a fingerprint maps to stays the auth layer's
/// concern, ADR-005).
///
/// Clients that present no cert are unaffected — there is nothing to
/// verify, and `client_auth_mandatory() == false`.
///
/// `supported_verify_schemes()` returns the same nine-scheme list as
/// `AcceptAnyCertVerifier` (the load-bearing list, pinned by an
/// exact-list test). `requires_raw_public_keys()` stays `false` — the
/// verifier accepts both cert types (ADR-007).
///
/// Use [`AcceptAnyCertVerifier`] only if a deployment deliberately
/// needs the no-pop posture (e.g. an auth layer that owns
/// challenge-response and wants handshake-speed over strictness); the
/// permissive posture is still pinned by `tests/impersonation_posture.rs`
/// when this verifier is explicitly installed.
pub struct VerifyPresentedCertVerifier {
supported: rustls::crypto::WebPkiSupportedAlgorithms,
}
impl VerifyPresentedCertVerifier {
pub fn new(provider: &Arc<rustls::crypto::CryptoProvider>) -> Self {
Self {
supported: provider.signature_verification_algorithms,
}
}
}
impl std::fmt::Debug for VerifyPresentedCertVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VerifyPresentedCertVerifier").finish()
}
}
impl rustls::server::danger::ClientCertVerifier for VerifyPresentedCertVerifier {
fn offer_client_auth(&self) -> bool {
true
}
fn client_auth_mandatory(&self) -> bool {
false
}
fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
&[]
}
fn verify_client_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
Ok(rustls::server::danger::ClientCertVerified::assertion())
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
if extract_ed25519_raw_key_from_spki(cert.as_ref()).is_some() {
let spki = rustls::pki_types::SubjectPublicKeyInfoDer::from(cert.as_ref().to_vec());
rustls::crypto::verify_tls13_signature_with_raw_key(
message,
&spki,
dss,
&self.supported,
)
} else {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported)
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
if extract_ed25519_raw_key_from_spki(cert.as_ref()).is_some() {
let spki = rustls::pki_types::SubjectPublicKeyInfoDer::from(cert.as_ref().to_vec());
rustls::crypto::verify_tls13_signature_with_raw_key(
message,
&spki,
dss,
&self.supported,
)
} else {
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported)
}
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
nine_supported_verify_schemes()
}
}
fn nine_supported_verify_schemes() -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
]
}
/// Server-side "request-but-don't-require" client cert verifier
/// (alknet ADR-034).
///
@@ -262,27 +407,18 @@ pub fn generate_self_signed_cert() -> Result<SelfSignedCert, TlsError> {
/// 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.
/// possession (challenge-response over the established channel is the
/// pattern), 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.
/// **Not the default** (ADR-008): [`VerifyPresentedCertVerifier`] is the
/// default client-cert verifier on every [`TlsServerConfig`] path — it
/// has the same request-not-require shape plus the CertificateVerify
/// possession check. Install this type explicitly only when a
/// deployment deliberately wants the no-pop posture. The spoofable
/// behavior is pinned by `tests/impersonation_posture.rs` (both cert
/// types, against a server configured with this verifier) — a change
/// must update that test together with this doc.
///
/// **Server-side only.** This must not be reused as a client-side
/// `ServerCertVerifier` — client-side verification is alknet ADR-034's
@@ -339,17 +475,7 @@ impl rustls::server::danger::ClientCertVerifier for AcceptAnyCertVerifier {
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
]
nine_supported_verify_schemes()
}
}
+91 -4
View File
@@ -14,8 +14,9 @@
//! verification).
//! - A raw-key *client* identity presents its SPKI under the X.509
//! offer (`only_raw_public_keys() == false` — the extension is an
//! offer format, not an identity statement); `AcceptAnyCertVerifier`
//! accepts it and the server extracts the `ed25519:` fingerprint.
//! offer format, not an identity statement); the server's default
//! `VerifyPresentedCertVerifier` possession-verifies it (ADR-008)
//! and the server extracts the `ed25519:` fingerprint.
//!
//! Mechanism reference: rustls 0.23.44's cert-type negotiation
//! (`server/hs.rs::process_cert_type_extension`,
@@ -262,8 +263,10 @@ async fn raw_key_server_path_completes_with_crate_pin_client() {
/// Suite 3b — N-4's interop trap, resolved (ADR-007): a raw-key client
/// identity presents its SPKI under the X.509 offer
/// (`only_raw_public_keys() == false` — the extension is an offer format,
/// not an identity statement), which `AcceptAnyCertVerifier` accepts; the
/// server extracts the client's `ed25519:` fingerprint end-to-end.
/// not an identity statement), which the server's default
/// `VerifyPresentedCertVerifier` accepts *and possession-verifies*
/// (ADR-008); the server extracts the client's `ed25519:` fingerprint
/// end-to-end.
#[tokio::test]
async fn raw_key_client_presents_spki_and_server_extracts_fingerprint() {
let dir = tempfile::tempdir().expect("tempdir");
@@ -297,3 +300,87 @@ async fn raw_key_client_presents_spki_and_server_extracts_fingerprint() {
"the server must extract the raw-key client's ed25519 fingerprint"
);
}
/// Suite 4 (ADR-008) — the possession-verifying default end-to-end: a
/// raw-key client pins the raw-key server's `ed25519:` fingerprint
/// (branch-2 shape), presents its own SPKI under the X.509 offer, and
/// the default `VerifyPresentedCertVerifier` verifies the client's
/// CertificateVerify against that SPKI's public key — the server
/// extracts the client's `ed25519:` fingerprint, possession-checked,
/// on the crate's default configuration. (A raw-key *client* without
/// the pin stays fail-closed against a raw-key server — the server
/// cert-type offer comes from the verifier, suite 2's rule.)
#[tokio::test]
async fn raw_key_client_vs_raw_key_server_default_verifier_checks_possession() {
let server_key = Ed25519SecretKey::generate();
let server_public: [u8; 32] = server_key.public().to_bytes();
let server_spki =
rustls::sign::public_key_to_spki(&rustls::pki_types::alg_id::ED25519, server_public)
.to_vec();
let server_fp = fingerprint_from_cert_der(&server_spki).expect("server fp");
let acceptor = server_config(&TlsIdentity::RawKey(server_key.clone()))
.await
.for_tcp_tls();
let client_key = Ed25519SecretKey::generate();
let client_public: [u8; 32] = client_key.public().to_bytes();
let credentials = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::RawKey(client_key))
.with_remote_identity(RemoteIdentity {
fingerprint: server_fp,
});
let server_seen = round_trip(acceptor, connector(&credentials)).await.expect(
"a pinning raw-key client must complete against a raw-key server \
whose default verifier possession-checks the presented SPKI",
);
let expected = format!("ed25519:{}", hex::encode(client_public));
assert_eq!(
server_seen.as_deref(),
Some(expected.as_str()),
"the possession-verifying default must accept the legit raw-key \
client and extract its fingerprint"
);
}
/// Suite 4b (ADR-008) — an X.509 client (self-signed chain, the
/// documented presentation) completes against the verifying default:
/// the CertificateVerify is verified through the standard X.509 route.
#[tokio::test]
async fn x509_client_vs_default_verifier_possession_checked() {
let dir = tempfile::tempdir().expect("tempdir");
let (server_cert, server_key, server_der) = write_x509_pair(dir.path());
let server_fp = fingerprint_from_cert_der(server_der.as_ref()).expect("server fp");
let acceptor = server_config(&TlsIdentity::X509 {
cert: server_cert.clone(),
key: server_key.clone(),
})
.await
.for_tcp_tls();
let (client_cert, client_key, client_der) = write_x509_pair(dir.path());
let client_fp = fingerprint_from_cert_der(client_der.as_ref()).expect("client fp");
let credentials = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::X509 {
cert: client_cert,
key: client_key,
})
.with_remote_identity(RemoteIdentity {
fingerprint: server_fp,
});
let server_seen = round_trip(acceptor, connector(&credentials))
.await
.expect("an X.509 client must complete against the verifying default");
assert_eq!(
server_seen.as_deref(),
Some(client_fp.as_str()),
"the verifying default must accept the legit X.509 client and \
extract its fingerprint"
);
}
+128 -39
View File
@@ -1,12 +1,16 @@
//! S-1 behavior pin (review 001): `AcceptAnyCertVerifier` performs no
//! proof-of-possession check — a handshake presenting the victim's public
//! cert bytes (X.509) or SPKI (RFC 7250) under an attacker-owned signer
//! completes, and the server extracts the victim's fingerprint.
//! S-1 posture pins (review 001 §S-1, resolved by ADR-008) — both
//! cert-type verifiers, both cert types:
//!
//! This test PINS the spoofable posture in both directions. If a future
//! change adds proof-of-possession (OQ-TLS-09 option (b)) or an upstream
//! rustls change enforces the signature, these tests fail and force the
//! `AcceptAnyCertVerifier` doc + OQ-TLS-09 update together.
//! 1. **Default** (`VerifyPresentedCertVerifier`, ADR-008): a handshake
//! presenting the victim's public cert bytes (X.509) or SPKI
//! (RFC 7250) under an attacker-owned signer **fails** the
//! CertificateVerify possession check — the attacker cannot
//! complete the handshake as the victim.
//! 2. **Escape hatch** (`AcceptAnyCertVerifier`, explicitly
//! installed): the same handshake completes and the server extracts
//! the victim's fingerprint — the documented no-pop posture of the
//! escape hatch, pinned so it cannot silently change either
//! direction.
//!
//! Gated on `tcp` (tokio-rustls provides the duplex-driven handshake;
//! no external transport is involved).
@@ -25,12 +29,6 @@ use alktls::{Ed25519SecretKey, TlsServerConfig};
const ALPN: &[u8] = b"alk/impersonation";
async fn server_config() -> alktls::TlsServerConfig {
TlsServerConfig::new(&alktls::TlsIdentity::SelfSigned, &[ALPN.to_vec()])
.await
.expect("server config must construct")
}
/// The attacker client: presents `presented_cert` (the victim's public
/// bytes) with the attacker's own Ed25519 signer, and accepts any server
/// cert (the probe stays independent of the crate's pin path).
@@ -134,13 +132,15 @@ fn attacker_client_config(
config
}
/// Drive the attacker handshake against `acceptor`. Returns
/// `Ok(Option<fingerprint>)` when the handshake completed (the server
/// side's extracted fingerprint), `Err(message)` when either side
/// aborted.
async fn run_impersonation(
acceptor: tokio_rustls::TlsAcceptor,
presented_cert: CertificateDer<'static>,
attacker_signer: &Ed25519SecretKey,
) -> Option<String> {
let server_config = server_config().await;
let acceptor = server_config.for_tcp_tls();
) -> Result<Option<String>, String> {
let client_config = attacker_client_config(presented_cert, attacker_signer);
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config));
@@ -155,30 +155,53 @@ async fn run_impersonation(
acceptor.accept(server_io),
);
let mut client_stream = client.expect("attacker handshake must complete (the S-1 posture)");
let mut server_stream = server.expect("server side of the handshake must complete");
let mut client_stream = client.map_err(|e| format!("client: {e}"))?;
let mut server_stream = server.map_err(|e| format!("server: {e}"))?;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
client_stream
.write_all(b"spoof")
.await
.expect("application data must flow after the spoofed handshake");
.map_err(|e| format!("client write: {e}"))?;
let mut buf = [0u8; 5];
server_stream
.read_exact(&mut buf)
.await
.expect("the server must read the attacker's application data");
.map_err(|e| format!("server read: {e}"))?;
let (_, server_conn) = server_stream.get_ref();
server_conn
Ok(server_conn
.peer_certificates()
.and_then(|certs| certs.first().map(|c| fingerprint_from_cert_der(c.as_ref())))
.flatten()
.flatten())
}
async fn default_acceptor() -> tokio_rustls::TlsAcceptor {
TlsServerConfig::new(&alktls::TlsIdentity::SelfSigned, &[ALPN.to_vec()])
.await
.expect("server config must construct")
.for_tcp_tls()
}
/// An explicit escape-hatch server: the crate's SelfSigned identity with
/// `AcceptAnyCertVerifier` installed in place of the default (the
/// documented no-pop opt-out, built through the public config builder).
fn escape_hatch_acceptor() -> tokio_rustls::TlsAcceptor {
let cert = alktls::generate_self_signed_cert().expect("self-signed cert generates");
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let mut config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("protocol versions")
.with_client_cert_verifier(Arc::new(alktls::AcceptAnyCertVerifier))
.with_single_cert(cert.cert_chain, cert.private_key)
.expect("self-signed server cert installs");
config.alpn_protocols = vec![ALPN.to_vec()];
tokio_rustls::TlsAcceptor::from(Arc::new(config))
}
#[tokio::test]
async fn x509_victim_cert_with_attacker_key_completes_and_fingerprint_is_victims() {
async fn default_verifier_rejects_x509_victim_cert_with_attacker_key() {
let victim_key_pair = rcgen::KeyPair::generate().expect("victim key gen");
let victim_cert = rcgen::CertificateParams::default()
.self_signed(&victim_key_pair)
@@ -191,19 +214,22 @@ async fn x509_victim_cert_with_attacker_key_completes_and_fingerprint_is_victims
let attacker_signer = Ed25519SecretKey::generate();
let server_seen = run_impersonation(victim_cert_der, &attacker_signer).await;
let result =
run_impersonation(default_acceptor().await, victim_cert_der, &attacker_signer).await;
assert_eq!(
server_seen.as_deref(),
Some(victim_fingerprint.as_str()),
"S-1: the server must extract the VICTIM's fingerprint from a \
handshake the attacker completed with its own key — the no-pop \
posture is the documented behavior (OQ-TLS-09)"
let err = result.expect_err(
"ADR-008: the default verifier must reject a handshake that presents \
the victim's X.509 cert under the attacker's key",
);
assert!(
err.contains("invalid peer certificate"),
"the possession check must fail the handshake with a certificate \
error, got: {err}"
);
}
#[tokio::test]
async fn raw_key_victim_spki_with_attacker_key_completes_and_fingerprint_is_victims() {
async fn default_verifier_rejects_raw_key_victim_spki_with_attacker_key() {
let victim_key = Ed25519SecretKey::generate();
let victim_public_bytes: [u8; 32] = victim_key.public().to_bytes();
let victim_spki_der =
@@ -221,15 +247,78 @@ async fn raw_key_victim_spki_with_attacker_key_completes_and_fingerprint_is_vict
"the attacker must hold a different key than the victim"
);
let server_seen =
run_impersonation(CertificateDer::from(victim_spki_der), &attacker_signer).await;
let result = run_impersonation(
default_acceptor().await,
CertificateDer::from(victim_spki_der),
&attacker_signer,
)
.await;
let err = result.expect_err(
"ADR-008: the default verifier must reject a handshake that presents \
the victim's SPKI under the attacker's key",
);
assert!(
err.contains("invalid peer certificate"),
"the possession check must fail the handshake with a certificate \
error, got: {err}"
);
}
#[tokio::test]
async fn escape_hatch_x509_victim_cert_completes_and_fingerprint_is_victims() {
let victim_key_pair = rcgen::KeyPair::generate().expect("victim key gen");
let victim_cert = rcgen::CertificateParams::default()
.self_signed(&victim_key_pair)
.expect("victim cert");
let victim_cert_der = victim_cert.der().clone();
let victim_fingerprint = fingerprint_from_cert_der(victim_cert_der.as_ref())
.expect("fingerprint of the victim cert");
let attacker_signer = Ed25519SecretKey::generate();
let server_seen = run_impersonation(escape_hatch_acceptor(), victim_cert_der, &attacker_signer)
.await
.expect("the escape-hatch verifier must accept the no-pop handshake");
assert_eq!(
server_seen.as_deref(),
Some(victim_fingerprint.as_str()),
"S-1 (RFC 7250 variant): the server must extract the VICTIM's \
ed25519 fingerprint from a handshake the attacker completed with \
its own signer — the spoofable posture is the documented behavior \
(OQ-TLS-09)"
"S-1 (escape hatch): AcceptAnyCertVerifier performs no \
proof-of-possession check — the server extracts the VICTIM's \
fingerprint from a handshake the attacker completed with its own \
key. This is the documented, deliberately-installed posture."
);
}
#[tokio::test]
async fn escape_hatch_raw_key_victim_spki_completes_and_fingerprint_is_victims() {
let victim_key = Ed25519SecretKey::generate();
let victim_public_bytes: [u8; 32] = victim_key.public().to_bytes();
let victim_spki_der =
rustls::sign::public_key_to_spki(&rustls::pki_types::alg_id::ED25519, victim_public_bytes)
.to_vec();
let victim_fingerprint =
fingerprint_from_cert_der(&victim_spki_der).expect("fingerprint of the victim SPKI");
let attacker_signer = Ed25519SecretKey::generate();
let server_seen = run_impersonation(
escape_hatch_acceptor(),
CertificateDer::from(victim_spki_der),
&attacker_signer,
)
.await
.expect("the escape-hatch verifier must accept the no-pop handshake");
assert_eq!(
server_seen.as_deref(),
Some(victim_fingerprint.as_str()),
"S-1 (escape hatch, RFC 7250 variant): the server extracts the \
VICTIM's ed25519 fingerprint from a handshake the attacker \
completed with its own signer — the no-pop posture only exists \
where AcceptAnyCertVerifier is explicitly installed."
);
}
+1 -1
View File
@@ -188,7 +188,7 @@ fn server_paths_carry_max_early_data_and_alpn() {
assert!(
!format!("{config:?}").is_empty(),
"the config carries its verifier through the derived Debug (request-but-don't-require \
shape asserted at unit level on AcceptAnyCertVerifier itself)"
shape asserted at unit level on the verifiers themselves)"
);
}
}