mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 08:14:11 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
@@ -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 {
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(super) mod reality_fixtures;
|
||||
mod reality_handshake;
|
||||
mod reality_post_quantum;
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user