task 2: handshake-level suites — pin, fail-closed, RFC 7250 paths executed (U-3)

tests/handshake_behavior.rs (tcp-gated, tokio duplex + tokio-rustls,
no new deps) turns the fail-closed / pin / raw-key language into
executed behavior:

- pin match: X.509 server + SHA256 pin -> handshake completes, app
  data round-trips, server extracts the client cert fingerprint
- pin mismatch: wrong pin -> handshake error (the pin IS the anchor)
- fail closed: remote_identity None + raw-key server -> HandshakeFailure
- raw-key server path end-to-end: completes with the iroh-shaped
  client verifier (requires_raw_public_keys == true); presented cert
  asserted to be the SPKI carrying the raw Ed25519 key
- N-4's interop trap executed: raw-key client resolver vs
  AcceptAnyCertVerifier -> IncorrectCertificateTypeExtension alert

Major finding, recorded as OQ-TLS-10 (open): a crate-built pin
client cannot reach a crate-built raw-key server over rustls TCP+TLS
— the raw-key resolver requires the client to offer [RawPublicKey]
server cert types, sent only when the client verifier overrides
requires_raw_public_keys() == true. FingerprintPinVerifier keeps the
trait default false (AcceptAnyCertVerifier too); iroh's verifier
overrides true on both sides. Gap inherited from alknet
(behavior-preserving); pinned both ways by the suite.

client.md / server.md carry the interop notes; task file updated
(premise adjustments documented in Notes, summary filled).

Verification: 81 default / 91 tcp / 99 all-features tests green
(+5 new), clippy -D warnings clean both configs, fmt clean,
cargo doc warning-free, taskgraph validate 14 tasks.
This commit is contained in:
2026-09-11 08:17:28 +00:00
parent d82956385b
commit 4a4fae64af
5 changed files with 511 additions and 7 deletions
+14 -1
View File
@@ -1,6 +1,6 @@
---
status: reviewed
last_updated: 2026-09-10
last_updated: 2026-09-11
---
# alktls — Client side
@@ -78,6 +78,19 @@ boundary in action.
**Fail-closed is structural**: known peer + fingerprint → pin;
unknown + X.509 → CA; unknown + raw key → fail. No fourth path.
All three outcomes are **executed** in
`tests/handshake_behavior.rs` (real rustls handshakes over a duplex
pair, `tcp`-gated).
**RFC 7250 over TCP is a negotiation gap, not a verified path**
(OQ-TLS-10): `FingerprintPinVerifier` keeps rustls' trait-default
`requires_raw_public_keys() == false`, so a crate-built pin client
cannot reach a crate-built raw-key server — the handshake fails
closed (`HandshakeFailure`). The executed raw-key-over-TCP pin
(`raw_key_server_path_completes_with_requires_raw_verifier`) uses the
iroh-shaped verifier (`requires_raw_public_keys() == true`) any
raw-key-over-TCP consumer must bring. Raw-key peers that ride
iroh/noq use those transports' own TLS and are unaffected.
## `FingerprintPinVerifier`
+68 -2
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-09-10
last_updated: 2026-09-11
---
# Open Questions
@@ -22,6 +22,7 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
| 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-10 | RFC 7250 over TCP: cert-type negotiation gap | **open** | high |
## Identity & types
@@ -143,6 +144,66 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
src/client.rs (`FingerprintPinVerifier`),
docs/reviews/001-implementation-review.md §S-1, alknet ADR-034
### OQ-TLS-10: How do RFC 7250 raw-key peers negotiate over rustls-driven TCP+TLS?
- **Origin**: `tasks/handshake-tests.md` (review 001 §U-3 execution):
the executed handshake suites found that the crate's own pin client
cannot complete a handshake against the crate's own raw-key server,
and a raw-key client resolver cannot present itself to
`AcceptAnyCertVerifier` — both fail with rustls' HandshakeFailure
alert (cert-type negotiation), not a verification outcome. Verified
empirically and against the rustls 0.23.44 sources
(`server/hs.rs::process_cert_type_extension`,
`client/hs.rs::process_cert_type_extension`); pinned by
`tests/handshake_behavior.rs`.
- **Status**: open (recorded 2026-09-11)
- **Priority**: high
- **Mechanism** (rustls 0.23.44, verified):
- A raw-key *server* resolver (`only_raw_public_keys() == true`)
requires the client to offer `server_certificate_types =
[RawPublicKey]`. rustls' client sends that offer only when the
client verifier overrides `requires_raw_public_keys() == true`.
- This crate's `FingerprintPinVerifier` keeps the trait default
(`false`), and `AcceptAnyCertVerifier` never overrides it either.
- Consequently: crate-pin-client ↔ crate-raw-key-server fails; and a
raw-key *client* resolver (offering `[RawPublicKey]` client cert
types) fails against `AcceptAnyCertVerifier` (the
`(false, true, false)` arm → `IncorrectCertificateTypeExtension`).
The fail-closed rule still holds — no path downgrades — but the
raw-key-over-TCP interop the extraction implies does not exist
yet.
- **Context**: the raw-key paths that work today are iroh's (its
built-in TLS overrides `requires_raw_public_keys() == true` on both
verifiers — `iroh/src/tls/verifier.rs`) and the QUIC path
(noq/iroh negotiate cert types differently from rustls' TCP
state machine). alknet's extracted client never negotiated
raw-key-over-TCP either (no override in alknet-tls) — behavior
preservation holds; the gap is inherited, not introduced.
- **Options**:
- **(a) Document the gap** — raw-key peers ride iroh/noq (their own
TLS stacks), not rustls TCP+TLS; TCP+TLS is the X.509 transport.
No code change; the handshake tests pin the executed behavior.
- **(b) Add a `requires_raw_public_keys() == true` override** on
`FingerprintPinVerifier` for `ed25519:` pins (the iroh shape).
Changes the pin verifier's negotiation: a pin client could reach
raw-key servers — but then an X.509 remote pinned by `SHA256:`
could no longer negotiate with the same client config (the offer
would exclude X509), so the two pin formats cannot share one
client config. An API-shape decision before the first consumer.
- **(c) Add a server-side verifier that overrides
`requires_raw_public_keys() == true`** (raw-key-only client auth),
additive like OQ-TLS-09's option (b); pairs with it if mandatory
raw-key client auth is wanted.
- **Constraints**: the executed behavior is pinned both ways by
`tests/handshake_behavior.rs` (`raw_key_server_path_completes_…`
with an iroh-shape verifier;
`raw_key_client_resolver_fails_against_accept_any_cert_verifier`) —
any decision must update those tests together with this OQ.
- **Cross-references**: src/client.rs (`FingerprintPinVerifier` — the
default `false`), src/server.rs (`RawKeyCertResolver`,
`AcceptAnyCertVerifier`), review 001 §N-4 (the client-resolver trap),
iroh `iroh/src/tls/verifier.rs` (the working prior art)
## Quality / process
### OQ-TLS-05: Test surface for the invariants
@@ -174,4 +235,9 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
- 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)).
or an API-shape call before the first consumer (option (b)).
- OQ-TLS-10 (RFC 7250 over TCP negotiation gap): open by design —
behavior-preserving (the gap is inherited from alknet); deciding
needs the first raw-key-over-TCP consumer in hand (options (b)/(c)
are API-shape decisions), or the X.509-only TCP posture is
documented as-is (option (a)).
+13 -1
View File
@@ -1,6 +1,6 @@
---
status: reviewed
last_updated: 2026-09-10
last_updated: 2026-09-11
---
# alktls — Server side
@@ -108,6 +108,18 @@ certificate: the SPKI DER (Ed25519 OID + 32-byte key) is the "cert",
`only_raw_public_keys() == true`, and the signing key is the shared
`Ed25519SigningKey` helper (`signing.rs`).
**Interop note (OQ-TLS-10, executed in
`tests/handshake_behavior.rs`)**: a raw-key server resolver requires
the client to offer `[RawPublicKey]` *server* cert types, which
rustls sends only when the client verifier overrides
`requires_raw_public_keys() == true` (iroh's verifier does; this
crate's server-side `AcceptAnyCertVerifier` also keeps the default
`false`, and a raw-key *client* resolver offering `[RawPublicKey]`
client cert types is rejected by it — N-4's trap, also executed).
Raw-key peers riding iroh/noq are unaffected (their TLS stacks own
the negotiation); rustls-driven TCP+TLS is the X.509 transport until
OQ-TLS-10 decides otherwise.
## The ACME path
For `TlsIdentity::Acme`, `new` (feature `acme`):
+64 -3
View File
@@ -1,7 +1,7 @@
---
id: handshake-tests
name: Handshake-level test suite — pin, fail-closed, raw-key path executed (U-3)
status: pending
status: completed
depends_on: []
scope: moderate
risk: medium
@@ -84,8 +84,69 @@ The four handshake-level gaps (review 001 §U-3):
## Notes
> Agent fills this during implementation.
> **Major finding during implementation (recorded as OQ-TLS-10):** the
> task's suite premises 1 and 3 did not hold as written. A temporary
> probe (the review's methodology, deleted after the run) found:
>
> - **A crate-built pin client cannot complete a handshake against a
> crate-built raw-key server** (HandshakeFailure). Mechanism,
> verified in the rustls 0.23.44 sources
> (`server/hs.rs::process_cert_type_extension`,
> `client/hs.rs::process_cert_type_extension`): the raw-key server
> resolver requires the client to offer
> `server_certificate_types = [RawPublicKey]`, which rustls sends
> only when the client verifier overrides
> `requires_raw_public_keys() == true`. `FingerprintPinVerifier`
> keeps the trait default `false` (as does
> `AcceptAnyCertVerifier`). iroh's verifier overrides `true` on both
> sides (`iroh/src/tls/verifier.rs`) — the working prior art. The
> gap is inherited from alknet (no override there either) —
> behavior-preserving, but the raw-key-over-TCP interop the
> extraction implies does not exist yet.
> - **Suite 3's premise adjusted**: the raw-key server path completes
> only with an iroh-shaped verifier (`requires_raw_public_keys()
> == true`); the suite pins that working shape (with
> `NoClientCertResolver` client auth) and asserts the presented
> SPKI carries the raw Ed25519 key.
> - **Suite 1's premise adjusted**: the crate pin client ↔ raw-key
> server shape was replaced by the pin client ↔ X.509 server
> (`SHA256:` pin, which the crate's verifier can negotiate), pin
> match + mismatch. The executed fail-closed (suite 2) is
> mechanically the same HandshakeFailure (raw-key server needs a
> raw-key offer the CA-path client never makes) — the alert is
> asserted, the no-downgrade outcome is what's pinned.
> - **N-4's trap executed as suite 3b**: raw-key client resolver ↔
> `AcceptAnyCertVerifier` fails (client offers
> `[RawPublicKey]` → `IncorrectCertificateTypeExtension`).
>
> OQ-TLS-10 recorded (open, options a/b/c, deferral rationale);
> client.md and server.md carry the interop notes.
## Summary
> Agent fills this on completion.
**Landed as `tests/handshake_behavior.rs`** (`tcp`-gated, 5 suites;
no new deps — tokio duplex + tokio-rustls under the existing feature):
1. `pin_match_completes_and_server_extracts_client_fingerprint`
X.509 server, client pins the matching `SHA256:` fingerprint →
handshake completes, app data round-trips, server extracts the
client cert's fingerprint (request-but-don't-require, executed).
2. `pin_mismatch_fails_the_handshake` — wrong pin → handshake error
carrying the pin verifier's mismatch message.
3. `unknown_raw_key_remote_fails_closed``remote_identity: None` +
raw-key server → HandshakeFailure (fail closed, executed).
4. `raw_key_server_path_completes_with_requires_raw_verifier` — the
RFC 7250 server path end-to-end with the iroh-shaped client
verifier: handshake completes, no client cert presented, and the
presented server cert is verified to be the SPKI carrying the raw
Ed25519 public key.
5. `raw_key_client_resolver_fails_against_accept_any_cert_verifier`
N-4's interop trap executed (raw-key client cert types rejected).
Plus **OQ-TLS-10** (open — the cert-type negotiation gap above),
server.md/client.md synced, frontmatter → completed.
**Verification:** 81 default / 91 tcp / 99 all-features tests green
(+5 new handshake suites), clippy `-D warnings` clean (default +
all-features), fmt clean, `cargo doc` warning-free, `taskgraph
validate` 14 tasks.
+352
View File
@@ -0,0 +1,352 @@
//! U-3 executed handshake suites (review 001): the pin path, the
//! fail-closed rule, and the RFC 7250 raw-key server path as *executed*
//! behavior — not structural config assertions. A duplex pair +
//! `tokio-rustls` drives real rustls handshakes; no external transport
//! is involved (ADR-006's boundary is not crossed).
//!
//! Verified against rustls 0.23.44's cert-type negotiation
//! (`server/hs.rs::process_cert_type_extension`,
//! `client/hs.rs::process_cert_type_extension`) and pinned here:
//!
//! - An X.509 server negotiates with any client not offering a raw-key
//! client resolver; the fingerprint pin executes on the presented cert.
//! - A raw-key server (resolver `only_raw_public_keys() == true`)
//! requires the client to offer `[RawPublicKey]` *server* cert types,
//! which rustls sends only when the client verifier overrides
//! `requires_raw_public_keys() == true` (the iroh prior-art shape,
//! `iroh/src/tls/verifier.rs`). This crate's `FingerprintPinVerifier`
//! keeps the trait default `false`, so a crate-built pin client
//! cannot reach a crate-built raw-key server — the handshake fails
//! closed (`HandshakeFailure`). Recorded as OQ-TLS-10.
//! - A raw-key *client* resolver offers `[RawPublicKey]` client cert
//! types, which `AcceptAnyCertVerifier` (`requires_raw_public_keys()
//! == false`) rejects — N-4's interop trap, executed.
#![cfg(feature = "tcp")]
use std::path::PathBuf;
use std::sync::Arc;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::DigitallySignedStruct;
use tokio::io::duplex;
use alktls::fingerprint_from_cert_der;
use alktls::{
ConnectionCredentials, Ed25519SecretKey, RemoteIdentity, TlsClientConfig, TlsIdentity,
TlsServerConfig,
};
const ALPN: &[u8] = b"alk/handshake";
fn write_x509_pair(dir: &std::path::Path) -> (PathBuf, PathBuf, CertificateDer<'static>) {
let key_pair = rcgen::KeyPair::generate().expect("key gen");
let cert = rcgen::CertificateParams::default()
.self_signed(&key_pair)
.expect("self-signed cert");
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
std::fs::write(&cert_path, cert.pem()).expect("write cert pem");
std::fs::write(&key_path, key_pair.serialize_pem()).expect("write key pem");
(cert_path, key_path, cert.der().clone())
}
async fn server_config(identity: &TlsIdentity) -> TlsServerConfig {
TlsServerConfig::new(identity, &[ALPN.to_vec()])
.await
.expect("server config must construct")
}
fn connector(credentials: &ConnectionCredentials) -> tokio_rustls::TlsConnector {
let config = TlsClientConfig::new(credentials, ALPN).expect("client config must construct");
tokio_rustls::TlsConnector::from(Arc::new(config.into_rustls_config()))
}
/// Drive a full handshake over a duplex pair and one application-data
/// round-trip. Returns the fingerprint the server extracted from the
/// client's presented cert (`None` when no client cert was presented),
/// or the error string from whichever side failed first.
async fn round_trip(
acceptor: tokio_rustls::TlsAcceptor,
connector: tokio_rustls::TlsConnector,
) -> Result<Option<String>, String> {
let (client_io, server_io) = duplex(64 * 1024);
let server_name = ServerName::try_from("handshake.test".to_string())
.expect("dns name")
.to_owned();
let (client, server) = tokio::join!(
connector.connect(server_name, client_io),
acceptor.accept(server_io),
);
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"ping")
.await
.map_err(|e| format!("client write: {e}"))?;
let mut buf = [0u8; 4];
server_stream
.read_exact(&mut buf)
.await
.map_err(|e| format!("server read: {e}"))?;
let (_, conn) = server_stream.get_ref();
Ok(conn
.peer_certificates()
.and_then(|certs| certs.first().map(|c| fingerprint_from_cert_der(c.as_ref())))
.flatten())
}
/// Suite 1 — the pin path end-to-end: a client pinning the server's
/// `SHA256:<hex>` cert fingerprint handshakes the matching X.509 server;
/// app data flows; the server extracts the client cert's fingerprint.
#[tokio::test]
async fn pin_match_completes_and_server_extracts_client_fingerprint() {
let dir = tempfile::tempdir().expect("tempdir");
let (cert_path, key_path, cert_der) = write_x509_pair(dir.path());
let server_fp = fingerprint_from_cert_der(cert_der.as_ref()).expect("server fp");
assert!(server_fp.starts_with("SHA256:"));
let acceptor = server_config(&TlsIdentity::X509 {
cert: cert_path.clone(),
key: key_path.clone(),
})
.await
.for_tcp_tls();
let client_fp = fingerprint_from_cert_der(cert_der.as_ref()).expect("client fp");
let credentials = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::X509 {
cert: cert_path,
key: key_path,
})
.with_remote_identity(RemoteIdentity {
fingerprint: server_fp,
});
let server_seen = round_trip(acceptor, connector(&credentials))
.await
.expect("handshake with a matching pin must complete");
assert_eq!(
server_seen.as_deref(),
Some(client_fp.as_str()),
"the server must extract the client cert's fingerprint through the \
request-but-don't-require verifier"
);
}
/// Suite 1b — pin mismatch fails the handshake: same server, a pin for a
/// different cert rejects at handshake time (the pin IS the anchor).
#[tokio::test]
async fn pin_mismatch_fails_the_handshake() {
let dir = tempfile::tempdir().expect("tempdir");
let (cert_path, key_path, _) = write_x509_pair(dir.path());
let acceptor = server_config(&TlsIdentity::X509 {
cert: cert_path.clone(),
key: key_path.clone(),
})
.await
.for_tcp_tls();
let credentials = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::X509 {
cert: cert_path,
key: key_path,
})
.with_remote_identity(RemoteIdentity {
fingerprint: "SHA256:0000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
});
let result = round_trip(acceptor, connector(&credentials)).await;
let err = result.expect_err("a mismatched pin must fail the handshake");
assert!(
err.contains("fingerprint pin mismatch"),
"the pin verifier's mismatch error must surface at handshake, got: {err}"
);
}
/// Suite 2 — the fail-closed rule, executed: `remote_identity: None` (the
/// CA-verification path) against a raw-key server fails the handshake —
/// never a silent downgrade. Mechanically the raw-key server resolver
/// (`only_raw_public_keys() == true`) requires the client to offer
/// `[RawPublicKey]` server cert types; the CA-path verifier keeps the
/// `requires_raw_public_keys() == false` default and offers none, so
/// rustls aborts with a HandshakeFailure alert. Either way — no
/// negotiation or no CA — the raw-key remote fails closed.
#[tokio::test]
async fn unknown_raw_key_remote_fails_closed() {
let acceptor = server_config(&TlsIdentity::RawKey(Ed25519SecretKey::generate()))
.await
.for_tcp_tls();
let credentials = ConnectionCredentials::new();
let result = round_trip(acceptor, connector(&credentials)).await;
let err = result.expect_err("an unknown raw-key remote must fail closed");
assert!(
err.contains("HandshakeFailure"),
"the fail-closed abort must surface as a handshake alert, got: {err}"
);
}
/// Suite 3 — the RFC 7250 raw-key server path executed end-to-end: the
/// server presents its SPKI, the client pins the `ed25519:<hex>`
/// fingerprint and completes — with the one verifier shape rustls'
/// negotiation requires on the client side
/// (`requires_raw_public_keys() == true`; iroh's prior art,
/// `iroh/src/tls/verifier.rs`). The crate's own `FingerprintPinVerifier`
/// cannot negotiate this path (no override — OQ-TLS-10), so the custom
/// verifier here is the shape any raw-key-over-TCP consumer must bring.
#[tokio::test]
async fn raw_key_server_path_completes_with_requires_raw_verifier() {
#[derive(Debug)]
struct RequiresRawPinVerifier {
fingerprint: String,
supported: rustls::crypto::WebPkiSupportedAlgorithms,
presented: Arc<std::sync::Mutex<Option<Vec<u8>>>>,
}
impl ServerCertVerifier for RequiresRawPinVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
let presented =
fingerprint_from_cert_der(end_entity.as_ref()).ok_or(rustls::Error::General(
"raw-key pin: failed to fingerprint the presented cert".to_string(),
))?;
if presented == self.fingerprint {
*self.presented.lock().unwrap_or_else(|e| e.into_inner()) =
Some(end_entity.as_ref().to_vec());
Ok(rustls::client::danger::ServerCertVerified::assertion())
} else {
Err(rustls::Error::General(format!(
"raw-key pin mismatch: expected {} got {}",
self.fingerprint, presented
)))
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Err(rustls::Error::General(
"raw-key pin: TLS 1.2 not exercised on this path".to_string(),
))
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature_with_raw_key(
message,
&rustls::pki_types::SubjectPublicKeyInfoDer::from(cert.as_ref().to_vec()),
dss,
&self.supported,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.supported.supported_schemes()
}
fn requires_raw_public_keys(&self) -> bool {
true
}
}
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");
assert!(server_fp.starts_with("ed25519:"));
let acceptor = server_config(&TlsIdentity::RawKey(server_key.clone()))
.await
.for_tcp_tls();
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let presented = Arc::new(std::sync::Mutex::new(None));
let verifier = RequiresRawPinVerifier {
fingerprint: server_fp,
supported: provider.signature_verification_algorithms,
presented: Arc::clone(&presented),
};
let mut config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("protocol versions")
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_client_cert_resolver(Arc::new(alktls::NoClientCertResolver));
config.alpn_protocols = vec![ALPN.to_vec()];
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
let server_seen = round_trip(acceptor, connector)
.await
.expect("the RFC 7250 raw-key handshake must complete");
assert!(
server_seen.is_none(),
"no client cert was presented; the server must see none, got: {server_seen:?}"
);
let presented = presented.lock().unwrap_or_else(|e| e.into_inner()).clone();
let presented = presented.expect("the verifier must have seen the server cert");
assert_eq!(
alktls::extract_ed25519_raw_key_from_spki(&presented),
Some(server_public),
"the server's presented cert must be the RFC 7250 SPKI carrying the \
raw Ed25519 public key"
);
}
/// Suite 3b — N-4's interop trap, executed: a crate-built raw-key *client*
/// resolver offers `client_certificate_types = [RawPublicKey]` only, which
/// `AcceptAnyCertVerifier` (`requires_raw_public_keys() == false`) rejects
/// with `IncorrectCertificateTypeExtension` → HandshakeFailure. A raw-key
/// client identity cannot present itself to this crate's own server today
/// (OQ-TLS-10). Pinned so any verifier change flags it.
#[tokio::test]
async fn raw_key_client_resolver_fails_against_accept_any_cert_verifier() {
let dir = tempfile::tempdir().expect("tempdir");
let (cert_path, key_path, _) = write_x509_pair(dir.path());
let acceptor = server_config(&TlsIdentity::X509 {
cert: cert_path,
key: key_path,
})
.await
.for_tcp_tls();
let credentials = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::RawKey(Ed25519SecretKey::generate()));
let result = round_trip(acceptor, connector(&credentials)).await;
let err = result.expect_err(
"a raw-key client resolver cannot negotiate client cert types with \
AcceptAnyCertVerifier (N-4)",
);
assert!(
err.contains("HandshakeFailure"),
"the cert-type negotiation failure must surface as a handshake alert, got: {err}"
);
}