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:
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -175,3 +236,8 @@ 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)).
|
||||
- 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)).
|
||||
@@ -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`):
|
||||
|
||||
@@ -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.
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user