Organize REALITY modules and tests (#211)

Clarifies the fork-backed REALITY code layout by naming modules after their responsibilities, colocating client and server handlers, and grouping shared fixtures and handshake tests under src/reality/tests. Keeps the reality_ filename prefixes and preserves protocol behavior while reusing existing helpers and simplifying imports.
This commit is contained in:
cfal
2026-10-22 19:34:05 -04:00
committed by GitHub
parent 4e0bd96a34
commit c24bdf8be5
22 changed files with 1176 additions and 1189 deletions
-1
View File
@@ -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;
-1
View File
@@ -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;
+13 -14
View File
@@ -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};
+6 -20
View File
@@ -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.
+1 -1
View File
@@ -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)]
@@ -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<crate::reality::Mldsa65VerifyKey>,
mldsa65_verify: Option<Mldsa65VerifyKey>,
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<crate::reality::Mldsa65VerifyKey>) -> Self {
pub fn with_mldsa65_verify(mut self, key: Option<Mldsa65VerifyKey>) -> Self {
self.mldsa65_verify = key;
self
}
@@ -110,63 +110,6 @@ fn hex_char_to_value(c: u8) -> Result<u8, &'static str> {
}
}
/// 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};
@@ -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<u8> {
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();
-334
View File
@@ -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<Bytes> {
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<Bytes> {
target_records(
RealityGroup::X25519MlKem768,
CipherSuite::AES_128_GCM_SHA256,
session,
)
}
pub(super) fn separate_target_records(
group: RealityGroup,
cipher: CipherSuite,
session: &[u8],
) -> Vec<Bytes> {
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<Bytes>,
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"
);
}
}
}
@@ -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 {
+5 -4
View File
@@ -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};
+3 -662
View File
@@ -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<Bytes>,
reads: usize,
}
impl AsyncRead for TargetReads {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buffer: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
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<usize> {
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<dyn Resolver> = 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<dyn AsyncStream> = 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::<Vec<_>>(),
)
.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<dyn Resolver> = 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<dyn AsyncStream> = 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<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
impl AsyncWrite for StalledShutdown {
fn poll_write(
self: Pin<&mut Self>,
_: &mut Context<'_>,
bytes: &[u8],
) -> Poll<io::Result<usize>> {
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<io::Result<()>> {
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<io::Result<()>> {
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<io::Result<bool>> {
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;
-43
View File
@@ -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<Vec<u8>> {
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<u8> {
let mut header = vec![record_type, 3, 3];
header.extend_from_slice(&length.to_be_bytes());
header
}
+3
View File
@@ -0,0 +1,3 @@
pub(super) mod reality_fixtures;
mod reality_handshake;
mod reality_post_quantum;
+213
View File
@@ -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<Vec<u8>> {
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<u8> {
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<Bytes> {
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<Bytes> {
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<Bytes> {
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<Bytes>,
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,
)
}
@@ -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<std::collections::HashMap<String, Vec<u8>>>);
@@ -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<Bytes>) {
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,
);
}
}
}
+152
View File
@@ -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"
);
}
}
}
+653
View File
@@ -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<Bytes>,
reads: usize,
}
impl AsyncRead for TargetReads {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buffer: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
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<usize> {
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<dyn Resolver> = 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<dyn AsyncStream> = 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::<Vec<_>>(),
)
.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<dyn Resolver> = 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<dyn AsyncStream> = 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<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
impl AsyncWrite for StalledShutdown {
fn poll_write(
self: Pin<&mut Self>,
_: &mut Context<'_>,
bytes: &[u8],
) -> Poll<io::Result<usize>> {
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<io::Result<()>> {
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<io::Result<()>> {
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<io::Result<bool>> {
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);
}
}
+3 -2
View File
@@ -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,
+1 -1
View File
@@ -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())