//! S-1 posture pins (review 001 §S-1, resolved by ADR-008) — both //! cert-type verifiers, both cert types: //! //! 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). #![cfg(feature = "tcp")] 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::{Ed25519SecretKey, TlsServerConfig}; const ALPN: &[u8] = b"alk/impersonation"; /// 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). fn attacker_client_config( presented_cert: CertificateDer<'static>, attacker_signer: &Ed25519SecretKey, ) -> rustls::ClientConfig { struct AcceptAnyServerCertVerifier; impl std::fmt::Debug for AcceptAnyServerCertVerifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AcceptAnyServerCertVerifier").finish() } } impl ServerCertVerifier for AcceptAnyServerCertVerifier { fn verify_server_cert( &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>, _ocsp_response: &[u8], _now: UnixTime, ) -> Result { Ok(rustls::client::danger::ServerCertVerified::assertion()) } fn verify_tls12_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn verify_tls13_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn supported_verify_schemes(&self) -> Vec { 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, ] } } let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider()); let signing_key = Arc::new(alktls::Ed25519SigningKey::new(attacker_signer.clone())); let certified_key = Arc::new(rustls::sign::CertifiedKey::new( vec![presented_cert], signing_key, )); struct FixedResolver(Arc); impl rustls::client::ResolvesClientCert for FixedResolver { fn resolve( &self, _root_hint_subjects: &[&[u8]], _sigschemes: &[rustls::SignatureScheme], ) -> Option> { Some(Arc::clone(&self.0)) } fn only_raw_public_keys(&self) -> bool { false } fn has_certs(&self) -> bool { true } } impl std::fmt::Debug for FixedResolver { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("FixedResolver").finish() } } let mut config = rustls::ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() .expect("protocol versions") .dangerous() .with_custom_certificate_verifier(Arc::new(AcceptAnyServerCertVerifier)) .with_client_cert_resolver(Arc::new(FixedResolver(certified_key))); config.alpn_protocols = vec![ALPN.to_vec()]; config } /// Drive the attacker handshake against `acceptor`. Returns /// `Ok(Option)` 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, ) -> Result, String> { let client_config = attacker_client_config(presented_cert, attacker_signer); let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config)); let (client_io, server_io) = duplex(64 * 1024); let server_name = ServerName::try_from("impersonation.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"spoof") .await .map_err(|e| format!("client write: {e}"))?; let mut buf = [0u8; 5]; server_stream .read_exact(&mut buf) .await .map_err(|e| format!("server read: {e}"))?; let (_, server_conn) = server_stream.get_ref(); Ok(server_conn .peer_certificates() .and_then(|certs| certs.first().map(|c| fingerprint_from_cert_der(c.as_ref()))) .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 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) .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"); assert!(victim_fingerprint.starts_with("SHA256:")); let attacker_signer = Ed25519SecretKey::generate(); let result = run_impersonation(default_acceptor().await, victim_cert_der, &attacker_signer).await; 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 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 = 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"); assert!(victim_fingerprint.starts_with("ed25519:")); let attacker_signer = Ed25519SecretKey::generate(); assert_ne!( attacker_signer.public().to_bytes(), victim_public_bytes, "the attacker must hold a different key than the victim" ); 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 (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." ); }