diff --git a/examples/src/bin/reality-client.rs b/examples/src/bin/reality-client.rs deleted file mode 100644 index da751864425..00000000000 --- a/examples/src/bin/reality-client.rs +++ /dev/null @@ -1,248 +0,0 @@ -//! Example client demonstrating the VLESS Reality protocol -//! -//! Reality is a protocol extension that provides enhanced privacy by encrypting -//! the TLS session ID using a shared secret derived from X25519 ECDH with the -//! server's public key. -//! -//! This example shows how to: -//! 1. Create a RealityConfig with server public key and short_id -//! 2. Build a ClientConfig with Reality enabled -//! 3. Make a TLS connection using the Reality protocol -//! -//! Note: This example requires a Reality-enabled server to complete the handshake. -//! The server must be configured with the corresponding X25519 private key. -//! -//! # Usage -//! -//! ```bash -//! cargo run --example reality-client -//! ``` -//! -//! Example (using airport Reality config format): -//! ```bash -//! cargo run --example reality-client example.com:443 \ -//! Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs \ -//! 1bc2c1ef1c -//! ``` - -use std::env; -use std::io::{stdout, Read, Write}; -use std::net::TcpStream; -use std::sync::Arc; - -use watfaq_rustls::client::RealityConfig; -use watfaq_rustls::pki_types; -use watfaq_rustls::RootCertStore; - -fn main() { - // Parse command line arguments - let args: Vec = env::args().collect(); - if args.len() != 5 { - eprintln!( - "Usage: {} ", - args[0] - ); - eprintln!(); - eprintln!("Parameters:"); - eprintln!(" Real server address (e.g., tw04.ctg.wtf:443)"); - eprintln!(" SNI hostname for disguise (e.g., www.microsoft.com)"); - eprintln!(" Server's X25519 public key in Base64 format"); - eprintln!(" Client identifier in hexadecimal format"); - eprintln!(); - eprintln!("Example using VLESS Reality config:"); - eprintln!(" If you have this config:"); - eprintln!(" server: tw04.ctg.wtf"); - eprintln!(" port: 443"); - eprintln!(" servername: www.microsoft.com"); - eprintln!(" reality-opts:"); - eprintln!(" public-key: Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"); - eprintln!(" short-id: 1bc2c1ef1c"); - eprintln!(); - eprintln!("Run:"); - eprintln!(" {} tw04.ctg.wtf:443 \\", args[0]); - eprintln!(" www.microsoft.com \\"); - eprintln!(" Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs \\"); - eprintln!(" 1bc2c1ef1c"); - std::process::exit(1); - } - - let server_addr = args[1].clone(); - let sni_servername = args[2].clone(); - let public_key_base64 = args[3].clone(); - let short_id_hex = args[4].clone(); - - // Parse server public key from Base64 (airport format) - let server_pubkey = base64_to_bytes(&public_key_base64) - .and_then(|bytes| { - if bytes.len() == 32 { - let mut arr = [0u8; 32]; - arr.copy_from_slice(&bytes); - Ok(arr) - } else { - Err(format!( - "Server public key must be exactly 32 bytes, got {} bytes", - bytes.len() - )) - } - }) - .unwrap_or_else(|e| { - eprintln!("Error parsing server public key (Base64): {}", e); - std::process::exit(1); - }); - - // Parse short_id - let short_id = hex_to_bytes(&short_id_hex).unwrap_or_else(|e| { - eprintln!("Error parsing short_id: {}", e); - std::process::exit(1); - }); - - if short_id.len() > 8 { - eprintln!("Error: short_id must be at most 8 bytes (16 hex characters)"); - std::process::exit(1); - } - - let short_id_len = short_id.len(); - - // Create Reality configuration - let reality_config = RealityConfig::new(server_pubkey, short_id).unwrap_or_else(|e| { - eprintln!("Error creating Reality config: {}", e); - std::process::exit(1); - }); - - println!("Reality configuration created successfully"); - println!(" Server public key: {}", bytes_to_hex(&server_pubkey)); - println!(" Short ID: {}", short_id_hex); - println!("\nDebug Info:"); - println!(" Base64 public key input: {}", public_key_base64); - println!(" Hex short_id input: {}", short_id_hex); - println!(" Short ID length: {} bytes", short_id_len); - - // Load root certificates - let root_store = RootCertStore { - roots: webpki_roots::TLS_SERVER_ROOTS.into(), - }; - - // Build client configuration with Reality - let mut config = watfaq_rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_reality(reality_config) - .with_no_client_auth(); - - // Allow using SSLKEYLOGFILE for debugging - config.key_log = Arc::new(watfaq_rustls::KeyLogFile::new()); - - println!( - "\nConnecting to {} (SNI: {})...", - &server_addr, &sni_servername - ); - - // Use SNI servername for TLS connection (for disguise/camouflage) - let server_name: pki_types::ServerName<'static> = sni_servername - .to_string() - .try_into() - .unwrap_or_else(|e| { - eprintln!("Error parsing SNI servername: {:?}", e); - std::process::exit(1); - }); - - // Create TLS connection - let mut conn = watfaq_rustls::ClientConnection::new(Arc::new(config), server_name) - .unwrap_or_else(|e| { - eprintln!("Error creating client connection: {}", e); - std::process::exit(1); - }); - - // Connect to server - let mut sock = TcpStream::connect(&server_addr).unwrap_or_else(|e| { - eprintln!("Error connecting to server: {}", e); - std::process::exit(1); - }); - - let mut tls = watfaq_rustls::Stream::new(&mut conn, &mut sock); - - // Send a simple HTTP request - println!("Performing TLS handshake and sending HTTP request..."); - let request = format!( - "GET / HTTP/1.1\r\n\ - Host: {}\r\n\ - Connection: close\r\n\ - Accept-Encoding: identity\r\n\ - \r\n", - sni_servername - ); - - if let Err(e) = tls.write_all(request.as_bytes()) { - eprintln!("\n❌ Error during TLS communication: {}", e); - eprintln!("\nPossible reasons:"); - eprintln!(" 1. Server's private key doesn't match the provided public key"); - eprintln!(" 2. Server is not a Reality-enabled server"); - eprintln!(" 3. Reality protocol version mismatch"); - eprintln!(" 4. Network/firewall issues"); - std::process::exit(1); - } - - println!("✓ TLS handshake completed successfully with Reality protocol!"); - - // Print connection details - if let Some(ciphersuite) = tls.conn.negotiated_cipher_suite() { - println!("✓ Cipher suite: {:?}", ciphersuite.suite()); - } - if let Some(version) = tls.conn.protocol_version() { - println!("✓ Protocol version: {:?}", version); - } - println!("✓ Request sent successfully"); - - // Read and print response - println!("\nServer response:"); - println!("----------------------------------------"); - let mut plaintext = Vec::new(); - tls.read_to_end(&mut plaintext) - .unwrap_or_else(|e| { - eprintln!("Error reading response: {}", e); - std::process::exit(1); - }); - stdout().write_all(&plaintext).unwrap(); - println!("----------------------------------------"); - println!("\nConnection closed successfully"); -} - -/// Helper function to convert hex string to bytes -fn hex_to_bytes(hex: &str) -> Result, &'static str> { - if !hex.len().is_multiple_of(2) { - return Err("Hex string must have even length"); - } - - let mut bytes = Vec::new(); - for i in (0..hex.len()).step_by(2) { - let byte_str = &hex[i..i + 2]; - let byte = u8::from_str_radix(byte_str, 16).map_err(|_| "Invalid hex character")?; - bytes.push(byte); - } - Ok(bytes) -} - -/// Helper function to convert bytes to hex string -fn bytes_to_hex(bytes: &[u8]) -> String { - bytes - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join("") -} - -/// Helper function to decode Base64 string to bytes -/// -/// Supports both standard Base64 and Base64 URL-safe encoding. -fn base64_to_bytes(base64_str: &str) -> Result, String> { - use base64::Engine; - - // Try standard Base64 first - if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(base64_str) { - return Ok(bytes); - } - - // Try URL-safe Base64 (some airports use this) - base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(base64_str) - .map_err(|e| format!("Invalid Base64: {}", e)) -} diff --git a/examples/src/bin/reality-vless-probe.rs b/examples/src/bin/reality-vless-probe.rs new file mode 100644 index 00000000000..e82937249ae --- /dev/null +++ b/examples/src/bin/reality-vless-probe.rs @@ -0,0 +1,281 @@ +//! VLESS Reality probe: sends a VLESS frame after the Reality TLS handshake +//! to distinguish between Reality auth success and fallback. +//! +//! Outcome interpretation: +//! - Response from → Reality auth OK, UUID accepted → full proxy working +//! - Connection closed / reset → Reality auth OK, UUID rejected (server recognised us as Reality client) +//! - Response from → Reality auth FAILED → server fell back to SNI destination +//! +//! Usage: +//! cargo run --bin reality-vless-probe \ +//! +//! +//! Example: +//! cargo run --bin reality-vless-probe \ +//! tw05.ctg.wtf:443 www.microsoft.com \ +//! Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs 1bc2c1ef1c \ +//! 5415d8e0-df92-3655-afa4-b79de66413f5 + +use std::env; +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::sync::Arc; + +use watfaq_rustls::client::RealityConfig; +use watfaq_rustls::pki_types; +use watfaq_rustls::RootCertStore; + +// Target we ask the proxy to reach. Plain HTTP so the response is readable. +const PROXY_TARGET_HOST: &str = "example.com"; +const PROXY_TARGET_PORT: u16 = 80; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() != 6 { + eprintln!( + "Usage: {} ", + args[0] + ); + eprintln!(" UUID format: 5415d8e0-df92-3655-afa4-b79de66413f5 (with or without dashes)"); + std::process::exit(1); + } + + let server_addr = &args[1]; + let sni_servername = &args[2]; + let public_key_base64 = &args[3]; + let short_id_hex = &args[4]; + let uuid_str = &args[5]; + + // --- Parse server public key --- + let server_pubkey: [u8; 32] = base64_to_bytes(public_key_base64) + .and_then(|b| { + b.try_into() + .map_err(|_| "Server public key must be exactly 32 bytes".to_string()) + }) + .unwrap_or_else(|e| { + eprintln!("Error parsing server public key: {}", e); + std::process::exit(1); + }); + + // --- Parse short_id --- + let short_id = hex_to_bytes(short_id_hex).unwrap_or_else(|e| { + eprintln!("Error parsing short_id: {}", e); + std::process::exit(1); + }); + if short_id.len() > 8 { + eprintln!("Error: short_id must be at most 8 bytes"); + std::process::exit(1); + } + + // --- Parse UUID (accepts "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" or raw 32 hex chars) --- + let uuid: [u8; 16] = parse_uuid(uuid_str).unwrap_or_else(|e| { + eprintln!("Error parsing UUID: {}", e); + std::process::exit(1); + }); + + // Install the default crypto provider (aws-lc-rs is the default feature for watfaq-rustls) + let _ = watfaq_rustls::crypto::aws_lc_rs::default_provider().install_default(); + + println!("=== Reality VLESS Probe ==="); + println!(" Server addr : {}", server_addr); + println!(" SNI : {}", sni_servername); + println!(" short_id : {}", short_id_hex); + println!(" UUID : {}", format_uuid(&uuid)); + println!(" Proxy target: {}:{}", PROXY_TARGET_HOST, PROXY_TARGET_PORT); + + // --- Build TLS client config with Reality --- + let reality_config = RealityConfig::new(server_pubkey, short_id).unwrap_or_else(|e| { + eprintln!("Error creating Reality config: {}", e); + std::process::exit(1); + }); + + let root_store = RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.into(), + }; + + let config = watfaq_rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_reality(reality_config) + .with_no_client_auth(); + + let server_name: pki_types::ServerName<'static> = sni_servername + .clone() + .try_into() + .unwrap_or_else(|e| { + eprintln!("Error parsing SNI servername: {:?}", e); + std::process::exit(1); + }); + + let mut conn = watfaq_rustls::ClientConnection::new(Arc::new(config), server_name) + .unwrap_or_else(|e| { + eprintln!("Error creating client connection: {}", e); + std::process::exit(1); + }); + + let mut sock = TcpStream::connect(server_addr).unwrap_or_else(|e| { + eprintln!("Error connecting to {}: {}", server_addr, e); + std::process::exit(1); + }); + + // Set read timeout before creating the TLS stream (which mutably borrows sock) + sock.set_read_timeout(Some(std::time::Duration::from_secs(8))) + .ok(); + + let mut tls = watfaq_rustls::Stream::new(&mut conn, &mut sock); + + // --- Build VLESS frame --- + // + // VLESS request header: + // [1] version = 0x00 + // [16] UUID + // [1] addon_len = 0x00 (no addons) + // [1] command = 0x01 (TCP) + // [2] port (big-endian) + // [1] addr_type = 0x02 (domain) + // [1] domain_len + // [N] domain + // + // Followed immediately by the proxied TCP payload. + let mut frame: Vec = Vec::new(); + frame.push(0x00); // version + frame.extend_from_slice(&uuid); // UUID + frame.push(0x00); // addon length + frame.push(0x01); // command: TCP + frame.extend_from_slice(&PROXY_TARGET_PORT.to_be_bytes()); // port + frame.push(0x02); // addr type: domain + frame.push(PROXY_TARGET_HOST.len() as u8); // domain length + frame.extend_from_slice(PROXY_TARGET_HOST.as_bytes()); // domain + + // Proxied HTTP request to PROXY_TARGET_HOST + let http_req = format!( + "GET / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", + PROXY_TARGET_HOST + ); + frame.extend_from_slice(http_req.as_bytes()); + + println!( + "\n[1] Sending VLESS frame ({} bytes) over Reality TLS...", + frame.len() + ); + + if let Err(e) = tls.write_all(&frame) { + eprintln!("\n[FAIL] TLS write error: {}", e); + eprintln!(" → Reality TLS handshake itself failed (cert mismatch / wrong public key?)"); + std::process::exit(1); + } + println!("[2] Frame sent. Reading response...\n"); + + let mut buf = vec![0u8; 65536]; + let mut response = Vec::new(); + + loop { + match tls.read(&mut buf) { + Ok(0) => break, + Ok(n) => { + response.extend_from_slice(&buf[..n]); + if response.len() > 4096 { + break; + } + } + Err(e) + if e.kind() == std::io::ErrorKind::WouldBlock + || e.kind() == std::io::ErrorKind::TimedOut => + { + break + } + Err(_) => break, + } + } + + // --- Print raw response --- + println!("=== Response ({} bytes) ===", response.len()); + if let Ok(text) = std::str::from_utf8(&response[..response.len().min(1024)]) { + println!("{}", text); + } else { + println!( + "(binary, first 32 bytes: {})", + bytes_to_hex(&response[..response.len().min(32)]) + ); + } + + // --- Verdict --- + println!("\n=== Verdict ==="); + let s = String::from_utf8_lossy(&response); + if response.is_empty() { + println!("✓ Reality auth likely SUCCEEDED — connection closed with no data."); + println!(" Server recognised us as a VLESS client but something was rejected."); + println!(" (UUID mismatch, command unsupported, or proxy target unreachable.)"); + } else if s.contains("example.com") + || s.contains("IANA") + || s.contains("illustrative examples") + || s.contains("Example Domain") + { + println!("✓✓ Reality auth SUCCEEDED and UUID accepted!"); + println!(" Got response from {} via VLESS proxy.", PROXY_TARGET_HOST); + } else if s.contains("microsoft") + || s.contains("Microsoft") + || s.contains("AkamaiNetStorage") + || s.contains("AkamaiGHost") + || s.contains("Akamai") + || s.contains("mscom") + { + println!("✗ Reality auth FAILED — fell back to SNI destination ({}).", sni_servername); + println!(" Possible causes: wrong public key, short_id not allowed,"); + println!(" or timestamp drift exceeds server tolerance."); + } else { + println!( + "? Inconclusive — {} bytes received, cannot identify source.", + response.len() + ); + } +} + +/// Parse UUID in "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" or "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" form. +fn parse_uuid(s: &str) -> Result<[u8; 16], String> { + let hex_only: String = s.chars().filter(|&c| c != '-').collect(); + if hex_only.len() != 32 { + return Err(format!( + "UUID must be 32 hex chars (got {})", + hex_only.len() + )); + } + let bytes = hex_to_bytes(&hex_only)?; + bytes + .try_into() + .map_err(|_| "UUID conversion failed".into()) +} + +fn format_uuid(b: &[u8; 16]) -> String { + format!( + "{}-{}-{}-{}-{}", + bytes_to_hex(&b[0..4]), + bytes_to_hex(&b[4..6]), + bytes_to_hex(&b[6..8]), + bytes_to_hex(&b[8..10]), + bytes_to_hex(&b[10..16]), + ) +} + +fn hex_to_bytes(hex: &str) -> Result, String> { + if hex.len() % 2 != 0 { + return Err("Hex string must have even length".into()); + } + (0..hex.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&hex[i..i + 2], 16).map_err(|_| "Invalid hex character".into()) + }) + .collect() +} + +fn bytes_to_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() +} + +fn base64_to_bytes(s: &str) -> Result, String> { + use base64::Engine; + base64::engine::general_purpose::STANDARD + .decode(s) + .or_else(|_| base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s)) + .map_err(|e| format!("Invalid Base64: {}", e)) +} diff --git a/rustls/src/client/builder.rs b/rustls/src/client/builder.rs index 9506e885efb..a4f397742e8 100644 --- a/rustls/src/client/builder.rs +++ b/rustls/src/client/builder.rs @@ -159,6 +159,13 @@ impl ConfigBuilder { /// .with_no_client_auth(); /// ``` pub fn with_reality(mut self, config: crate::client::reality::RealityConfig) -> Self { + #[cfg(feature = "std")] + { + use crate::client::reality::RealityServerCertVerifier; + let auth_key_slot = Arc::clone(&config.auth_key_slot); + let inner = Arc::clone(&self.state.verifier); + self.state.verifier = RealityServerCertVerifier::new(auth_key_slot, inner); + } self.state.reality_config = Some(Arc::new(config)); self } diff --git a/rustls/src/client/reality.rs b/rustls/src/client/reality.rs index 3a901c73960..3fc19983455 100644 --- a/rustls/src/client/reality.rs +++ b/rustls/src/client/reality.rs @@ -11,9 +11,9 @@ //! ## 1. Reality Authentication (session_id encryption) //! 1. Client generates ephemeral X25519 keypair (client_private, client_public) //! 2. Client performs ECDH with server's **static public key**: auth_shared_secret = ECDH(client_private, server_static_public_key) -//! 3. Client derives auth_key using HKDF-SHA256(auth_shared_secret, hello_random[:20], "REALITY") +//! 3. Client derives auth_key using HKDF-SHA256(auth_shared_secret, hello_random[:20], "REALITY") → 32 bytes //! 4. Client constructs 16-byte plaintext: [version(3) | reserved(1) | timestamp(4) | short_id(8)] -//! 5. Client encrypts plaintext using AES-128-GCM with: +//! 5. Client encrypts plaintext using AES-256-GCM with: //! - key: auth_key //! - nonce: hello_random[20..32] //! - aad: full ClientHello bytes @@ -40,6 +40,9 @@ use crate::msgs::enums::NamedGroup; use crate::msgs::handshake::{KeyShareEntry, Random}; use crate::SupportedCipherSuite; +#[cfg(feature = "std")] +use std::sync::Mutex; + /// VLESS Reality protocol configuration /// /// This configuration specifies the parameters needed for the Reality protocol, @@ -70,6 +73,10 @@ pub struct RealityConfig { short_id: Vec, /// Protocol version (3 bytes, default [0, 0, 0]) client_version: [u8; 3], + /// Shared slot for the derived auth_key, populated during handshake + /// and consumed by RealityServerCertVerifier + #[cfg(feature = "std")] + pub(crate) auth_key_slot: Arc>>, } impl RealityConfig { @@ -101,6 +108,8 @@ impl RealityConfig { server_public_key, short_id, client_version: [0, 0, 0], + #[cfg(feature = "std")] + auth_key_slot: Arc::new(Mutex::new(None)), }) } @@ -308,10 +317,11 @@ impl RealitySessionState { ) -> Result<[u8; 32], Error> { // Step 1: Derive auth_key using HKDF-SHA256 // auth_key = HKDF(auth_shared_secret, salt=hello_random[:20], info="REALITY") + // Key is 32 bytes → used with AES-256-GCM (matching Xray reference implementation) let salt = &random.0[..20]; let auth_key_expander = hkdf.extract_from_secret(Some(salt), &self.auth_shared_secret); - let mut auth_key = [0u8; 16]; + let mut auth_key = [0u8; 32]; auth_key_expander .expand_slice(&[b"REALITY"], &mut auth_key) .map_err(|_| Error::General("HKDF expand failed".into()))?; @@ -332,14 +342,20 @@ impl RealitySessionState { plaintext[8..8 + short_id_len].copy_from_slice(&self.config.short_id); // Remaining bytes are already zero - // Step 3: AES-128-GCM encryption + // Step 3: AES-256-GCM encryption // nonce = hello_random[20..32] (12 bytes) // aad = full ClientHello bytes let nonce: &[u8; 12] = random.0[20..32] .try_into() .map_err(|_| Error::General("Invalid nonce length".into()))?; - let result = aes_128_gcm_encrypt(&auth_key, nonce, hello_bytes, &plaintext)?; + let result = aes_256_gcm_encrypt(&auth_key, nonce, hello_bytes, &plaintext)?; + + // Store auth_key in the slot so RealityServerCertVerifier can use it + #[cfg(feature = "std")] + if let Some(mut slot) = self.config.auth_key_slot.lock().ok() { + *slot = Some(auth_key); + } Ok(result) } @@ -387,23 +403,25 @@ impl ActiveKeyExchange for RealityKeyExchange { // X25519 ECDH is now handled through the unified CryptoProvider::x25519_provider interface // No need for provider-specific implementations here -/// AES-128-GCM encryption for Reality session_id +/// AES-256-GCM encryption for Reality session_id /// /// Encrypts 16-byte plaintext and returns ciphertext + tag (32 bytes total). -fn aes_128_gcm_encrypt( - key: &[u8; 16], +/// Uses a 32-byte key, matching the Xray reference implementation which derives +/// a 32-byte AuthKey via HKDF-SHA256 and uses it with AES-256-GCM. +fn aes_256_gcm_encrypt( + key: &[u8; 32], nonce: &[u8; 12], aad: &[u8], plaintext: &[u8; 16], ) -> Result<[u8; 32], Error> { #[cfg(feature = "ring")] { - aes_128_gcm_encrypt_ring(key, nonce, aad, plaintext) + aes_256_gcm_encrypt_ring(key, nonce, aad, plaintext) } #[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] { - aes_128_gcm_encrypt_aws_lc_rs(key, nonce, aad, plaintext) + aes_256_gcm_encrypt_aws_lc_rs(key, nonce, aad, plaintext) } #[cfg(not(any(feature = "ring", feature = "aws_lc_rs")))] @@ -414,18 +432,18 @@ fn aes_128_gcm_encrypt( } } -/// AES-128-GCM encryption using ring +/// AES-256-GCM encryption using ring #[cfg(feature = "ring")] -fn aes_128_gcm_encrypt_ring( - key: &[u8; 16], +fn aes_256_gcm_encrypt_ring( + key: &[u8; 32], nonce: &[u8; 12], aad: &[u8], plaintext: &[u8; 16], ) -> Result<[u8; 32], Error> { use ring::aead; - let unbound_key = aead::UnboundKey::new(&aead::AES_128_GCM, key) - .map_err(|_| Error::General("AES-128-GCM key creation failed".into()))?; + let unbound_key = aead::UnboundKey::new(&aead::AES_256_GCM, key) + .map_err(|_| Error::General("AES-256-GCM key creation failed".into()))?; let sealing_key = aead::LessSafeKey::new(unbound_key); let mut in_out = plaintext.to_vec(); @@ -434,7 +452,7 @@ fn aes_128_gcm_encrypt_ring( sealing_key .seal_in_place_append_tag(nonce, aad, &mut in_out) - .map_err(|_| Error::General("AES-128-GCM encryption failed".into()))?; + .map_err(|_| Error::General("AES-256-GCM encryption failed".into()))?; // in_out now contains: plaintext (16 bytes) + tag (16 bytes) = 32 bytes let mut result = [0u8; 32]; @@ -442,18 +460,18 @@ fn aes_128_gcm_encrypt_ring( Ok(result) } -/// AES-128-GCM encryption using aws-lc-rs +/// AES-256-GCM encryption using aws-lc-rs #[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] -fn aes_128_gcm_encrypt_aws_lc_rs( - key: &[u8; 16], +fn aes_256_gcm_encrypt_aws_lc_rs( + key: &[u8; 32], nonce: &[u8; 12], aad: &[u8], plaintext: &[u8; 16], ) -> Result<[u8; 32], Error> { use aws_lc_rs::aead; - let unbound_key = aead::UnboundKey::new(&aead::AES_128_GCM, key) - .map_err(|_| Error::General("AES-128-GCM key creation failed".into()))?; + let unbound_key = aead::UnboundKey::new(&aead::AES_256_GCM, key) + .map_err(|_| Error::General("AES-256-GCM key creation failed".into()))?; let sealing_key = aead::LessSafeKey::new(unbound_key); let mut in_out = plaintext.to_vec(); @@ -462,7 +480,7 @@ fn aes_128_gcm_encrypt_aws_lc_rs( sealing_key .seal_in_place_append_tag(nonce, aad, &mut in_out) - .map_err(|_| Error::General("AES-128-GCM encryption failed".into()))?; + .map_err(|_| Error::General("AES-256-GCM encryption failed".into()))?; let mut result = [0u8; 32]; result.copy_from_slice(&in_out); @@ -494,6 +512,238 @@ pub(crate) fn get_hkdf_sha256_from_config( .ok_or_else(|| Error::General("No SHA256 HKDF available for Reality".into())) } +// ============================================================================ +// RealityServerCertVerifier +// ============================================================================ + +/// Extract the Ed25519 public key bytes from a REALITY server certificate DER. +/// +/// REALITY server certs embed an Ed25519 public key. We locate it by searching +/// for the Ed25519 OID (1.3.101.112 = `06 03 2b 65 70`) followed by a BIT +/// STRING header (`03 21 00`) and then 32 bytes of public key material. +fn extract_ed25519_pubkey_from_reality_cert(cert_der: &[u8]) -> Option<[u8; 32]> { + // Ed25519 OID bytes: 06 03 2b 65 70 + const OID: [u8; 5] = [0x06, 0x03, 0x2b, 0x65, 0x70]; + // BIT STRING: 03 (tag) 21 (length=33) 00 (unused bits=0) + const BIT_STRING_HDR: [u8; 3] = [0x03, 0x21, 0x00]; + + let n = cert_der.len(); + if n < OID.len() + BIT_STRING_HDR.len() + 32 { + return None; + } + + for i in 0..n.saturating_sub(OID.len()) { + if cert_der[i..i + OID.len()] != OID { + continue; + } + // OID found; scan forward a small window for the BIT STRING header + let search_end = (i + OID.len() + 16).min(n.saturating_sub(BIT_STRING_HDR.len() + 32)); + for j in (i + OID.len())..=search_end { + if cert_der[j..j + BIT_STRING_HDR.len()] == BIT_STRING_HDR { + let key_start = j + BIT_STRING_HDR.len(); + if key_start + 32 <= n { + let mut pubkey = [0u8; 32]; + pubkey.copy_from_slice(&cert_der[key_start..key_start + 32]); + return Some(pubkey); + } + } + } + } + None +} + +/// Constant-time byte slice comparison. +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +/// Compute HMAC-SHA512 using ring +#[cfg(feature = "ring")] +fn hmac_sha512(key: &[u8; 32], data: &[u8]) -> [u8; 64] { + use ring::hmac; + let k = hmac::Key::new(hmac::HMAC_SHA512, key); + let tag = hmac::sign(&k, data); + let mut out = [0u8; 64]; + out.copy_from_slice(tag.as_ref()); + out +} + +/// Compute HMAC-SHA512 using aws-lc-rs +#[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] +fn hmac_sha512(key: &[u8; 32], data: &[u8]) -> [u8; 64] { + use aws_lc_rs::hmac; + let k = hmac::Key::new(hmac::HMAC_SHA512, key); + let tag = hmac::sign(&k, data); + let mut out = [0u8; 64]; + out.copy_from_slice(tag.as_ref()); + out +} + +/// Verify an Ed25519 signature using ring +#[cfg(feature = "ring")] +fn ed25519_verify(pubkey: &[u8; 32], message: &[u8], signature: &[u8]) -> bool { + use ring::signature; + let pk = signature::UnparsedPublicKey::new(&signature::ED25519, pubkey.as_ref()); + pk.verify(message, signature).is_ok() +} + +/// Verify an Ed25519 signature using aws-lc-rs +#[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] +fn ed25519_verify(pubkey: &[u8; 32], message: &[u8], signature: &[u8]) -> bool { + use aws_lc_rs::signature; + let pk = signature::UnparsedPublicKey::new(&signature::ED25519, pubkey.as_ref()); + pk.verify(message, signature).is_ok() +} + +/// Check whether a certificate is a valid REALITY server certificate. +/// +/// Returns `Some(Ok(...))` if the cert passes REALITY HMAC verification, +/// `None` if it does not look like a REALITY cert (caller should try the +/// inner verifier), and `Some(Err(...))` on internal error. +#[cfg(any(feature = "ring", feature = "aws_lc_rs"))] +fn verify_reality_cert( + cert: &pki_types::CertificateDer<'_>, + auth_key: &[u8; 32], +) -> Option> { + let cert_bytes = cert.as_ref(); + if cert_bytes.len() < 64 { + return None; + } + + let pubkey = extract_ed25519_pubkey_from_reality_cert(cert_bytes)?; + + let expected = hmac_sha512(auth_key, &pubkey); + let cert_tail = &cert_bytes[cert_bytes.len() - 64..]; + + if constant_time_eq(&expected, cert_tail) { + Some(Ok(crate::verify::ServerCertVerified::assertion())) + } else { + // HMAC mismatch — not a Reality cert for this session + None + } +} + +#[cfg(not(any(feature = "ring", feature = "aws_lc_rs")))] +fn verify_reality_cert( + _cert: &pki_types::CertificateDer<'_>, + _auth_key: &[u8; 32], +) -> Option> { + None +} + +/// A `ServerCertVerifier` that understands REALITY's custom certificate format. +/// +/// REALITY servers present a minimal Ed25519 X.509v1 certificate whose last +/// 64 bytes are overwritten with `HMAC-SHA512(auth_key, ed25519_public_key)`. +/// Standard verifiers (e.g. webpki) reject this cert as `BadEncoding` because +/// X.509v1 certs lack the `version` field that webpki requires. +/// +/// This verifier first attempts the REALITY HMAC check; if it passes the cert +/// is accepted without a CA chain. If it fails (normal TLS destination), the +/// inner verifier is tried. +/// +/// `verify_tls13_signature` similarly tries the inner verifier first; if that +/// fails it falls back to direct Ed25519 verification using the pubkey found +/// in the cert DER. +#[cfg(feature = "std")] +#[derive(Debug)] +pub struct RealityServerCertVerifier { + /// Slot containing the auth_key computed during ClientHello construction + auth_key_slot: Arc>>, + /// Fallback verifier (used when the cert is not a REALITY cert) + inner: Arc, +} + +#[cfg(feature = "std")] +impl RealityServerCertVerifier { + /// Create a new verifier wrapping `inner`. + pub fn new( + auth_key_slot: Arc>>, + inner: Arc, + ) -> Arc { + Arc::new(Self { auth_key_slot, inner }) + } +} + +#[cfg(feature = "std")] +impl crate::verify::ServerCertVerifier for RealityServerCertVerifier { + fn verify_server_cert( + &self, + end_entity: &pki_types::CertificateDer<'_>, + intermediates: &[pki_types::CertificateDer<'_>], + server_name: &pki_types::ServerName<'_>, + ocsp_response: &[u8], + now: pki_types::UnixTime, + ) -> Result { + // Try REALITY cert verification if we have the auth_key + let auth_key: Option<[u8; 32]> = self + .auth_key_slot + .lock() + .ok() + .and_then(|g| *g); + + if let Some(ref key) = auth_key { + if let Some(result) = verify_reality_cert(end_entity, key) { + return result; + } + } + + // Not a REALITY cert — fall back to the inner verifier + self.inner + .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &crate::verify::DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &pki_types::CertificateDer<'_>, + dss: &crate::verify::DigitallySignedStruct, + ) -> Result { + // Try the inner verifier first (handles normal TLS destinations) + if let Ok(valid) = self.inner.verify_tls13_signature(message, cert, dss) { + return Ok(valid); + } + + // Inner verifier failed; try direct Ed25519 verification for REALITY certs + if dss.scheme == crate::enums::SignatureScheme::ED25519 { + if let Some(pubkey) = extract_ed25519_pubkey_from_reality_cert(cert.as_ref()) { + #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] + if ed25519_verify(&pubkey, message, dss.signature()) { + return Ok(crate::verify::HandshakeSignatureValid::assertion()); + } + } + } + + Err(Error::InvalidCertificate( + crate::error::CertificateError::BadSignature, + )) + } + + fn supported_verify_schemes(&self) -> Vec { + let mut schemes = self.inner.supported_verify_schemes(); + if !schemes.contains(&crate::enums::SignatureScheme::ED25519) { + schemes.push(crate::enums::SignatureScheme::ED25519); + } + schemes + } +} + #[cfg(test)] mod tests { use super::*; @@ -592,14 +842,14 @@ mod tests { #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] #[test] - fn test_aes_128_gcm_encryption() { - // Test AES-128-GCM encryption produces 32-byte output (16 bytes ciphertext + 16 bytes tag) - let key = [0u8; 16]; + fn test_aes_256_gcm_encryption() { + // Test AES-256-GCM encryption produces 32-byte output (16 bytes ciphertext + 16 bytes tag) + let key = [0u8; 32]; let nonce = [0u8; 12]; let aad = b"test aad"; let plaintext = [0u8; 16]; - let result = aes_128_gcm_encrypt(&key, &nonce, aad, &plaintext); + let result = aes_256_gcm_encrypt(&key, &nonce, aad, &plaintext); assert!(result.is_ok()); let ciphertext_with_tag = result.unwrap(); assert_eq!(ciphertext_with_tag.len(), 32);