diff --git a/src/lib.rs b/src/lib.rs index 1db6bfd1..1e7beaf1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,6 @@ mod quic_endpoint; mod quic_server; mod quic_stream; mod reality; -mod reality_client_handler; pub mod resolver; pub mod resources; mod routing; diff --git a/src/main.rs b/src/main.rs index 18e82ef0..543580d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,6 @@ mod quic_endpoint; mod quic_server; mod quic_stream; mod reality; -mod reality_client_handler; mod resolver; mod resources; mod routing; diff --git a/src/reality/mod.rs b/src/reality/mod.rs index 9d24285f..3dba80ce 100644 --- a/src/reality/mod.rs +++ b/src/reality/mod.rs @@ -2,37 +2,36 @@ mod reality_auth; mod reality_certificate; +mod reality_certificate_verify; mod reality_cipher_suite; mod reality_client; -mod reality_client_verify; -mod reality_key_exchange; +mod reality_client_handler; +mod reality_config; +mod reality_hello; mod reality_mldsa; -mod reality_rustls; +mod reality_rustls_adapter; mod reality_server; mod reality_server_handler; -mod reality_target_template; -mod reality_util; +mod reality_target_flight; #[cfg(test)] -mod test_tls; - +mod tests; #[cfg(test)] -mod reality_pq_tests; -#[cfg(test)] -mod reality_rustls_tests; -#[cfg(test)] -pub(crate) use reality_pq_tests::{default_target_records, established_pair}; +pub(crate) use tests::reality_fixtures::{default_target_records, established_pair}; pub use reality_cipher_suite::{CipherSuite, DEFAULT_CIPHER_SUITES}; +pub(crate) use reality_config::parse_server_name; #[allow(unused_imports)] pub use reality_mldsa::{ VerifyKey as Mldsa65VerifyKey, decode_mldsa65_seed, decode_mldsa65_verify, generate_mldsa65_keypair, mldsa65_signing_key, }; -pub(crate) use reality_util::parse_server_name; // generate_keypair is used by binary, not by library's public API #[allow(unused_imports)] -pub use reality_util::{decode_private_key, decode_public_key, decode_short_id, generate_keypair}; +pub use reality_config::{ + decode_private_key, decode_public_key, decode_short_id, generate_keypair, +}; pub use reality_client::RealityClientConfig; +pub use reality_client_handler::RealityClientHandler; pub use reality_server::RealityServerConfig; pub use reality_server_handler::{RealityServerTarget, setup_reality_server_stream}; diff --git a/src/reality/reality_certificate.rs b/src/reality/reality_certificate.rs index 00b177a1..33b31ef3 100644 --- a/src/reality/reality_certificate.rs +++ b/src/reality/reality_certificate.rs @@ -104,14 +104,14 @@ pub fn generate_hmac_certificate( #[cfg(test)] mod tests { + use super::super::reality_certificate_verify::{ + verify_certificate_hmac, verify_certificate_mldsa, + }; use super::*; use aws_lc_rs::signature::KeyPair; #[test] fn malformed_mldsa_extensions_fail_even_with_valid_hmac() { - use super::super::reality_client_verify::{ - verify_certificate_hmac, verify_certificate_mldsa, - }; use super::super::reality_mldsa; let (seed, verify) = crate::reality::generate_mldsa65_keypair().unwrap(); @@ -170,23 +170,9 @@ mod tests { assert_eq!(extensions[0].oid.as_bytes(), [0]); assert!(!extensions[0].critical); assert_eq!(extensions[0].value.len(), 3309); - super::super::reality_client_verify::verify_certificate_hmac(cert.der(), &auth).unwrap(); - super::super::reality_client_verify::verify_certificate_mldsa( - cert.der(), - &auth, - b"clientserver", - &key, - ) - .unwrap(); - assert!( - super::super::reality_client_verify::verify_certificate_mldsa( - cert.der(), - &auth, - b"Clientserver", - &key - ) - .is_err() - ); + verify_certificate_hmac(cert.der(), &auth).unwrap(); + verify_certificate_mldsa(cert.der(), &auth, b"clientserver", &key).unwrap(); + assert!(verify_certificate_mldsa(cert.der(), &auth, b"Clientserver", &key).is_err()); } /// Find the signature bytes in a DER-encoded certificate. diff --git a/src/reality/reality_client_verify.rs b/src/reality/reality_certificate_verify.rs similarity index 100% rename from src/reality/reality_client_verify.rs rename to src/reality/reality_certificate_verify.rs diff --git a/src/reality/reality_client.rs b/src/reality/reality_client.rs index fb34977b..65efb598 100644 --- a/src/reality/reality_client.rs +++ b/src/reality/reality_client.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use rustls::pki_types::ServerName; use super::CipherSuite; -use super::reality_rustls::{client_authentication, provider}; +use super::reality_rustls_adapter::{client_authentication, provider}; use crate::crypto::{TlsConnection, tls_error}; #[derive(Clone)] diff --git a/src/reality_client_handler.rs b/src/reality/reality_client_handler.rs similarity index 96% rename from src/reality_client_handler.rs rename to src/reality/reality_client_handler.rs index d13d65f5..3745475d 100644 --- a/src/reality_client_handler.rs +++ b/src/reality/reality_client_handler.rs @@ -2,15 +2,15 @@ use async_trait::async_trait; +use super::{CipherSuite, Mldsa65VerifyKey, RealityClientConfig}; use crate::address::ResolvedLocation; use crate::async_stream::AsyncStream; use crate::crypto::{CryptoTlsStream, TlsReadMode}; -use crate::reality::{CipherSuite, RealityClientConfig}; use crate::tcp::tcp_handler::{TcpClientHandler, TcpClientSetupResult}; #[derive(Debug)] pub struct RealityClientHandler { - mldsa65_verify: Option, + mldsa65_verify: Option, public_key: [u8; 32], short_id: [u8; 8], server_name: rustls::pki_types::ServerName<'static>, @@ -62,7 +62,7 @@ impl RealityClientHandler { } } - pub fn with_mldsa65_verify(mut self, key: Option) -> Self { + pub fn with_mldsa65_verify(mut self, key: Option) -> Self { self.mldsa65_verify = key; self } diff --git a/src/reality/reality_util.rs b/src/reality/reality_config.rs similarity index 65% rename from src/reality/reality_util.rs rename to src/reality/reality_config.rs index 394a1a6d..f8f05c51 100644 --- a/src/reality/reality_util.rs +++ b/src/reality/reality_config.rs @@ -110,63 +110,6 @@ fn hex_char_to_value(c: u8) -> Result { } } -/// Extracts ClientRandom from ClientHello -/// -/// ClientHello structure (simplified): -/// - TLS Header: 5 bytes -/// - Handshake Type: 1 byte -/// - Length: 3 bytes -/// - Protocol Version: 2 bytes -/// - ClientRandom: 32 bytes (starts at offset 11) -pub fn extract_client_random(client_hello: &[u8]) -> Result<[u8; 32], std::io::Error> { - const TLS_HEADER_LEN: usize = 5; - const HANDSHAKE_HEADER_LEN: usize = 4; // type(1) + length(3) - const PROTOCOL_VERSION_LEN: usize = 2; - const RANDOM_OFFSET: usize = TLS_HEADER_LEN + HANDSHAKE_HEADER_LEN + PROTOCOL_VERSION_LEN; - - if client_hello.len() < RANDOM_OFFSET + 32 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "ClientHello too short to extract random", - )); - } - - let mut random = [0u8; 32]; - random.copy_from_slice(&client_hello[RANDOM_OFFSET..RANDOM_OFFSET + 32]); - Ok(random) -} - -/// Extracts SessionId slice from ClientHello without allocation -/// -/// SessionId comes after ClientRandom and has a 1-byte length prefix. -/// Returns a slice pointing into the original buffer. -pub fn extract_session_id_slice(client_hello: &[u8]) -> Result<&[u8], std::io::Error> { - const TLS_HEADER_LEN: usize = 5; - const HANDSHAKE_HEADER_LEN: usize = 4; - const PROTOCOL_VERSION_LEN: usize = 2; - const RANDOM_LEN: usize = 32; - const SESSION_ID_LEN_OFFSET: usize = - TLS_HEADER_LEN + HANDSHAKE_HEADER_LEN + PROTOCOL_VERSION_LEN + RANDOM_LEN; - - if client_hello.len() < SESSION_ID_LEN_OFFSET + 1 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "ClientHello too short to extract session ID", - )); - } - - let session_id_len = client_hello[SESSION_ID_LEN_OFFSET] as usize; - - if client_hello.len() < SESSION_ID_LEN_OFFSET + 1 + session_id_len { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "ClientHello session ID extends past end", - )); - } - - Ok(&client_hello[SESSION_ID_LEN_OFFSET + 1..SESSION_ID_LEN_OFFSET + 1 + session_id_len]) -} - pub fn generate_keypair() -> std::io::Result<(String, String)> { // Generate 32 random bytes for private key let mut private_key_bytes = [0u8; 32]; @@ -212,34 +155,6 @@ mod tests { assert!(result.is_err()); } - #[test] - fn test_extract_client_random() { - // Create a minimal ClientHello with random - let mut client_hello = vec![0u8; 100]; - - // TLS Header - client_hello[0] = 0x16; // Handshake - client_hello[1] = 0x03; // Version major - client_hello[2] = 0x03; // Version minor (TLS 1.2) - - // Handshake header - client_hello[5] = 0x01; // ClientHello type - - // Protocol version in handshake - client_hello[9] = 0x03; // Major - client_hello[10] = 0x03; // Minor - - // ClientRandom starts at offset 11 - for i in 0..32 { - client_hello[11 + i] = (i + 1) as u8; - } - - let random = extract_client_random(&client_hello).unwrap(); - for (i, byte) in random.iter().enumerate() { - assert_eq!(*byte, (i + 1) as u8); - } - } - #[test] fn test_decode_public_key() { use base64::engine::{Engine as _, general_purpose::URL_SAFE_NO_PAD}; diff --git a/src/reality/reality_key_exchange.rs b/src/reality/reality_hello.rs similarity index 76% rename from src/reality/reality_key_exchange.rs rename to src/reality/reality_hello.rs index 82f21974..d11299ce 100644 --- a/src/reality/reality_key_exchange.rs +++ b/src/reality/reality_hello.rs @@ -173,10 +173,88 @@ pub fn validate_server_key_share(record: &[u8]) -> io::Result<()> { Ok(()) } +/// Extracts ClientRandom from ClientHello +/// +/// ClientHello structure (simplified): +/// - TLS Header: 5 bytes +/// - Handshake Type: 1 byte +/// - Length: 3 bytes +/// - Protocol Version: 2 bytes +/// - ClientRandom: 32 bytes (starts at offset 11) +pub fn extract_client_random(client_hello: &[u8]) -> io::Result<[u8; 32]> { + const TLS_HEADER_LEN: usize = 5; + const HANDSHAKE_HEADER_LEN: usize = 4; // type(1) + length(3) + const PROTOCOL_VERSION_LEN: usize = 2; + const RANDOM_OFFSET: usize = TLS_HEADER_LEN + HANDSHAKE_HEADER_LEN + PROTOCOL_VERSION_LEN; + + if client_hello.len() < RANDOM_OFFSET + 32 { + return Err(invalid("ClientHello too short to extract random")); + } + + let mut random = [0u8; 32]; + random.copy_from_slice(&client_hello[RANDOM_OFFSET..RANDOM_OFFSET + 32]); + Ok(random) +} + +/// Extracts SessionId slice from ClientHello without allocation +/// +/// SessionId comes after ClientRandom and has a 1-byte length prefix. +/// Returns a slice pointing into the original buffer. +pub fn extract_session_id_slice(client_hello: &[u8]) -> io::Result<&[u8]> { + const TLS_HEADER_LEN: usize = 5; + const HANDSHAKE_HEADER_LEN: usize = 4; + const PROTOCOL_VERSION_LEN: usize = 2; + const RANDOM_LEN: usize = 32; + const SESSION_ID_LEN_OFFSET: usize = + TLS_HEADER_LEN + HANDSHAKE_HEADER_LEN + PROTOCOL_VERSION_LEN + RANDOM_LEN; + + if client_hello.len() < SESSION_ID_LEN_OFFSET + 1 { + return Err(invalid("ClientHello too short to extract session ID")); + } + + let session_id_len = client_hello[SESSION_ID_LEN_OFFSET] as usize; + + if client_hello.len() < SESSION_ID_LEN_OFFSET + 1 + session_id_len { + return Err(invalid("ClientHello session ID extends past end")); + } + + Ok(&client_hello[SESSION_ID_LEN_OFFSET + 1..SESSION_ID_LEN_OFFSET + 1 + session_id_len]) +} + #[cfg(test)] mod tests { - use super::super::test_tls::{construct_server_hello, write_record_header}; use super::*; + use crate::reality::tests::reality_fixtures::{ + configs, construct_server_hello, write_record_header, + }; + + #[test] + fn test_extract_client_random() { + // Create a minimal ClientHello with random + let mut client_hello = vec![0u8; 100]; + + // TLS Header + client_hello[0] = 0x16; // Handshake + client_hello[1] = 0x03; // Version major + client_hello[2] = 0x03; // Version minor (TLS 1.2) + + // Handshake header + client_hello[5] = 0x01; // ClientHello type + + // Protocol version in handshake + client_hello[9] = 0x03; // Major + client_hello[10] = 0x03; // Minor + + // ClientRandom starts at offset 11 + for i in 0..32 { + client_hello[11 + i] = (i + 1) as u8; + } + + let random = extract_client_random(&client_hello).unwrap(); + for (i, byte) in random.iter().enumerate() { + assert_eq!(*byte, (i + 1) as u8); + } + } fn share_vector(entries: &[(u16, &[u8])]) -> Vec { let mut data = vec![0, 0]; @@ -239,8 +317,7 @@ mod tests { #[test] fn hello_parsers_check_framing_and_retry() { - let (config, _) = - super::super::reality_pq_tests::configs(super::super::CipherSuite::AES_128_GCM_SHA256); + let (config, _) = configs(super::super::CipherSuite::AES_128_GCM_SHA256); let mut client = config.create_connection().unwrap(); let mut record = Vec::new(); client.write_tls(&mut record).unwrap(); diff --git a/src/reality/reality_pq_tests.rs b/src/reality/reality_pq_tests.rs deleted file mode 100644 index a2fd8a46..00000000 --- a/src/reality/reality_pq_tests.rs +++ /dev/null @@ -1,334 +0,0 @@ -use std::io::{Read, Write}; - -use crate::crypto::TlsConnection; -use aws_lc_rs::agreement; -use bytes::Bytes; - -use super::reality_key_exchange::{RealityGroup, extract_client_key_shares}; -use super::test_tls::{construct_server_hello, write_record_header}; -use super::*; - -fn public_key(private: &[u8; 32]) -> [u8; 32] { - agreement::PrivateKey::from_private_key(&agreement::X25519, private) - .unwrap() - .compute_public_key() - .unwrap() - .as_ref() - .try_into() - .unwrap() -} - -pub(super) fn configs(cipher: CipherSuite) -> (RealityClientConfig, RealityServerConfig) { - let private = [42; 32]; - ( - RealityClientConfig { - mldsa65_verify: None, - public_key: public_key(&private), - short_id: [1; 8], - server_name: "cover.test".try_into().unwrap(), - cipher_suites: vec![cipher], - }, - RealityServerConfig { - mldsa65_key: None, - private_key: private, - short_ids: vec![[1; 8]], - dest: crate::address::NetLocation::from_str("cover.test:443", None).unwrap(), - max_time_diff: None, - min_client_version: None, - max_client_version: None, - cipher_suites: vec![cipher], - }, - ) -} - -pub(super) fn target_records( - group: RealityGroup, - cipher: CipherSuite, - session: &[u8], -) -> Vec { - let hello = construct_server_hello( - &[3; 32], - session, - cipher.id(), - group, - &vec![0; group.share_len(false)], - ) - .unwrap(); - let mut record = write_record_header(22, hello.len() as u16); - record.extend_from_slice(&hello); - let mut encrypted = write_record_header(23, 6000); - encrypted.resize(6005, 0); - vec![ - record.into(), - Bytes::from_static(&[20, 3, 3, 0, 1, 1]), - encrypted.into(), - ] -} - -pub(crate) fn default_target_records(session: &[u8]) -> Vec { - target_records( - RealityGroup::X25519MlKem768, - CipherSuite::AES_128_GCM_SHA256, - session, - ) -} - -pub(super) fn separate_target_records( - group: RealityGroup, - cipher: CipherSuite, - session: &[u8], -) -> Vec { - let mut records = target_records(group, cipher, session); - records.truncate(2); - for payload_len in [32, 4096, 128, 96] { - let mut record = write_record_header(23, payload_len); - record.resize(5 + payload_len as usize, 0); - records.push(record.into()); - } - records -} - -pub(super) fn target_with_unknown_extension(record: &Bytes) -> Bytes { - let mut hello = record.to_vec(); - let extensions_offset = 47 + usize::from(hello[43]); - let extensions_len = - u16::from_be_bytes([hello[extensions_offset], hello[extensions_offset + 1]]); - hello.extend_from_slice(&[0xfe, 0x30, 0, 0]); - hello[extensions_offset..extensions_offset + 2] - .copy_from_slice(&(extensions_len + 4).to_be_bytes()); - let record_len = (hello.len() - 5) as u16; - hello[3..5].copy_from_slice(&record_len.to_be_bytes()); - let message_len = (hello.len() - 9) as u32; - hello[6..9].copy_from_slice(&message_len.to_be_bytes()[1..]); - assert!( - crate::shadow_tls::parse_server_hello(&hello) - .unwrap() - .is_tls13 - ); - super::reality_key_exchange::validate_server_key_share(&hello).unwrap(); - hello.into() -} - -fn handshake( - client_config: RealityClientConfig, - server_config: RealityServerConfig, - group: RealityGroup, - make_target_records: fn(RealityGroup, CipherSuite, &[u8]) -> Vec, - fragment: usize, -) -> (TlsConnection, TlsConnection) { - let cipher = client_config.cipher_suites[0]; - let mut client = client_config.create_connection().unwrap(); - let mut hello = Vec::new(); - client.write_tls(&mut hello).unwrap(); - let shares = extract_client_key_shares(&hello).unwrap(); - assert!(shares.hybrid.is_some()); - assert!(shares.x25519.is_some()); - let authenticated = server_config.authenticate_client_hello(&hello).unwrap(); - let target = make_target_records(group, cipher, &hello[44..76]); - let mut server = authenticated.into_connection(&target).unwrap(); - let mut response = Vec::new(); - server.write_tls(&mut response).unwrap(); - let mut deframer = crate::crypto::tls_deframer::TlsDeframer::new(); - deframer.feed(&response); - let records = deframer.next_records().unwrap(); - assert_eq!(records.len(), target.len()); - for (actual, expected) in records.iter().zip(&target) { - assert_eq!(actual.len(), expected.len()); - } - let share_start = target[0].len() - group.share_len(false); - assert_eq!(&records[0][..share_start], &target[0][..share_start]); - for chunk in response.chunks(fragment) { - client.feed(chunk).unwrap(); - } - let mut finished = Vec::new(); - client.write_tls(&mut finished).unwrap(); - for chunk in finished.chunks(fragment) { - server.feed(chunk).unwrap(); - } - assert!(!client.is_handshaking()); - assert!(!server.is_handshaking()); - assert_eq!( - client.negotiated_group(), - Some(rustls::NamedGroup::from(group as u16)) - ); - assert_eq!( - server.negotiated_group(), - Some(rustls::NamedGroup::from(group as u16)) - ); - (client, server) -} - -pub(crate) fn established_pair() -> (TlsConnection, TlsConnection) { - let (client, server) = configs(CipherSuite::AES_128_GCM_SHA256); - handshake( - client, - server, - RealityGroup::X25519MlKem768, - target_records, - 37, - ) -} - -#[test] -fn reality_separate_and_observed_trailing_record_without_mldsa() { - for &cipher in DEFAULT_CIPHER_SUITES { - for group in [RealityGroup::X25519, RealityGroup::X25519MlKem768] { - let (cc, sc) = configs(cipher); - handshake(cc, sc, group, separate_target_records, 1); - let (cc, sc) = configs(cipher); - handshake( - cc, - sc, - group, - |group, cipher, session| { - let mut records = separate_target_records(group, cipher, session); - let mut trailing = write_record_header(23, 59); - trailing.resize(64, 0); - records.push(trailing.into()); - records - }, - 17, - ); - } - } -} - -#[test] -fn reality_pq_handshake_and_data_all_suites() { - for &cipher in DEFAULT_CIPHER_SUITES { - for group in [RealityGroup::X25519MlKem768, RealityGroup::X25519] { - let (cc, sc) = configs(cipher); - let (mut client, mut server) = handshake(cc, sc, group, target_records, 37); - let expected = vec![17; 200_000]; - let mut wire = Vec::new(); - for chunk in expected.chunks(8192) { - client.writer().write_all(chunk).unwrap(); - while client.wants_write() { - client.write_tls(&mut wire).unwrap(); - } - } - let mut actual = Vec::new(); - for chunk in wire.chunks(1024) { - server.feed(chunk).unwrap(); - let mut buffer = [0; 16384]; - loop { - match server.reader().read(&mut buffer) { - Ok(0) => break, - Ok(n) => actual.extend_from_slice(&buffer[..n]), - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break, - Err(e) => panic!("{e}"), - } - } - } - assert_eq!(actual, expected); - server.writer().write_all(b"response").unwrap(); - wire.clear(); - server.write_tls(&mut wire).unwrap(); - client.feed(&wire).unwrap(); - let mut response = [0; 8]; - client.reader().read_exact(&mut response).unwrap(); - assert_eq!(&response, b"response"); - client.send_close_notify(); - wire.clear(); - client.write_tls(&mut wire).unwrap(); - server.feed(&wire).unwrap(); - assert_eq!(server.reader().read(&mut response).unwrap(), 0); - } - } -} - -#[test] -fn reality_pq_mldsa_optional_and_required() { - let (seed, verify) = generate_mldsa65_keypair().unwrap(); - let signer = mldsa65_signing_key(&seed).unwrap(); - let key = decode_mldsa65_verify(&verify).unwrap(); - for &cipher in DEFAULT_CIPHER_SUITES { - for group in [RealityGroup::X25519MlKem768, RealityGroup::X25519] { - for required in [false, true] { - let (mut cc, mut sc) = configs(cipher); - sc.mldsa65_key = Some(signer.clone()); - cc.mldsa65_verify = required.then(|| key.clone()); - handshake(cc.clone(), sc.clone(), group, target_records, 19); - handshake(cc, sc, group, separate_target_records, 19); - } - } - } -} - -#[test] -fn reality_pq_mldsa_required_fails_closed() { - let (seed, verify) = generate_mldsa65_keypair().unwrap(); - let (_, wrong) = generate_mldsa65_keypair().unwrap(); - for sign in [false, true] { - let (mut cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256); - cc.mldsa65_verify = - Some(decode_mldsa65_verify(if sign { &wrong } else { &verify }).unwrap()); - sc.mldsa65_key = sign.then(|| mldsa65_signing_key(&seed).unwrap()); - let mut client = cc.create_connection().unwrap(); - let mut hello = Vec::new(); - client.write_tls(&mut hello).unwrap(); - let authenticated = sc.authenticate_client_hello(&hello).unwrap(); - let mut server = authenticated - .into_connection(&default_target_records(&hello[44..76])) - .unwrap(); - let mut flight = Vec::new(); - server.write_tls(&mut flight).unwrap(); - assert_eq!( - client.feed(&flight).unwrap_err().kind(), - std::io::ErrorKind::PermissionDenied - ); - assert!(client.is_handshaking()); - let _ = client.writer().write(b"forbidden"); - let mut output = Vec::new(); - client.write_tls(&mut output).unwrap(); - assert!(output.is_empty()); - } -} - -#[test] -fn reality_pq_mldsa_small_target_does_not_emit_synthetic_bytes() { - let (cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256); - let (seed, _) = generate_mldsa65_keypair().unwrap(); - sc.mldsa65_key = Some(mldsa65_signing_key(&seed).unwrap()); - let mut client = cc.create_connection().unwrap(); - let mut hello = Vec::new(); - client.write_tls(&mut hello).unwrap(); - let authenticated = sc.authenticate_client_hello(&hello).unwrap(); - let mut records = default_target_records(&hello[44..76]); - let mut small = write_record_header(23, 595); - small.resize(600, 0); - records[2] = small.into(); - let error = authenticated.into_connection(&records).unwrap_err(); - assert_eq!(error.kind(), std::io::ErrorKind::Unsupported); - assert_eq!( - error.to_string(), - "Unsupported REALITY target or insufficient capacity" - ); -} - -#[test] -fn reality_pq_mldsa_separate_records_each_require_capacity() { - let (seed, _) = generate_mldsa65_keypair().unwrap(); - let signer = mldsa65_signing_key(&seed).unwrap(); - for &cipher in DEFAULT_CIPHER_SUITES { - for small_record in 2..6 { - let (cc, mut sc) = configs(cipher); - sc.mldsa65_key = Some(signer.clone()); - let mut client = cc.create_connection().unwrap(); - let mut hello = Vec::new(); - client.write_tls(&mut hello).unwrap(); - let authenticated = sc.authenticate_client_hello(&hello).unwrap(); - let mut records = - separate_target_records(RealityGroup::X25519MlKem768, cipher, &hello[44..76]); - let mut small = write_record_header(23, 17); - small.resize(22, 0); - records[small_record] = small.into(); - let error = authenticated.into_connection(&records).unwrap_err(); - assert_eq!(error.kind(), std::io::ErrorKind::Unsupported); - assert_eq!( - error.to_string(), - "Unsupported REALITY target or insufficient capacity" - ); - } - } -} diff --git a/src/reality/reality_rustls.rs b/src/reality/reality_rustls_adapter.rs similarity index 99% rename from src/reality/reality_rustls.rs rename to src/reality/reality_rustls_adapter.rs index 569037f0..06596eb3 100644 --- a/src/reality/reality_rustls.rs +++ b/src/reality/reality_rustls_adapter.rs @@ -11,10 +11,10 @@ use rustls::{CertificateError, DigitallySignedStruct, Error, NamedGroup, Signatu use zeroize::Zeroizing; use super::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh}; -use super::reality_client_verify::{ +use super::reality_certificate_verify::{ extract_ed25519_public_key, verify_certificate_hmac, verify_certificate_mldsa, }; -use super::reality_key_exchange::MLKEM_CIPHERTEXT_LEN; +use super::reality_hello::MLKEM_CIPHERTEXT_LEN; use super::{CipherSuite, DEFAULT_CIPHER_SUITES, RealityClientConfig}; pub(super) fn provider(suites: &[CipherSuite]) -> CryptoProvider { diff --git a/src/reality/reality_server.rs b/src/reality/reality_server.rs index f22382b0..8118af29 100644 --- a/src/reality/reality_server.rs +++ b/src/reality/reality_server.rs @@ -8,10 +8,11 @@ use zeroize::Zeroizing; use super::CipherSuite; use super::reality_auth::{decrypt_session_id, derive_auth_key, perform_ecdh}; use super::reality_certificate::generate_hmac_certificate; -use super::reality_key_exchange::extract_client_key_shares; -use super::reality_rustls::{RealitySigningKey, provider}; -use super::reality_target_template::{flight_plan, verify_staged_flight}; -use super::reality_util::{extract_client_random, extract_session_id_slice}; +use super::reality_hello::{ + extract_client_key_shares, extract_client_random, extract_session_id_slice, +}; +use super::reality_rustls_adapter::{RealitySigningKey, provider}; +use super::reality_target_flight::{flight_plan, verify_staged_flight}; use crate::address::{Address, NetLocation}; use crate::crypto::{TlsConnection, tls_error}; diff --git a/src/reality/reality_server_handler.rs b/src/reality/reality_server_handler.rs index ddc9ac90..d94f4ec0 100644 --- a/src/reality/reality_server_handler.rs +++ b/src/reality/reality_server_handler.rs @@ -19,7 +19,7 @@ use crate::tls_server_handler::InnerProtocol; use crate::util::{allocate_vec, write_all}; use super::RealityServerConfig; -use super::reality_key_exchange::validate_server_key_share; +use super::reality_hello::validate_server_key_share; #[derive(Debug)] pub struct RealityServerTarget { @@ -449,664 +449,5 @@ fn start_forward_to_dest( } #[cfg(test)] -mod cleanup_tests { - use super::*; - use crate::crypto::TlsConnection; - use std::io; - use std::pin::Pin; - use std::task::{Context, Poll}; - use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - - struct TargetReads { - chunks: std::collections::VecDeque, - reads: usize, - } - - impl AsyncRead for TargetReads { - fn poll_read( - mut self: Pin<&mut Self>, - _cx: &mut Context<'_>, - buffer: &mut ReadBuf<'_>, - ) -> Poll> { - let Some(chunk) = self.chunks.pop_front() else { - // An open, idle target must not make collection wait for an unseen seventh record. - return Poll::Pending; - }; - assert!(chunk.len() <= buffer.remaining()); - buffer.put_slice(&chunk); - self.reads += 1; - Poll::Ready(Ok(())) - } - } - - #[tokio::test] - async fn target_collection_respects_observed_read_boundaries() { - use crate::reality::CipherSuite; - use crate::reality::reality_key_exchange::RealityGroup; - use crate::reality::reality_pq_tests::{configs, separate_target_records}; - - for case in ["six", "seven", "split-seven", "combined-trailing"] { - let (client_config, server_config) = configs(CipherSuite::AES_128_GCM_SHA256); - let mut client = client_config.create_connection().unwrap(); - let mut hello = Vec::new(); - client.write_tls(&mut hello).unwrap(); - let mut records = if case == "combined-trailing" { - crate::reality::default_target_records(&hello[44..76]) - } else { - separate_target_records( - RealityGroup::X25519MlKem768, - CipherSuite::AES_128_GCM_SHA256, - &hello[44..76], - ) - }; - if matches!(case, "seven" | "split-seven") { - let mut trailing = vec![23, 3, 3, 0, 59]; - trailing.resize(64, 0); - records.push(trailing.into()); - } - let mut wire = records.concat(); - let remaining: &[u8] = if case == "combined-trailing" { - &[23, 3, 3] - } else { - &[] - }; - wire.extend_from_slice(remaining); - let chunks = if case == "split-seven" { - let split = wire.len() - 61; - vec![ - Bytes::copy_from_slice(&wire[..split]), - Bytes::copy_from_slice(&wire[split..]), - ] - } else { - vec![Bytes::copy_from_slice(&wire)] - }; - let expected_reads = chunks.len(); - let mut target = TargetReads { - chunks: chunks.into(), - reads: 0, - }; - let flight = tokio::time::timeout( - Duration::from_secs(1), - read_target_flight(&mut target, &server_config.dest, true), - ) - .await - .expect(case) - .unwrap(); - assert_eq!(target.reads, expected_reads, "{case}"); - assert_eq!(flight.records, records, "{case}"); - assert_eq!(flight.remaining.as_ref(), remaining, "{case}"); - assert_eq!(flight.usable, case != "combined-trailing", "{case}"); - if flight.usable { - let mut server = server_config - .authenticate_client_hello(&hello) - .unwrap() - .into_connection(&flight.records) - .unwrap(); - let mut wire = Vec::new(); - server.write_tls(&mut wire).unwrap(); - client.feed(&wire).unwrap(); - wire.clear(); - client.write_tls(&mut wire).unwrap(); - server.feed(&wire).unwrap(); - assert!( - !client.is_handshaking() && !server.is_handshaking(), - "{case}" - ); - } - } - } - - #[test] - fn synthesis_failures_only_forward_explicit_incompatibilities() { - assert_eq!(forward_unsupported(Ok(())).unwrap(), Some(())); - assert_eq!( - forward_unsupported::<()>(Err(io::ErrorKind::Unsupported.into())).unwrap(), - None - ); - for kind in [ - io::ErrorKind::InvalidData, - io::ErrorKind::UnexpectedEof, - io::ErrorKind::Other, - ] { - let error = io::Error::new(kind, "injected synthesis failure"); - let propagated = forward_unsupported::<()>(Err(error)).unwrap_err(); - assert_eq!(propagated.kind(), kind); - assert_eq!(propagated.to_string(), "injected synthesis failure"); - } - } - - fn client_key_share_offsets(hello: &[u8]) -> Vec { - use crate::buf_reader::BufReader; - - let mut reader = BufReader::new(hello); - reader.skip(43).unwrap(); - let session_len = reader.read_u8().unwrap() as usize; - reader.skip(session_len).unwrap(); - let cipher_len = reader.read_u16_be().unwrap() as usize; - reader.skip(cipher_len).unwrap(); - let compression_len = reader.read_u8().unwrap() as usize; - reader.skip(compression_len).unwrap(); - let extensions_len = reader.read_u16_be().unwrap() as usize; - assert_eq!(extensions_len, reader.remaining()); - while !reader.is_consumed() { - let id = reader.read_u16_be().unwrap(); - let len = reader.read_u16_be().unwrap() as usize; - let start = reader.position(); - let data = reader.read_slice(len).unwrap(); - if id == 51 { - let mut shares = BufReader::new(data); - let len = shares.read_u16_be().unwrap() as usize; - assert_eq!(len, shares.remaining()); - let mut offsets = Vec::new(); - while !shares.is_consumed() { - offsets.push(start + shares.position()); - shares.skip(2).unwrap(); - let len = shares.read_u16_be().unwrap() as usize; - shares.skip(len).unwrap(); - } - return offsets; - } - } - panic!("missing key_share extension"); - } - - #[tokio::test] - async fn incompatible_hellos_and_small_targets_forward_unchanged() { - use crate::client_proxy_chain::InitialHopEntry; - use crate::port_forward_handler::PortForwardServerHandler; - use crate::reality::reality_key_exchange::RealityGroup; - use crate::reality::reality_pq_tests::{ - configs, separate_target_records, target_with_unknown_extension, - }; - use crate::reality::{CipherSuite, mldsa65_signing_key}; - use crate::tcp::socket_connector_impl::SocketConnectorImpl; - - for case in [ - "small", - "small-separate", - "retry", - "unsupported", - "client-unknown-shares", - "client-truncated-share", - "client-bad-auth", - "bad-late-record", - "bad-ccs", - "wrong-session", - "unknown-extension", - "combined-trailing", - ] { - let capture = crate::logging::test_capture::Capture::new(); - let (cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256); - let (seed, _) = crate::reality::generate_mldsa65_keypair().unwrap(); - sc.mldsa65_key = Some(mldsa65_signing_key(&seed).unwrap()); - let mut client = cc.create_connection().unwrap(); - let mut hello = Vec::new(); - client.write_tls(&mut hello).unwrap(); - let auth_failure = matches!( - case, - "client-unknown-shares" | "client-truncated-share" | "client-bad-auth" - ); - let mut response = match case { - "client-unknown-shares" | "client-truncated-share" | "client-bad-auth" => { - let offsets = client_key_share_offsets(&hello); - assert_eq!(offsets.len(), 2); - let expected_error = match case { - "client-unknown-shares" => { - for (offset, group) in offsets.into_iter().zip([0xfe30u16, 0xfe31]) { - hello[offset..offset + 2].copy_from_slice(&group.to_be_bytes()); - } - io::ErrorKind::InvalidData - } - "client-truncated-share" => { - // Keeps the TLS framing valid while overstating the last share's length. - let offset = offsets[1] + 2; - assert_eq!(&hello[offset..offset + 2], &[0, 32]); - hello[offset..offset + 2].copy_from_slice(&33u16.to_be_bytes()); - io::ErrorKind::UnexpectedEof - } - _ => { - hello[44] ^= 1; - io::ErrorKind::PermissionDenied - } - }; - let check = sc.clone(); - assert_eq!( - check.authenticate_client_hello(&hello).unwrap_err().kind(), - expected_error - ); - // A valid target flight reaches the admission-error branch, not hello fallback. - crate::reality::default_target_records(&hello[44..76]).concat() - } - "small" | "small-separate" => { - let (mut records, small_record) = if case == "small" { - (crate::reality::default_target_records(&hello[44..76]), 2) - } else { - ( - separate_target_records( - RealityGroup::X25519MlKem768, - CipherSuite::AES_128_GCM_SHA256, - &hello[44..76], - ), - 3, - ) - }; - let mut encrypted = vec![23, 3, 3, 2, 88]; - encrypted.resize(605, 0); - records[small_record] = encrypted.into(); - let check = sc.clone(); - let authenticated = check.authenticate_client_hello(&hello).unwrap(); - assert_eq!( - authenticated - .into_connection(&records) - .unwrap_err() - .to_string(), - "Unsupported REALITY target or insufficient capacity" - ); - records.concat() - } - "bad-late-record" | "bad-ccs" | "wrong-session" | "unknown-extension" - | "combined-trailing" => { - let mut records = crate::reality::default_target_records(&hello[44..76]); - match case { - "bad-ccs" => records[1] = Bytes::from_static(&[20, 3, 3, 0, 1, 0]), - "wrong-session" => { - let mut sh = records[0].to_vec(); - sh[44] ^= 1; - records[0] = sh.into(); - } - "unknown-extension" => { - records[0] = target_with_unknown_extension(&records[0]) - } - "combined-trailing" => {} - "bad-late-record" => records.push(Bytes::from_static(&[255, 3, 3, 0, 0])), - _ => unreachable!(), - } - records.concat() - } - _ => { - let mut message = vec![2, 0, 0, 0, 3, 3]; - if case == "retry" { - let retry_random = aws_lc_rs::digest::digest( - &aws_lc_rs::digest::SHA256, - b"HelloRetryRequest", - ); - message.extend_from_slice(retry_random.as_ref()); - } else { - message.extend_from_slice(&[3; 32]); - } - message.push(32); - message.extend_from_slice(&hello[44..76]); - message.extend_from_slice(&[0x13, 1, 0]); - let mut extensions = vec![0, 43, 0, 2, 3, 4]; - if case == "retry" { - extensions.extend_from_slice(&[0, 51, 0, 2, 0, 23]); - } else { - extensions.extend_from_slice(&[0, 51, 0, 5, 0xfe, 0x30, 0, 1, 1]); - } - message.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); - message.extend_from_slice(&extensions); - let len = (message.len() - 4) as u32; - message[1..4].copy_from_slice(&len.to_be_bytes()[1..]); - let mut record = vec![22, 3, 3]; - record.extend_from_slice(&(message.len() as u16).to_be_bytes()); - record.extend_from_slice(&message); - record - } - }; - // A partial trailing record exercises the deframer's retained bytes. - if matches!(case, "retry" | "unsupported" | "combined-trailing") { - response.extend_from_slice(&[23, 3, 3]); - } - - let listener = tokio::net::TcpListener::bind("0.0.0.0:0").await.unwrap(); - let dest = NetLocation::new( - crate::address::Address::Hostname("localhost".into()), - listener.local_addr().unwrap().port(), - ); - let selector = Arc::new(ClientProxySelector::new(Vec::new())); - let target = RealityServerTarget { - mldsa65_key: sc.mldsa65_key, - private_key: sc.private_key, - short_ids: sc.short_ids, - dest: dest.clone(), - max_time_diff: None, - min_client_version: None, - max_client_version: None, - cipher_suites: sc.cipher_suites, - effective_selector: selector.clone(), - inner_protocol: InnerProtocol::Normal(Box::new(PortForwardServerHandler::new( - vec![dest], - selector, - ))), - dest_client_chain: ClientProxyChain::new( - vec![InitialHopEntry::Direct(Arc::new( - SocketConnectorImpl::from_config( - &crate::config::ClientConfig::default(), - None, - ) - .unwrap(), - ))], - Vec::new(), - ), - }; - let resolver: Arc = Arc::new(crate::resolver::NativeResolver::new()); - let (io, mut peer) = tokio::io::duplex(65536); - let mut flight = hello.clone(); - flight.extend_from_slice(b"pending"); - peer.write_all(&flight).await.unwrap(); - let mut io: Box = Box::new(io); - let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap(); - assert!(parsed.supports_tls13); - assert_eq!(parsed.client_reader.unparsed_data(), b"pending"); - let decoy_io = async { - let (mut socket, _) = listener.accept().await.unwrap(); - let mut forwarded = vec![0; hello.len()]; - socket.read_exact(&mut forwarded).await.unwrap(); - assert_eq!(forwarded, hello); - socket.write_all(&response).await.unwrap(); - let mut upstream = Vec::new(); - socket.read_to_end(&mut upstream).await.unwrap(); - assert_eq!(upstream, b"pending-live"); - socket.write_all(b"-reply").await.unwrap(); - socket.shutdown().await.unwrap(); - }; - let handler = async { - let result = setup_reality_server_stream(io, &target, parsed, &resolver) - .await - .unwrap(); - let TcpServerSetupResult::Session(session) = result else { - panic!("expected transparent fallback for {case}"); - }; - session.await; - }; - let peer_io = async { - let mut actual = vec![0; response.len()]; - peer.read_exact(&mut actual).await.unwrap(); - assert_eq!(actual, response, "{case}"); - peer.write_all(b"-live").await.unwrap(); - peer.shutdown().await.unwrap(); - let mut reply = Vec::new(); - peer.read_to_end(&mut reply).await.unwrap(); - assert_eq!(reply, b"-reply"); - }; - tokio::time::timeout(Duration::from_secs(2), async { - tokio::join!(handler, decoy_io, peer_io); - }) - .await - .unwrap_or_else(|_| panic!("transparent forwarding stalled for {case}")); - let logs = capture.finish(); - if auth_failure { - assert!(logs.contains("REALITY: Auth failed"), "{case}: {logs}"); - } - if matches!(case, "small" | "small-separate" | "unknown-extension") { - assert!( - logs.contains("Auth succeeded, building response"), - "{case}: {logs}" - ); - assert!( - logs.contains("REALITY: Cannot synthesize"), - "{case}: {logs}" - ); - } - if case == "combined-trailing" { - assert!( - logs.contains("REALITY: Dest handshake failed"), - "{case}: {logs}" - ); - } - } - } - - #[tokio::test] - async fn authenticated_non_vision_oversized_preread_returns_error_without_panicking() { - use crate::client_proxy_chain::InitialHopEntry; - use crate::port_forward_handler::PortForwardServerHandler; - use crate::reality::RealityClientConfig; - use crate::tcp::socket_connector_impl::SocketConnectorImpl; - use aws_lc_rs::agreement; - - let private_key = [1; 32]; - let public_key = agreement::PrivateKey::from_private_key(&agreement::X25519, &private_key) - .unwrap() - .compute_public_key() - .unwrap(); - let mut client = RealityClientConfig { - mldsa65_verify: None, - public_key: public_key.as_ref().try_into().unwrap(), - short_id: [0; 8], - server_name: "localhost".try_into().unwrap(), - cipher_suites: Vec::new(), - } - .create_connection() - .unwrap(); - let mut hello = Vec::new(); - client.write_tls(&mut hello).unwrap(); - - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = listener.local_addr().unwrap(); - let dest = NetLocation::new( - crate::address::Address::Hostname("localhost".into()), - address.port(), - ); - let cert = rcgen::generate_simple_self_signed( - (0..100) - .map(|index| format!("host-{index}.example.test")) - .collect::>(), - ) - .unwrap(); - let decoy_config = - rustls::ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13]) - .with_no_client_auth() - .with_single_cert( - vec![cert.cert.der().clone()], - rustls::pki_types::PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()) - .into(), - ) - .unwrap(); - let mut decoy = - TlsConnection::new(rustls::ServerConnection::new(Arc::new(decoy_config)).unwrap()); - decoy.feed(&hello).unwrap(); - let mut response = Vec::new(); - decoy.write_tls(&mut response).unwrap(); - let selector = Arc::new(ClientProxySelector::new(Vec::new())); - let target = RealityServerTarget { - mldsa65_key: None, - private_key, - short_ids: vec![[0; 8]], - dest: dest.clone(), - max_time_diff: None, - min_client_version: None, - max_client_version: None, - cipher_suites: Vec::new(), - effective_selector: selector.clone(), - inner_protocol: InnerProtocol::Normal(Box::new(PortForwardServerHandler::new( - vec![dest], - selector, - ))), - dest_client_chain: ClientProxyChain::new( - vec![InitialHopEntry::Direct(Arc::new( - SocketConnectorImpl::from_config(&crate::config::ClientConfig::default(), None) - .unwrap(), - ))], - Vec::new(), - ), - }; - let resolver: Arc = Arc::new(crate::resolver::NativeResolver::new()); - let (io, mut peer) = tokio::io::duplex(65540); - let mut flight = hello.clone(); - flight.extend_from_slice(&[0x17, 0x03, 0x03, 0, 1, 0].repeat(6000)); - peer.write_all(&flight).await.unwrap(); - let mut io: Box = Box::new(io); - let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap(); - assert!(parsed.client_reader.unparsed_data().len() > 33290); - - let decoy_io = async { - let (mut socket, _) = listener.accept().await.unwrap(); - let mut forwarded_hello = vec![0; hello.len()]; - socket.read_exact(&mut forwarded_hello).await.unwrap(); - assert_eq!(forwarded_hello, hello); - socket.write_all(&response).await.unwrap(); - }; - let (result, ()) = tokio::time::timeout(Duration::from_secs(2), async { - tokio::join!( - setup_reality_server_stream(io, &target, parsed, &resolver), - decoy_io, - ) - }) - .await - .unwrap(); - let error = result.err().expect("invalid encrypted records must fail"); - assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{error}"); - } - - #[tokio::test] - async fn fallback_preread_backpressure_does_not_block_server_response() { - let (client, mut client_peer) = tokio::io::duplex(64); - let (dest, mut dest_peer) = tokio::io::duplex(64); - let pending = vec![42; 128]; - let TcpServerSetupResult::Session(session) = start_forward_to_dest( - Box::new(client), - Box::new(dest), - vec![Bytes::from_static(b"server")], - Bytes::new(), - Bytes::copy_from_slice(&pending), - ) else { - panic!("expected fallback session"); - }; - tokio::time::timeout(Duration::from_secs(1), async { - tokio::join!(session, async { - let mut response = [0; 6]; - client_peer.read_exact(&mut response).await.unwrap(); - assert_eq!(&response, b"server"); - let mut received = vec![0; pending.len()]; - dest_peer.read_exact(&mut received).await.unwrap(); - assert_eq!(received, pending); - client_peer.shutdown().await.unwrap(); - dest_peer.shutdown().await.unwrap(); - }); - }) - .await - .unwrap(); - } - - #[tokio::test] - async fn fallback_preserves_preread_client_bytes() { - let (client, mut client_peer) = tokio::io::duplex(64); - let (dest, mut dest_peer) = tokio::io::duplex(64); - let TcpServerSetupResult::Session(session) = start_forward_to_dest( - Box::new(client), - Box::new(dest), - vec![Bytes::from_static(b"server")], - Bytes::from_static(b"-tail"), - Bytes::from_static(b"client"), - ) else { - panic!("expected fallback session"); - }; - tokio::time::timeout(Duration::from_secs(1), async { - tokio::join!(session, async { - client_peer.write_all(b"-live").await.unwrap(); - client_peer.shutdown().await.unwrap(); - let mut received = [0; 11]; - dest_peer.read_exact(&mut received).await.unwrap(); - assert_eq!(&received, b"client-live"); - dest_peer.write_all(b"-reply").await.unwrap(); - dest_peer.shutdown().await.unwrap(); - let mut response = Vec::new(); - client_peer.read_to_end(&mut response).await.unwrap(); - assert_eq!(response, b"server-tail-reply"); - }); - }) - .await - .unwrap(); - } - - struct StalledShutdown { - fail_write: bool, - fail_flush: bool, - _marker: Arc<()>, - } - - impl AsyncRead for StalledShutdown { - fn poll_read( - self: Pin<&mut Self>, - _: &mut Context<'_>, - _: &mut ReadBuf<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } - } - - impl AsyncWrite for StalledShutdown { - fn poll_write( - self: Pin<&mut Self>, - _: &mut Context<'_>, - bytes: &[u8], - ) -> Poll> { - if self.fail_write { - Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())) - } else { - Poll::Ready(Ok(bytes.len())) - } - } - fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { - if self.fail_flush { - Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())) - } else { - Poll::Ready(Ok(())) - } - } - fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { - Poll::Pending - } - } - - impl crate::async_stream::AsyncPing for StalledShutdown { - fn supports_ping(&self) -> bool { - false - } - fn poll_write_ping(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(false)) - } - } - - impl AsyncStream for StalledShutdown {} - - #[tokio::test(start_paused = true)] - async fn fallback_cleanup_is_bounded_after_copy_and_early_errors() { - let bytes = Bytes::from_static(b"handshake"); - let cases = [ - (false, false, vec![], Bytes::new(), 10), - (true, false, vec![bytes.clone()], Bytes::new(), 5), - (false, true, vec![bytes.clone()], Bytes::new(), 5), - (true, false, vec![], bytes, 5), - ]; - for (fail_write, fail_flush, records, remaining, seconds) in cases { - let marker = Arc::new(()); - let client = StalledShutdown { - fail_write, - fail_flush, - _marker: marker.clone(), - }; - let dest = StalledShutdown { - fail_write: false, - fail_flush: false, - _marker: marker.clone(), - }; - let TcpServerSetupResult::Session(session) = start_forward_to_dest( - Box::new(client), - Box::new(dest), - records, - remaining, - Bytes::new(), - ) else { - panic!("fallback must remain owned by its caller") - }; - let started = tokio::time::Instant::now(); - tokio::time::timeout(std::time::Duration::from_secs(11), session) - .await - .expect("fallback cleanup retained the streams indefinitely"); - assert_eq!(started.elapsed(), std::time::Duration::from_secs(seconds)); - assert_eq!(Arc::strong_count(&marker), 1); - } - } -} +#[path = "tests/reality_server_handler.rs"] +mod tests; diff --git a/src/reality/reality_target_template.rs b/src/reality/reality_target_flight.rs similarity index 100% rename from src/reality/reality_target_template.rs rename to src/reality/reality_target_flight.rs diff --git a/src/reality/test_tls.rs b/src/reality/test_tls.rs deleted file mode 100644 index 5a8f800c..00000000 --- a/src/reality/test_tls.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Independent target-wire fixtures for REALITY admission and mirroring tests. - -use super::reality_key_exchange::RealityGroup; - -pub(super) fn construct_server_hello( - random: &[u8; 32], - session: &[u8], - cipher: u16, - group: RealityGroup, - key_share: &[u8], -) -> std::io::Result> { - if key_share.len() != group.share_len(false) { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Invalid server key share size", - )); - } - let mut hello = vec![2, 0, 0, 0, 3, 3]; - hello.extend_from_slice(random); - hello.push(session.len() as u8); - hello.extend_from_slice(session); - hello.extend_from_slice(&cipher.to_be_bytes()); - hello.push(0); - - let mut extensions = vec![0, 43, 0, 2, 3, 4]; - extensions.extend_from_slice(&[0, 51]); - extensions.extend_from_slice(&((4 + key_share.len()) as u16).to_be_bytes()); - extensions.extend_from_slice(&(group as u16).to_be_bytes()); - extensions.extend_from_slice(&(key_share.len() as u16).to_be_bytes()); - extensions.extend_from_slice(key_share); - hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); - hello.extend_from_slice(&extensions); - - let length = (hello.len() - 4) as u32; - hello[1..4].copy_from_slice(&length.to_be_bytes()[1..]); - Ok(hello) -} - -pub(super) fn write_record_header(record_type: u8, length: u16) -> Vec { - let mut header = vec![record_type, 3, 3]; - header.extend_from_slice(&length.to_be_bytes()); - header -} diff --git a/src/reality/tests/mod.rs b/src/reality/tests/mod.rs new file mode 100644 index 00000000..73d57d80 --- /dev/null +++ b/src/reality/tests/mod.rs @@ -0,0 +1,3 @@ +pub(super) mod reality_fixtures; +mod reality_handshake; +mod reality_post_quantum; diff --git a/src/reality/tests/reality_fixtures.rs b/src/reality/tests/reality_fixtures.rs new file mode 100644 index 00000000..10d08720 --- /dev/null +++ b/src/reality/tests/reality_fixtures.rs @@ -0,0 +1,213 @@ +//! Shared REALITY connection and independent target-wire fixtures. + +use aws_lc_rs::agreement; +use bytes::Bytes; + +use crate::crypto::TlsConnection; +use crate::reality::reality_hello::{ + RealityGroup, extract_client_key_shares, validate_server_key_share, +}; +use crate::reality::{CipherSuite, RealityClientConfig, RealityServerConfig}; + +pub(in crate::reality) fn construct_server_hello( + random: &[u8; 32], + session: &[u8], + cipher: u16, + group: RealityGroup, + key_share: &[u8], +) -> std::io::Result> { + if key_share.len() != group.share_len(false) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid server key share size", + )); + } + let mut hello = vec![2, 0, 0, 0, 3, 3]; + hello.extend_from_slice(random); + hello.push(session.len() as u8); + hello.extend_from_slice(session); + hello.extend_from_slice(&cipher.to_be_bytes()); + hello.push(0); + + let mut extensions = vec![0, 43, 0, 2, 3, 4]; + extensions.extend_from_slice(&[0, 51]); + extensions.extend_from_slice(&((4 + key_share.len()) as u16).to_be_bytes()); + extensions.extend_from_slice(&(group as u16).to_be_bytes()); + extensions.extend_from_slice(&(key_share.len() as u16).to_be_bytes()); + extensions.extend_from_slice(key_share); + hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); + hello.extend_from_slice(&extensions); + + let length = (hello.len() - 4) as u32; + hello[1..4].copy_from_slice(&length.to_be_bytes()[1..]); + Ok(hello) +} + +pub(in crate::reality) fn write_record_header(record_type: u8, length: u16) -> Vec { + let mut header = vec![record_type, 3, 3]; + header.extend_from_slice(&length.to_be_bytes()); + header +} + +fn public_key(private: &[u8; 32]) -> [u8; 32] { + agreement::PrivateKey::from_private_key(&agreement::X25519, private) + .unwrap() + .compute_public_key() + .unwrap() + .as_ref() + .try_into() + .unwrap() +} + +pub(in crate::reality) fn configs( + cipher: CipherSuite, +) -> (RealityClientConfig, RealityServerConfig) { + let private = [42; 32]; + ( + RealityClientConfig { + mldsa65_verify: None, + public_key: public_key(&private), + short_id: [1; 8], + server_name: "cover.test".try_into().unwrap(), + cipher_suites: vec![cipher], + }, + RealityServerConfig { + mldsa65_key: None, + private_key: private, + short_ids: vec![[1; 8]], + dest: crate::address::NetLocation::from_str("cover.test:443", None).unwrap(), + max_time_diff: None, + min_client_version: None, + max_client_version: None, + cipher_suites: vec![cipher], + }, + ) +} + +pub(in crate::reality) fn target_records( + group: RealityGroup, + cipher: CipherSuite, + session: &[u8], +) -> Vec { + let hello = construct_server_hello( + &[3; 32], + session, + cipher.id(), + group, + &vec![0; group.share_len(false)], + ) + .unwrap(); + let mut record = write_record_header(22, hello.len() as u16); + record.extend_from_slice(&hello); + let mut encrypted = write_record_header(23, 6000); + encrypted.resize(6005, 0); + vec![ + record.into(), + Bytes::from_static(&[20, 3, 3, 0, 1, 1]), + encrypted.into(), + ] +} + +pub(crate) fn default_target_records(session: &[u8]) -> Vec { + target_records( + RealityGroup::X25519MlKem768, + CipherSuite::AES_128_GCM_SHA256, + session, + ) +} + +pub(in crate::reality) fn separate_target_records( + group: RealityGroup, + cipher: CipherSuite, + session: &[u8], +) -> Vec { + let mut records = target_records(group, cipher, session); + records.truncate(2); + for payload_len in [32, 4096, 128, 96] { + let mut record = write_record_header(23, payload_len); + record.resize(5 + payload_len as usize, 0); + records.push(record.into()); + } + records +} + +pub(in crate::reality) fn target_with_unknown_extension(record: &Bytes) -> Bytes { + let mut hello = record.to_vec(); + let extensions_offset = 47 + usize::from(hello[43]); + let extensions_len = + u16::from_be_bytes([hello[extensions_offset], hello[extensions_offset + 1]]); + hello.extend_from_slice(&[0xfe, 0x30, 0, 0]); + hello[extensions_offset..extensions_offset + 2] + .copy_from_slice(&(extensions_len + 4).to_be_bytes()); + let record_len = (hello.len() - 5) as u16; + hello[3..5].copy_from_slice(&record_len.to_be_bytes()); + let message_len = (hello.len() - 9) as u32; + hello[6..9].copy_from_slice(&message_len.to_be_bytes()[1..]); + assert!( + crate::shadow_tls::parse_server_hello(&hello) + .unwrap() + .is_tls13 + ); + validate_server_key_share(&hello).unwrap(); + hello.into() +} + +pub(super) fn handshake( + client_config: RealityClientConfig, + server_config: RealityServerConfig, + group: RealityGroup, + make_target_records: fn(RealityGroup, CipherSuite, &[u8]) -> Vec, + fragment: usize, +) -> (TlsConnection, TlsConnection) { + let cipher = client_config.cipher_suites[0]; + let mut client = client_config.create_connection().unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + let shares = extract_client_key_shares(&hello).unwrap(); + assert!(shares.hybrid.is_some()); + assert!(shares.x25519.is_some()); + let authenticated = server_config.authenticate_client_hello(&hello).unwrap(); + let target = make_target_records(group, cipher, &hello[44..76]); + let mut server = authenticated.into_connection(&target).unwrap(); + let mut response = Vec::new(); + server.write_tls(&mut response).unwrap(); + let mut deframer = crate::crypto::tls_deframer::TlsDeframer::new(); + deframer.feed(&response); + let records = deframer.next_records().unwrap(); + assert_eq!(records.len(), target.len()); + for (actual, expected) in records.iter().zip(&target) { + assert_eq!(actual.len(), expected.len()); + } + let share_start = target[0].len() - group.share_len(false); + assert_eq!(&records[0][..share_start], &target[0][..share_start]); + for chunk in response.chunks(fragment) { + client.feed(chunk).unwrap(); + } + let mut finished = Vec::new(); + client.write_tls(&mut finished).unwrap(); + for chunk in finished.chunks(fragment) { + server.feed(chunk).unwrap(); + } + assert!(!client.is_handshaking()); + assert!(!server.is_handshaking()); + assert_eq!( + client.negotiated_group(), + Some(rustls::NamedGroup::from(group as u16)) + ); + assert_eq!( + server.negotiated_group(), + Some(rustls::NamedGroup::from(group as u16)) + ); + (client, server) +} + +pub(crate) fn established_pair() -> (TlsConnection, TlsConnection) { + let (client, server) = configs(CipherSuite::AES_128_GCM_SHA256); + handshake( + client, + server, + RealityGroup::X25519MlKem768, + target_records, + 37, + ) +} diff --git a/src/reality/reality_rustls_tests.rs b/src/reality/tests/reality_handshake.rs similarity index 91% rename from src/reality/reality_rustls_tests.rs rename to src/reality/tests/reality_handshake.rs index d202d7c2..3aa893c2 100644 --- a/src/reality/reality_rustls_tests.rs +++ b/src/reality/tests/reality_handshake.rs @@ -5,11 +5,12 @@ use crate::crypto::TlsConnection; use bytes::Bytes; use parking_lot::Mutex; -use super::reality_key_exchange::RealityGroup; -use super::reality_pq_tests::{ - configs, separate_target_records, target_records, target_with_unknown_extension, +use super::reality_fixtures::{ + configs, established_pair, handshake, separate_target_records, target_records, + target_with_unknown_extension, write_record_header, }; -use super::*; +use crate::reality::reality_hello::{RealityGroup, extract_client_key_shares}; +use crate::reality::{CipherSuite, DEFAULT_CIPHER_SUITES, reality_rustls_adapter}; #[derive(Debug, Default)] struct Secrets(Mutex>>); @@ -24,7 +25,7 @@ fn provider_preserves_cipher_defaults_order_and_duplicates() { ], vec![CipherSuite::AES_128_GCM_SHA256; 2], ] { - let provider = reality_rustls::provider(&suites); + let provider = reality_rustls_adapter::provider(&suites); let expected = if suites.is_empty() { DEFAULT_CIPHER_SUITES } else { @@ -101,7 +102,7 @@ fn pending_pair( omit_ed25519: bool, ) -> (TlsConnection, TlsConnection, Vec) { let (cc, sc) = configs(CipherSuite::AES_128_GCM_SHA256); - let crypto = reality_rustls::provider(&cc.cipher_suites); + let crypto = reality_rustls_adapter::provider(&cc.cipher_suites); let mut schemes = crypto.signature_verification_algorithms.supported_schemes(); if omit_ed25519 { schemes.retain(|scheme| *scheme != rustls::SignatureScheme::ED25519); @@ -247,7 +248,7 @@ fn certificate_verify_and_server_finished_must_verify() { #[test] fn client_hello_carries_authenticated_reality_metadata() { - use super::reality_auth::{decrypt_session_id, derive_auth_key, perform_ecdh}; + use crate::reality::reality_auth::{decrypt_session_id, derive_auth_key, perform_ecdh}; let (mut cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256); let short_id = [1, 2, 3, 4, 5, 6, 7, 8]; @@ -260,7 +261,7 @@ fn client_hello_carries_authenticated_reality_metadata() { let mut client = cc.create_connection().unwrap(); let mut hello = Vec::new(); client.write_tls(&mut hello).unwrap(); - let shares = super::reality_key_exchange::extract_client_key_shares(&hello).unwrap(); + let shares = extract_client_key_shares(&hello).unwrap(); let shared = perform_ecdh(&sc.private_key, shares.authentication_public_key().unwrap()).unwrap(); let auth = derive_auth_key(&shared, &hello[11..31], b"REALITY").unwrap(); @@ -295,7 +296,7 @@ fn authentication_policy_and_client_share_variants() { "maximum" => sc.max_client_version = Some([1, 7, 9]), _ => {} } - let mut crypto = reality_rustls::provider(&cc.cipher_suites); + let mut crypto = reality_rustls_adapter::provider(&cc.cipher_suites); if case == "plain-only" { crypto.kx_groups.remove(0); } @@ -312,8 +313,8 @@ fn authentication_policy_and_client_share_variants() { hello[11] ^= 1; } if case == "timestamp" { - use super::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh}; - let shares = super::reality_key_exchange::extract_client_key_shares(&hello).unwrap(); + use crate::reality::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh}; + let shares = extract_client_key_shares(&hello).unwrap(); let shared = perform_ecdh(&sc.private_key, shares.authentication_public_key().unwrap()).unwrap(); let auth = derive_auth_key(&shared, &hello[11..31], b"REALITY").unwrap(); @@ -484,12 +485,12 @@ fn rejected_templates_never_queue_synthetic_output() { #[test] fn authenticated_invalid_tls_shares_forward_before_commit() { - use super::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh}; + use crate::reality::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh}; let (cc, sc) = configs(CipherSuite::AES_128_GCM_SHA256); let mut client = cc.create_connection().unwrap(); let mut hello = Vec::new(); client.write_tls(&mut hello).unwrap(); - let shares = super::reality_key_exchange::extract_client_key_shares(&hello).unwrap(); + let shares = extract_client_key_shares(&hello).unwrap(); let shared = perform_ecdh(&sc.private_key, shares.authentication_public_key().unwrap()).unwrap(); let auth = derive_auth_key(&shared, &hello[11..31], b"REALITY").unwrap(); @@ -550,3 +551,27 @@ fn tcp_eof_without_close_notify_is_not_clean_tls_eof() { io::ErrorKind::UnexpectedEof ); } + +#[test] +fn reality_separate_and_observed_trailing_record_without_mldsa() { + for &cipher in DEFAULT_CIPHER_SUITES { + for group in [RealityGroup::X25519, RealityGroup::X25519MlKem768] { + let (cc, sc) = configs(cipher); + handshake(cc, sc, group, separate_target_records, 1); + let (cc, sc) = configs(cipher); + handshake( + cc, + sc, + group, + |group, cipher, session| { + let mut records = separate_target_records(group, cipher, session); + let mut trailing = write_record_header(23, 59); + trailing.resize(64, 0); + records.push(trailing.into()); + records + }, + 17, + ); + } + } +} diff --git a/src/reality/tests/reality_post_quantum.rs b/src/reality/tests/reality_post_quantum.rs new file mode 100644 index 00000000..5e43eddf --- /dev/null +++ b/src/reality/tests/reality_post_quantum.rs @@ -0,0 +1,152 @@ +use std::io::{Read, Write}; + +use crate::reality::reality_hello::RealityGroup; +use crate::reality::{ + CipherSuite, DEFAULT_CIPHER_SUITES, decode_mldsa65_verify, generate_mldsa65_keypair, + mldsa65_signing_key, +}; + +use super::reality_fixtures::{ + configs, default_target_records, handshake, separate_target_records, target_records, + write_record_header, +}; + +#[test] +fn reality_pq_handshake_and_data_all_suites() { + for &cipher in DEFAULT_CIPHER_SUITES { + for group in [RealityGroup::X25519MlKem768, RealityGroup::X25519] { + let (cc, sc) = configs(cipher); + let (mut client, mut server) = handshake(cc, sc, group, target_records, 37); + let expected = vec![17; 200_000]; + let mut wire = Vec::new(); + for chunk in expected.chunks(8192) { + client.writer().write_all(chunk).unwrap(); + while client.wants_write() { + client.write_tls(&mut wire).unwrap(); + } + } + let mut actual = Vec::new(); + for chunk in wire.chunks(1024) { + server.feed(chunk).unwrap(); + let mut buffer = [0; 16384]; + loop { + match server.reader().read(&mut buffer) { + Ok(0) => break, + Ok(n) => actual.extend_from_slice(&buffer[..n]), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break, + Err(e) => panic!("{e}"), + } + } + } + assert_eq!(actual, expected); + server.writer().write_all(b"response").unwrap(); + wire.clear(); + server.write_tls(&mut wire).unwrap(); + client.feed(&wire).unwrap(); + let mut response = [0; 8]; + client.reader().read_exact(&mut response).unwrap(); + assert_eq!(&response, b"response"); + client.send_close_notify(); + wire.clear(); + client.write_tls(&mut wire).unwrap(); + server.feed(&wire).unwrap(); + assert_eq!(server.reader().read(&mut response).unwrap(), 0); + } + } +} + +#[test] +fn reality_pq_mldsa_optional_and_required() { + let (seed, verify) = generate_mldsa65_keypair().unwrap(); + let signer = mldsa65_signing_key(&seed).unwrap(); + let key = decode_mldsa65_verify(&verify).unwrap(); + for &cipher in DEFAULT_CIPHER_SUITES { + for group in [RealityGroup::X25519MlKem768, RealityGroup::X25519] { + for required in [false, true] { + let (mut cc, mut sc) = configs(cipher); + sc.mldsa65_key = Some(signer.clone()); + cc.mldsa65_verify = required.then(|| key.clone()); + handshake(cc.clone(), sc.clone(), group, target_records, 19); + handshake(cc, sc, group, separate_target_records, 19); + } + } + } +} + +#[test] +fn reality_pq_mldsa_required_fails_closed() { + let (seed, verify) = generate_mldsa65_keypair().unwrap(); + let (_, wrong) = generate_mldsa65_keypair().unwrap(); + for sign in [false, true] { + let (mut cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256); + cc.mldsa65_verify = + Some(decode_mldsa65_verify(if sign { &wrong } else { &verify }).unwrap()); + sc.mldsa65_key = sign.then(|| mldsa65_signing_key(&seed).unwrap()); + let mut client = cc.create_connection().unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + let authenticated = sc.authenticate_client_hello(&hello).unwrap(); + let mut server = authenticated + .into_connection(&default_target_records(&hello[44..76])) + .unwrap(); + let mut flight = Vec::new(); + server.write_tls(&mut flight).unwrap(); + assert_eq!( + client.feed(&flight).unwrap_err().kind(), + std::io::ErrorKind::PermissionDenied + ); + assert!(client.is_handshaking()); + let _ = client.writer().write(b"forbidden"); + let mut output = Vec::new(); + client.write_tls(&mut output).unwrap(); + assert!(output.is_empty()); + } +} + +#[test] +fn reality_pq_mldsa_small_target_does_not_emit_synthetic_bytes() { + let (cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256); + let (seed, _) = generate_mldsa65_keypair().unwrap(); + sc.mldsa65_key = Some(mldsa65_signing_key(&seed).unwrap()); + let mut client = cc.create_connection().unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + let authenticated = sc.authenticate_client_hello(&hello).unwrap(); + let mut records = default_target_records(&hello[44..76]); + let mut small = write_record_header(23, 595); + small.resize(600, 0); + records[2] = small.into(); + let error = authenticated.into_connection(&records).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::Unsupported); + assert_eq!( + error.to_string(), + "Unsupported REALITY target or insufficient capacity" + ); +} + +#[test] +fn reality_pq_mldsa_separate_records_each_require_capacity() { + let (seed, _) = generate_mldsa65_keypair().unwrap(); + let signer = mldsa65_signing_key(&seed).unwrap(); + for &cipher in DEFAULT_CIPHER_SUITES { + for small_record in 2..6 { + let (cc, mut sc) = configs(cipher); + sc.mldsa65_key = Some(signer.clone()); + let mut client = cc.create_connection().unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + let authenticated = sc.authenticate_client_hello(&hello).unwrap(); + let mut records = + separate_target_records(RealityGroup::X25519MlKem768, cipher, &hello[44..76]); + let mut small = write_record_header(23, 17); + small.resize(22, 0); + records[small_record] = small.into(); + let error = authenticated.into_connection(&records).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::Unsupported); + assert_eq!( + error.to_string(), + "Unsupported REALITY target or insufficient capacity" + ); + } + } +} diff --git a/src/reality/tests/reality_server_handler.rs b/src/reality/tests/reality_server_handler.rs new file mode 100644 index 00000000..a0d1983d --- /dev/null +++ b/src/reality/tests/reality_server_handler.rs @@ -0,0 +1,653 @@ +use super::*; +use crate::crypto::TlsConnection; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +struct TargetReads { + chunks: std::collections::VecDeque, + reads: usize, +} + +impl AsyncRead for TargetReads { + fn poll_read( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buffer: &mut ReadBuf<'_>, + ) -> Poll> { + let Some(chunk) = self.chunks.pop_front() else { + // An open, idle target must not make collection wait for an unseen seventh record. + return Poll::Pending; + }; + assert!(chunk.len() <= buffer.remaining()); + buffer.put_slice(&chunk); + self.reads += 1; + Poll::Ready(Ok(())) + } +} + +#[tokio::test] +async fn target_collection_respects_observed_read_boundaries() { + use crate::reality::CipherSuite; + use crate::reality::reality_hello::RealityGroup; + use crate::reality::tests::reality_fixtures::{ + configs, separate_target_records, write_record_header, + }; + + for case in ["six", "seven", "split-seven", "combined-trailing"] { + let (client_config, server_config) = configs(CipherSuite::AES_128_GCM_SHA256); + let mut client = client_config.create_connection().unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + let mut records = if case == "combined-trailing" { + crate::reality::default_target_records(&hello[44..76]) + } else { + separate_target_records( + RealityGroup::X25519MlKem768, + CipherSuite::AES_128_GCM_SHA256, + &hello[44..76], + ) + }; + if matches!(case, "seven" | "split-seven") { + let mut trailing = write_record_header(23, 59); + trailing.resize(64, 0); + records.push(trailing.into()); + } + let mut wire = records.concat(); + let remaining: &[u8] = if case == "combined-trailing" { + &[23, 3, 3] + } else { + &[] + }; + wire.extend_from_slice(remaining); + let chunks = if case == "split-seven" { + let split = wire.len() - 61; + vec![ + Bytes::copy_from_slice(&wire[..split]), + Bytes::copy_from_slice(&wire[split..]), + ] + } else { + vec![Bytes::copy_from_slice(&wire)] + }; + let expected_reads = chunks.len(); + let mut target = TargetReads { + chunks: chunks.into(), + reads: 0, + }; + let flight = tokio::time::timeout( + Duration::from_secs(1), + read_target_flight(&mut target, &server_config.dest, true), + ) + .await + .expect(case) + .unwrap(); + assert_eq!(target.reads, expected_reads, "{case}"); + assert_eq!(flight.records, records, "{case}"); + assert_eq!(flight.remaining.as_ref(), remaining, "{case}"); + assert_eq!(flight.usable, case != "combined-trailing", "{case}"); + if flight.usable { + let mut server = server_config + .authenticate_client_hello(&hello) + .unwrap() + .into_connection(&flight.records) + .unwrap(); + let mut wire = Vec::new(); + server.write_tls(&mut wire).unwrap(); + client.feed(&wire).unwrap(); + wire.clear(); + client.write_tls(&mut wire).unwrap(); + server.feed(&wire).unwrap(); + assert!( + !client.is_handshaking() && !server.is_handshaking(), + "{case}" + ); + } + } +} + +#[test] +fn synthesis_failures_only_forward_explicit_incompatibilities() { + assert_eq!(forward_unsupported(Ok(())).unwrap(), Some(())); + assert_eq!( + forward_unsupported::<()>(Err(io::ErrorKind::Unsupported.into())).unwrap(), + None + ); + for kind in [ + io::ErrorKind::InvalidData, + io::ErrorKind::UnexpectedEof, + io::ErrorKind::Other, + ] { + let error = io::Error::new(kind, "injected synthesis failure"); + let propagated = forward_unsupported::<()>(Err(error)).unwrap_err(); + assert_eq!(propagated.kind(), kind); + assert_eq!(propagated.to_string(), "injected synthesis failure"); + } +} + +fn client_key_share_offsets(hello: &[u8]) -> Vec { + use crate::buf_reader::BufReader; + + let mut reader = BufReader::new(hello); + reader.skip(43).unwrap(); + let session_len = reader.read_u8().unwrap() as usize; + reader.skip(session_len).unwrap(); + let cipher_len = reader.read_u16_be().unwrap() as usize; + reader.skip(cipher_len).unwrap(); + let compression_len = reader.read_u8().unwrap() as usize; + reader.skip(compression_len).unwrap(); + let extensions_len = reader.read_u16_be().unwrap() as usize; + assert_eq!(extensions_len, reader.remaining()); + while !reader.is_consumed() { + let id = reader.read_u16_be().unwrap(); + let len = reader.read_u16_be().unwrap() as usize; + let start = reader.position(); + let data = reader.read_slice(len).unwrap(); + if id == 51 { + let mut shares = BufReader::new(data); + let len = shares.read_u16_be().unwrap() as usize; + assert_eq!(len, shares.remaining()); + let mut offsets = Vec::new(); + while !shares.is_consumed() { + offsets.push(start + shares.position()); + shares.skip(2).unwrap(); + let len = shares.read_u16_be().unwrap() as usize; + shares.skip(len).unwrap(); + } + return offsets; + } + } + panic!("missing key_share extension"); +} + +#[tokio::test] +async fn incompatible_hellos_and_small_targets_forward_unchanged() { + use crate::client_proxy_chain::InitialHopEntry; + use crate::port_forward_handler::PortForwardServerHandler; + use crate::reality::reality_hello::RealityGroup; + use crate::reality::tests::reality_fixtures::{ + configs, separate_target_records, target_with_unknown_extension, write_record_header, + }; + use crate::reality::{CipherSuite, mldsa65_signing_key}; + use crate::tcp::socket_connector_impl::SocketConnectorImpl; + + for case in [ + "small", + "small-separate", + "retry", + "unsupported", + "client-unknown-shares", + "client-truncated-share", + "client-bad-auth", + "bad-late-record", + "bad-ccs", + "wrong-session", + "unknown-extension", + "combined-trailing", + ] { + let capture = crate::logging::test_capture::Capture::new(); + let (cc, mut sc) = configs(CipherSuite::AES_128_GCM_SHA256); + let (seed, _) = crate::reality::generate_mldsa65_keypair().unwrap(); + sc.mldsa65_key = Some(mldsa65_signing_key(&seed).unwrap()); + let mut client = cc.create_connection().unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + let auth_failure = matches!( + case, + "client-unknown-shares" | "client-truncated-share" | "client-bad-auth" + ); + let mut response = match case { + "client-unknown-shares" | "client-truncated-share" | "client-bad-auth" => { + let offsets = client_key_share_offsets(&hello); + assert_eq!(offsets.len(), 2); + let expected_error = match case { + "client-unknown-shares" => { + for (offset, group) in offsets.into_iter().zip([0xfe30u16, 0xfe31]) { + hello[offset..offset + 2].copy_from_slice(&group.to_be_bytes()); + } + io::ErrorKind::InvalidData + } + "client-truncated-share" => { + // Keeps the TLS framing valid while overstating the last share's length. + let offset = offsets[1] + 2; + assert_eq!(&hello[offset..offset + 2], &[0, 32]); + hello[offset..offset + 2].copy_from_slice(&33u16.to_be_bytes()); + io::ErrorKind::UnexpectedEof + } + _ => { + hello[44] ^= 1; + io::ErrorKind::PermissionDenied + } + }; + let check = sc.clone(); + assert_eq!( + check.authenticate_client_hello(&hello).unwrap_err().kind(), + expected_error + ); + // A valid target flight reaches the admission-error branch, not hello fallback. + crate::reality::default_target_records(&hello[44..76]).concat() + } + "small" | "small-separate" => { + let (mut records, small_record) = if case == "small" { + (crate::reality::default_target_records(&hello[44..76]), 2) + } else { + ( + separate_target_records( + RealityGroup::X25519MlKem768, + CipherSuite::AES_128_GCM_SHA256, + &hello[44..76], + ), + 3, + ) + }; + let mut encrypted = write_record_header(23, 600); + encrypted.resize(605, 0); + records[small_record] = encrypted.into(); + let check = sc.clone(); + let authenticated = check.authenticate_client_hello(&hello).unwrap(); + assert_eq!( + authenticated + .into_connection(&records) + .unwrap_err() + .to_string(), + "Unsupported REALITY target or insufficient capacity" + ); + records.concat() + } + "bad-late-record" | "bad-ccs" | "wrong-session" | "unknown-extension" + | "combined-trailing" => { + let mut records = crate::reality::default_target_records(&hello[44..76]); + match case { + "bad-ccs" => records[1] = Bytes::from_static(&[20, 3, 3, 0, 1, 0]), + "wrong-session" => { + let mut sh = records[0].to_vec(); + sh[44] ^= 1; + records[0] = sh.into(); + } + "unknown-extension" => records[0] = target_with_unknown_extension(&records[0]), + "combined-trailing" => {} + "bad-late-record" => records.push(Bytes::from_static(&[255, 3, 3, 0, 0])), + _ => unreachable!(), + } + records.concat() + } + _ => { + let mut message = vec![2, 0, 0, 0, 3, 3]; + if case == "retry" { + let retry_random = + aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, b"HelloRetryRequest"); + message.extend_from_slice(retry_random.as_ref()); + } else { + message.extend_from_slice(&[3; 32]); + } + message.push(32); + message.extend_from_slice(&hello[44..76]); + message.extend_from_slice(&[0x13, 1, 0]); + let mut extensions = vec![0, 43, 0, 2, 3, 4]; + if case == "retry" { + extensions.extend_from_slice(&[0, 51, 0, 2, 0, 23]); + } else { + extensions.extend_from_slice(&[0, 51, 0, 5, 0xfe, 0x30, 0, 1, 1]); + } + message.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); + message.extend_from_slice(&extensions); + let len = (message.len() - 4) as u32; + message[1..4].copy_from_slice(&len.to_be_bytes()[1..]); + let mut record = write_record_header(22, message.len() as u16); + record.extend_from_slice(&message); + record + } + }; + // A partial trailing record exercises the deframer's retained bytes. + if matches!(case, "retry" | "unsupported" | "combined-trailing") { + response.extend_from_slice(&[23, 3, 3]); + } + + let listener = tokio::net::TcpListener::bind("0.0.0.0:0").await.unwrap(); + let dest = NetLocation::new( + crate::address::Address::Hostname("localhost".into()), + listener.local_addr().unwrap().port(), + ); + let selector = Arc::new(ClientProxySelector::new(Vec::new())); + let target = RealityServerTarget { + mldsa65_key: sc.mldsa65_key, + private_key: sc.private_key, + short_ids: sc.short_ids, + dest: dest.clone(), + max_time_diff: None, + min_client_version: None, + max_client_version: None, + cipher_suites: sc.cipher_suites, + effective_selector: selector.clone(), + inner_protocol: InnerProtocol::Normal(Box::new(PortForwardServerHandler::new( + vec![dest], + selector, + ))), + dest_client_chain: ClientProxyChain::new( + vec![InitialHopEntry::Direct(Arc::new( + SocketConnectorImpl::from_config(&crate::config::ClientConfig::default(), None) + .unwrap(), + ))], + Vec::new(), + ), + }; + let resolver: Arc = Arc::new(crate::resolver::NativeResolver::new()); + let (io, mut peer) = tokio::io::duplex(65536); + let mut flight = hello.clone(); + flight.extend_from_slice(b"pending"); + peer.write_all(&flight).await.unwrap(); + let mut io: Box = Box::new(io); + let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap(); + assert!(parsed.supports_tls13); + assert_eq!(parsed.client_reader.unparsed_data(), b"pending"); + let decoy_io = async { + let (mut socket, _) = listener.accept().await.unwrap(); + let mut forwarded = vec![0; hello.len()]; + socket.read_exact(&mut forwarded).await.unwrap(); + assert_eq!(forwarded, hello); + socket.write_all(&response).await.unwrap(); + let mut upstream = Vec::new(); + socket.read_to_end(&mut upstream).await.unwrap(); + assert_eq!(upstream, b"pending-live"); + socket.write_all(b"-reply").await.unwrap(); + socket.shutdown().await.unwrap(); + }; + let handler = async { + let result = setup_reality_server_stream(io, &target, parsed, &resolver) + .await + .unwrap(); + let TcpServerSetupResult::Session(session) = result else { + panic!("expected transparent fallback for {case}"); + }; + session.await; + }; + let peer_io = async { + let mut actual = vec![0; response.len()]; + peer.read_exact(&mut actual).await.unwrap(); + assert_eq!(actual, response, "{case}"); + peer.write_all(b"-live").await.unwrap(); + peer.shutdown().await.unwrap(); + let mut reply = Vec::new(); + peer.read_to_end(&mut reply).await.unwrap(); + assert_eq!(reply, b"-reply"); + }; + tokio::time::timeout(Duration::from_secs(2), async { + tokio::join!(handler, decoy_io, peer_io); + }) + .await + .unwrap_or_else(|_| panic!("transparent forwarding stalled for {case}")); + let logs = capture.finish(); + if auth_failure { + assert!(logs.contains("REALITY: Auth failed"), "{case}: {logs}"); + } + if matches!(case, "small" | "small-separate" | "unknown-extension") { + assert!( + logs.contains("Auth succeeded, building response"), + "{case}: {logs}" + ); + assert!( + logs.contains("REALITY: Cannot synthesize"), + "{case}: {logs}" + ); + } + if case == "combined-trailing" { + assert!( + logs.contains("REALITY: Dest handshake failed"), + "{case}: {logs}" + ); + } + } +} + +#[tokio::test] +async fn authenticated_non_vision_oversized_preread_returns_error_without_panicking() { + use crate::client_proxy_chain::InitialHopEntry; + use crate::port_forward_handler::PortForwardServerHandler; + use crate::reality::RealityClientConfig; + use crate::tcp::socket_connector_impl::SocketConnectorImpl; + use aws_lc_rs::agreement; + + let private_key = [1; 32]; + let public_key = agreement::PrivateKey::from_private_key(&agreement::X25519, &private_key) + .unwrap() + .compute_public_key() + .unwrap(); + let mut client = RealityClientConfig { + mldsa65_verify: None, + public_key: public_key.as_ref().try_into().unwrap(), + short_id: [0; 8], + server_name: "localhost".try_into().unwrap(), + cipher_suites: Vec::new(), + } + .create_connection() + .unwrap(); + let mut hello = Vec::new(); + client.write_tls(&mut hello).unwrap(); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let dest = NetLocation::new( + crate::address::Address::Hostname("localhost".into()), + address.port(), + ); + let cert = rcgen::generate_simple_self_signed( + (0..100) + .map(|index| format!("host-{index}.example.test")) + .collect::>(), + ) + .unwrap(); + let decoy_config = + rustls::ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13]) + .with_no_client_auth() + .with_single_cert( + vec![cert.cert.der().clone()], + rustls::pki_types::PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()) + .into(), + ) + .unwrap(); + let mut decoy = + TlsConnection::new(rustls::ServerConnection::new(Arc::new(decoy_config)).unwrap()); + decoy.feed(&hello).unwrap(); + let mut response = Vec::new(); + decoy.write_tls(&mut response).unwrap(); + let selector = Arc::new(ClientProxySelector::new(Vec::new())); + let target = RealityServerTarget { + mldsa65_key: None, + private_key, + short_ids: vec![[0; 8]], + dest: dest.clone(), + max_time_diff: None, + min_client_version: None, + max_client_version: None, + cipher_suites: Vec::new(), + effective_selector: selector.clone(), + inner_protocol: InnerProtocol::Normal(Box::new(PortForwardServerHandler::new( + vec![dest], + selector, + ))), + dest_client_chain: ClientProxyChain::new( + vec![InitialHopEntry::Direct(Arc::new( + SocketConnectorImpl::from_config(&crate::config::ClientConfig::default(), None) + .unwrap(), + ))], + Vec::new(), + ), + }; + let resolver: Arc = Arc::new(crate::resolver::NativeResolver::new()); + let (io, mut peer) = tokio::io::duplex(65540); + let mut flight = hello.clone(); + flight.extend_from_slice(&[0x17, 0x03, 0x03, 0, 1, 0].repeat(6000)); + peer.write_all(&flight).await.unwrap(); + let mut io: Box = Box::new(io); + let parsed = crate::shadow_tls::read_client_hello(&mut io).await.unwrap(); + assert!(parsed.client_reader.unparsed_data().len() > 33290); + + let decoy_io = async { + let (mut socket, _) = listener.accept().await.unwrap(); + let mut forwarded_hello = vec![0; hello.len()]; + socket.read_exact(&mut forwarded_hello).await.unwrap(); + assert_eq!(forwarded_hello, hello); + socket.write_all(&response).await.unwrap(); + }; + let (result, ()) = tokio::time::timeout(Duration::from_secs(2), async { + tokio::join!( + setup_reality_server_stream(io, &target, parsed, &resolver), + decoy_io, + ) + }) + .await + .unwrap(); + let error = result.err().expect("invalid encrypted records must fail"); + assert_eq!(error.kind(), io::ErrorKind::InvalidData, "{error}"); +} + +#[tokio::test] +async fn fallback_preread_backpressure_does_not_block_server_response() { + let (client, mut client_peer) = tokio::io::duplex(64); + let (dest, mut dest_peer) = tokio::io::duplex(64); + let pending = vec![42; 128]; + let TcpServerSetupResult::Session(session) = start_forward_to_dest( + Box::new(client), + Box::new(dest), + vec![Bytes::from_static(b"server")], + Bytes::new(), + Bytes::copy_from_slice(&pending), + ) else { + panic!("expected fallback session"); + }; + tokio::time::timeout(Duration::from_secs(1), async { + tokio::join!(session, async { + let mut response = [0; 6]; + client_peer.read_exact(&mut response).await.unwrap(); + assert_eq!(&response, b"server"); + let mut received = vec![0; pending.len()]; + dest_peer.read_exact(&mut received).await.unwrap(); + assert_eq!(received, pending); + client_peer.shutdown().await.unwrap(); + dest_peer.shutdown().await.unwrap(); + }); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn fallback_preserves_preread_client_bytes() { + let (client, mut client_peer) = tokio::io::duplex(64); + let (dest, mut dest_peer) = tokio::io::duplex(64); + let TcpServerSetupResult::Session(session) = start_forward_to_dest( + Box::new(client), + Box::new(dest), + vec![Bytes::from_static(b"server")], + Bytes::from_static(b"-tail"), + Bytes::from_static(b"client"), + ) else { + panic!("expected fallback session"); + }; + tokio::time::timeout(Duration::from_secs(1), async { + tokio::join!(session, async { + client_peer.write_all(b"-live").await.unwrap(); + client_peer.shutdown().await.unwrap(); + let mut received = [0; 11]; + dest_peer.read_exact(&mut received).await.unwrap(); + assert_eq!(&received, b"client-live"); + dest_peer.write_all(b"-reply").await.unwrap(); + dest_peer.shutdown().await.unwrap(); + let mut response = Vec::new(); + client_peer.read_to_end(&mut response).await.unwrap(); + assert_eq!(response, b"server-tail-reply"); + }); + }) + .await + .unwrap(); +} + +struct StalledShutdown { + fail_write: bool, + fail_flush: bool, + _marker: Arc<()>, +} + +impl AsyncRead for StalledShutdown { + fn poll_read( + self: Pin<&mut Self>, + _: &mut Context<'_>, + _: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncWrite for StalledShutdown { + fn poll_write( + self: Pin<&mut Self>, + _: &mut Context<'_>, + bytes: &[u8], + ) -> Poll> { + if self.fail_write { + Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())) + } else { + Poll::Ready(Ok(bytes.len())) + } + } + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + if self.fail_flush { + Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())) + } else { + Poll::Ready(Ok(())) + } + } + fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Pending + } +} + +impl crate::async_stream::AsyncPing for StalledShutdown { + fn supports_ping(&self) -> bool { + false + } + fn poll_write_ping(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(false)) + } +} + +impl AsyncStream for StalledShutdown {} + +#[tokio::test(start_paused = true)] +async fn fallback_cleanup_is_bounded_after_copy_and_early_errors() { + let bytes = Bytes::from_static(b"handshake"); + let cases = [ + (false, false, vec![], Bytes::new(), 10), + (true, false, vec![bytes.clone()], Bytes::new(), 5), + (false, true, vec![bytes.clone()], Bytes::new(), 5), + (true, false, vec![], bytes, 5), + ]; + for (fail_write, fail_flush, records, remaining, seconds) in cases { + let marker = Arc::new(()); + let client = StalledShutdown { + fail_write, + fail_flush, + _marker: marker.clone(), + }; + let dest = StalledShutdown { + fail_write: false, + fail_flush: false, + _marker: marker.clone(), + }; + let TcpServerSetupResult::Session(session) = start_forward_to_dest( + Box::new(client), + Box::new(dest), + records, + remaining, + Bytes::new(), + ) else { + panic!("fallback must remain owned by its caller") + }; + let started = tokio::time::Instant::now(); + tokio::time::timeout(std::time::Duration::from_secs(11), session) + .await + .expect("fallback cleanup retained the streams indefinitely"); + assert_eq!(started.elapsed(), std::time::Duration::from_secs(seconds)); + assert_eq!(Arc::strong_count(&marker), 1); + } +} diff --git a/src/tcp/tcp_client_handler_factory.rs b/src/tcp/tcp_client_handler_factory.rs index c5ac8d96..f6c7f9c8 100644 --- a/src/tcp/tcp_client_handler_factory.rs +++ b/src/tcp/tcp_client_handler_factory.rs @@ -14,6 +14,7 @@ use crate::h2mux::H2MuxClientHandler; use crate::http_handler::HttpTcpClientHandler; use crate::naiveproxy::NaiveProxyTcpClientHandler; use crate::port_forward_handler::PortForwardClientHandler; +use crate::reality::RealityClientHandler; use crate::resolver::Resolver; use crate::rustls_config_util::try_create_client_config; use crate::shadow_tls::ShadowTlsClientHandler; @@ -285,7 +286,7 @@ pub fn create_tcp_client_handler( .expect("Invalid user_id UUID") .into_boxed_slice(); Box::new( - crate::reality_client_handler::RealityClientHandler::new_vision_vless( + RealityClientHandler::new_vision_vless( public_key_bytes, short_id_bytes, server_name, @@ -298,7 +299,7 @@ pub fn create_tcp_client_handler( } else { let inner_handler = create_tcp_client_handler(*protocol, None, resolver.clone())?; Box::new( - crate::reality_client_handler::RealityClientHandler::new( + RealityClientHandler::new( public_key_bytes, short_id_bytes, server_name, diff --git a/src/tls_client_handler.rs b/src/tls_client_handler.rs index 3573652b..8fbc81ca 100644 --- a/src/tls_client_handler.rs +++ b/src/tls_client_handler.rs @@ -181,7 +181,7 @@ mod tests { #[tokio::test] async fn only_default_tls_and_reality_handlers_forward_reuse() { - use crate::reality_client_handler::RealityClientHandler; + use crate::reality::RealityClientHandler; let config = Arc::new( rustls::ClientConfig::builder() .with_root_certificates(rustls::RootCertStore::empty())