From fff49eb8201da2f704c0dfc8c21e099ff88d7b40 Mon Sep 17 00:00:00 2001 From: chechunchi Date: Wed, 11 Mar 2026 18:47:30 +0800 Subject: [PATCH 1/2] implement vless reality --- Cargo.lock | 1 + REALITY.md | 260 ++++++++++++++ examples/Cargo.toml | 1 + examples/src/bin/reality-client.rs | 363 +++++++++++++++++++ rustls/src/client/builder.rs | 30 ++ rustls/src/client/client_conn.rs | 3 + rustls/src/client/hs.rs | 59 ++- rustls/src/client/reality.rs | 556 +++++++++++++++++++++++++++++ rustls/src/crypto/aws_lc_rs/mod.rs | 40 ++- rustls/src/crypto/mod.rs | 40 +++ rustls/src/crypto/ring/mod.rs | 37 +- rustls/src/lib.rs | 2 + 12 files changed, 1389 insertions(+), 3 deletions(-) create mode 100644 REALITY.md create mode 100644 examples/src/bin/reality-client.rs create mode 100644 rustls/src/client/reality.rs diff --git a/Cargo.lock b/Cargo.lock index 3e237968984..51ea164319e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2485,6 +2485,7 @@ name = "rustls-examples" version = "0.0.1" dependencies = [ "async-std", + "base64", "clap", "env_logger", "hickory-resolver", diff --git a/REALITY.md b/REALITY.md new file mode 100644 index 00000000000..4762db03e93 --- /dev/null +++ b/REALITY.md @@ -0,0 +1,260 @@ +# VLESS Reality Protocol Implementation + +This document describes the implementation of the VLESS Reality protocol in rustls. + +## Overview + +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 implementation allows rustls clients to establish TLS connections with Reality-enabled servers. + +## Protocol Specification + +The Reality protocol follows these steps: + +1. **Key Generation**: Client generates an ephemeral X25519 keypair (`client_secret`, `client_public`) + +2. **ECDH**: Client performs Elliptic Curve Diffie-Hellman with server's public key: + ``` + shared_secret = client_secret.diffie_hellman(server_public_key) + ``` + +3. **Key Derivation**: Client derives authentication key using HKDF-SHA256: + ``` + auth_key = HKDF-SHA256( + ikm=shared_secret, + salt=hello_random[:20], + info="REALITY" + ) + ``` + +4. **Plaintext Construction**: Client constructs 16-byte plaintext: + ``` + [0..3] = client_version (3 bytes) + reserved (1 byte) + [4..8] = Unix timestamp (big-endian u32) + [8..16] = short_id (zero-padded to 8 bytes) + ``` + +5. **Encryption**: Client encrypts plaintext using AES-128-GCM: + ``` + session_id = AES-128-GCM( + key=auth_key, + nonce=hello_random[20..32], + aad=full_ClientHello_bytes, + plaintext=plaintext + ) + ``` + The result is 32 bytes: ciphertext (16 bytes) + authentication tag (16 bytes) + +6. **Key Share Injection**: Client's public key (`client_public`) is injected into the ClientHello `key_share` extension as an X25519 key exchange + +## API Usage + +### Basic Example + +```rust +use std::sync::Arc; +use watfaq_rustls::client::RealityConfig; +use watfaq_rustls::{ClientConfig, RootCertStore}; + +// Server's X25519 public key (obtained securely out-of-band) +let server_pubkey = [0u8; 32]; // Replace with actual key + +// Client identifier +let short_id = vec![0x12, 0x34, 0x56, 0x78]; + +// Create Reality configuration +let reality = RealityConfig::new(server_pubkey, short_id)?; + +// Build client configuration with Reality +let config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_reality(reality) + .with_no_client_auth(); + +// Use normally +let conn = ClientConnection::new(Arc::new(config), server_name)?; +``` + +### Advanced Configuration + +```rust +// Customize client version +let reality = RealityConfig::new(server_pubkey, short_id)? + .with_client_version([1, 2, 3]); +``` + +### Running the Example + +The `reality-client` example demonstrates a complete Reality connection: + +```bash +cd examples +cargo run --bin reality-client -- \ + example.com:443 \ + 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \ + 12345678 +``` + +Arguments: +- `example.com:443` - Server address and port +- Second argument - Server's X25519 public key (64 hex characters = 32 bytes) +- Third argument - Short ID (up to 16 hex characters = 8 bytes) + +## Architecture + +### Module Structure + +``` +rustls/src/client/ +├── reality.rs # Core Reality implementation +├── hs.rs # Handshake integration +├── client_conn.rs # ClientConfig with reality_config field +└── builder.rs # Builder pattern (with_reality method) +``` + +### Key Components + +#### `RealityConfig` (Public API) + +```rust +pub struct RealityConfig { + server_public_key: [u8; 32], // Server's X25519 public key + short_id: Vec, // Client identifier (max 8 bytes) + client_version: [u8; 3], // Protocol version +} +``` + +#### `RealitySessionState` (Internal) + +```rust +pub(crate) struct RealitySessionState { + config: Arc, + client_public: [u8; 32], // Client's ephemeral public key + shared_secret: [u8; 32], // ECDH shared secret +} +``` + +### Handshake Integration + +The Reality protocol is integrated into the TLS handshake at key points: + +1. **Initialization** (`hs.rs:start_handshake`): + - Creates `RealitySessionState` if Reality is configured + - Performs X25519 ECDH to derive shared secret + +2. **Key Share Injection** (`hs.rs:emit_client_hello_for_retry`): + - Replaces normal key_share with Reality X25519 public key + - Ensures ClientHello contains the correct key exchange + +3. **Session ID Computation** (`hs.rs:emit_client_hello_for_retry`): + - After ClientHello is constructed with zero session_id + - Encodes ClientHello to get full bytes + - Computes encrypted session_id using Reality protocol + - Updates ClientHello with computed session_id + +## Cryptographic Implementation + +### Crypto Provider Support + +Reality implementation supports both `ring` and `aws-lc-rs` crypto providers through conditional compilation: + +```rust +#[cfg(feature = "ring")] +fn perform_x25519_ecdh(...) { /* ring implementation */ } + +#[cfg(feature = "aws_lc_rs")] +fn perform_x25519_ecdh(...) { /* aws-lc-rs implementation */ } +``` + +### Cryptographic Operations + +1. **X25519 ECDH**: Uses provider's X25519 implementation +2. **HKDF-SHA256**: Obtained from TLS13_AES_128_GCM_SHA256 cipher suite +3. **AES-128-GCM**: Uses provider's AEAD implementation +4. **Randomness**: Uses provider's secure random generator + +## Security Considerations + +### Requirements + +- **Server Public Key Security**: The server's X25519 public key must be obtained through a secure, authenticated channel. An attacker with the ability to substitute the server public key can break the Reality protocol's privacy guarantees. + +- **Short ID Confidentiality**: The `short_id` serves as a client identifier. Keep it confidential to prevent tracking. + +- **Crypto Provider**: Reality requires X25519 and AES-128-GCM support. Use either `ring` or `aws-lc-rs` features. + +- **TLS Version**: Reality is compatible with both TLS 1.2 and TLS 1.3, but works best with TLS 1.3. + +### Threat Model + +Reality protects against: +- **Passive Observation**: Session IDs are encrypted, preventing observers from correlating connections +- **Active Probing**: Without the correct server private key, attackers cannot decrypt or forge valid session IDs + +Reality does NOT protect against: +- **Compromised Server**: If the server's private key is compromised, past session IDs can be decrypted +- **Traffic Analysis**: Connection timing and size metadata may still leak information + +## Testing + +### Unit Tests + +```bash +cargo test --package watfaq-rustls --lib reality +``` + +Tests include: +- Configuration validation +- X25519 ECDH correctness +- AES-128-GCM encryption +- Session ID plaintext structure +- Full session ID computation pipeline + +### Integration Tests + +Integration tests verify: +- RealitySessionState creation +- Key share entry generation +- Complete session_id computation with both crypto providers +- Builder pattern integration + +## Implementation Notes + +### Design Decisions + +1. **Inline Computation**: Session ID is computed inline during handshake emission rather than using a callback, providing access to `Random` value and full `ClientHello` bytes. + +2. **State Management**: Reality state is passed through the handshake state machine to maintain access to cryptographic materials. + +3. **Provider Abstraction**: Uses rustls's existing crypto provider system for all cryptographic operations. + +4. **No Protocol Breaking Changes**: Reality is implemented as an optional extension without modifying core TLS protocol handling. + +### Limitations + +- **One-Time Use**: The ephemeral X25519 keypair is generated per connection and not reused. + +- **No HelloRetryRequest**: Reality state is not carried through HelloRetryRequest flows (set to `None` on retry). + +- **Fixed Crypto**: Currently requires HKDF-SHA256 (from TLS13_AES_128_GCM_SHA256) and AES-128-GCM. + +## References + +- [VLESS Protocol Specification](https://github.com/XTLS/REALITY) +- [RFC 7748: Elliptic Curves for Security (X25519)](https://datatracker.ietf.org/doc/html/rfc7748) +- [RFC 5869: HKDF-SHA256](https://datatracker.ietf.org/doc/html/rfc5869) +- [RFC 5116: AES-GCM](https://datatracker.ietf.org/doc/html/rfc5116) + +## Changelog + +### Version 0.23.21 + +- Initial implementation of VLESS Reality protocol +- Support for both `ring` and `aws-lc-rs` crypto providers +- Public API: `RealityConfig`, `RealityConfigError` +- Builder method: `ConfigBuilder::with_reality()` +- Example: `reality-client` binary +- Comprehensive unit and integration tests + +## License + +This implementation follows the same license as rustls: Apache-2.0 OR ISC OR MIT. diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 64eb054a6ba..ab503053833 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] async-std = { workspace = true, optional = true } +base64 = { workspace = true } clap = { workspace = true } env_logger = { workspace = true } hickory-resolver = { workspace = true } diff --git a/examples/src/bin/reality-client.rs b/examples/src/bin/reality-client.rs new file mode 100644 index 00000000000..2260b2bf3d1 --- /dev/null +++ b/examples/src/bin/reality-client.rs @@ -0,0 +1,363 @@ +//! 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 +//! ``` +//! +//! Note: You can directly copy-paste the values from airport Reality config! +//! +//! ## Method 2: Parsing airport Reality configuration format +//! +//! Many airports/VPN providers give Reality config in YAML format like: +//! ```yaml +//! reality-opts: +//! public-key: Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs +//! short-id: 1bc2c1ef1c +//! ``` +//! +//! See the `parse_airport_reality_config()` function below for how to parse this format. +//! The public-key is Base64 encoded and short-id is hexadecimal. + +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() != 4 { + eprintln!("Usage: {} ", args[0]); + eprintln!(); + eprintln!("Parameters:"); + eprintln!(" Server address (e.g., example.com:443)"); + eprintln!(" Server's X25519 public key in Base64 format"); + eprintln!(" Client identifier in hexadecimal format"); + eprintln!(); + eprintln!("Example (using airport Reality config):"); + eprintln!(" {} example.com:443 \\", args[0]); + eprintln!(" Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs \\"); + eprintln!(" 1bc2c1ef1c"); + eprintln!(); + eprintln!("If you have airport Reality config in this format:"); + eprintln!(" reality-opts:"); + eprintln!(" public-key: Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"); + eprintln!(" short-id: 1bc2c1ef1c"); + eprintln!(); + eprintln!("Just copy and paste the values directly!"); + std::process::exit(1); + } + + let server_addr = args[1].clone(); + let public_key_base64 = args[2].clone(); + let short_id_hex = args[3].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); + } + + // 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); + + // 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 {}...", &server_addr); + + // Extract server name from address + let server_name: pki_types::ServerName<'static> = server_addr + .split(':') + .next() + .unwrap() + .to_string() + .try_into() + .unwrap_or_else(|e| { + eprintln!("Error parsing server name: {:?}", 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); + + // Complete the handshake first + println!("Performing TLS handshake..."); + tls.conn.complete_io(&mut sock).unwrap_or_else(|e| { + eprintln!("Error during TLS handshake: {}", e); + std::process::exit(1); + }); + + println!("✓ TLS handshake completed successfully with Reality protocol!"); + + // Print negotiated cipher suite + if let Some(ciphersuite) = tls.conn.negotiated_cipher_suite() { + println!("✓ Cipher suite: {:?}", ciphersuite.suite()); + } + + // Print protocol version + if let Some(version) = tls.conn.protocol_version() { + println!("✓ Protocol version: {:?}", version); + } + + // Send a simple HTTP request + println!("\nSending 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", + server_addr.split(':').next().unwrap() + ); + + tls.write_all(request.as_bytes()) + .unwrap_or_else(|e| { + eprintln!("Error sending request: {}", e); + std::process::exit(1); + }); + + 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() % 2 != 0 { + 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("") +} + +/// Example: Parse airport Reality configuration format +/// +/// Many airports/VPN providers give Reality config in this format: +/// ```yaml +/// reality-opts: +/// public-key: Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs +/// short-id: 1bc2c1ef1c +/// ``` +/// +/// This function demonstrates how to parse such configuration into RealityConfig. +/// +/// # Example +/// +/// ```no_run +/// use watfaq_rustls::client::RealityConfig; +/// +/// // Configuration from airport +/// let public_key_base64 = "Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"; +/// let short_id_hex = "1bc2c1ef1c"; +/// +/// // Parse public key from Base64 +/// let public_key_bytes = base64_to_bytes(public_key_base64) +/// .expect("Invalid base64 public key"); +/// let mut public_key = [0u8; 32]; +/// public_key.copy_from_slice(&public_key_bytes); +/// +/// // Parse short_id from hex +/// let short_id = hex_to_bytes(short_id_hex) +/// .expect("Invalid hex short_id"); +/// +/// // Create RealityConfig +/// let reality_config = RealityConfig::new(public_key, short_id) +/// .expect("Invalid Reality configuration"); +/// ``` +#[allow(dead_code)] +fn parse_airport_reality_config( + public_key_base64: &str, + short_id_hex: &str, +) -> Result { + // Parse public key from Base64 + let public_key_bytes = base64_to_bytes(public_key_base64) + .map_err(|e| format!("Failed to decode public key: {}", e))?; + + if public_key_bytes.len() != 32 { + return Err(format!( + "Invalid public key length: expected 32 bytes, got {}", + public_key_bytes.len() + )); + } + + let mut public_key = [0u8; 32]; + public_key.copy_from_slice(&public_key_bytes); + + // Parse short_id from hex + let short_id = hex_to_bytes(short_id_hex) + .map_err(|e| format!("Failed to decode short_id: {}", e))?; + + if short_id.len() > 8 { + return Err(format!( + "short_id too long: expected max 8 bytes, got {}", + short_id.len() + )); + } + + // Create RealityConfig + RealityConfig::new(public_key, short_id) + .map_err(|e| format!("Failed to create RealityConfig: {}", e)) +} + +/// Helper function to decode Base64 string to bytes +/// +/// Supports both standard Base64 and Base64 URL-safe encoding. +#[allow(dead_code)] +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)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_airport_reality_config() { + // Example configuration from an airport + let public_key_base64 = "Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"; + let short_id_hex = "1bc2c1ef1c"; + + let config = parse_airport_reality_config(public_key_base64, short_id_hex); + assert!(config.is_ok(), "Should parse valid airport config"); + + let config = config.unwrap(); + // Verify we can use it (this just checks it was created successfully) + drop(config); + } + + #[test] + fn test_base64_to_bytes() { + // Standard Base64 + let result = base64_to_bytes("SGVsbG8gV29ybGQ="); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), b"Hello World"); + + // URL-safe Base64 + let result = base64_to_bytes("Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 32); // X25519 public key is 32 bytes + } + + #[test] + fn test_hex_to_bytes() { + let result = hex_to_bytes("1bc2c1ef1c"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec![0x1b, 0xc2, 0xc1, 0xef, 0x1c]); + } +} diff --git a/rustls/src/client/builder.rs b/rustls/src/client/builder.rs index bb2a7d66ecc..9506e885efb 100644 --- a/rustls/src/client/builder.rs +++ b/rustls/src/client/builder.rs @@ -73,6 +73,7 @@ impl ConfigBuilder { versions: self.state.versions, verifier, client_ech_mode: self.state.client_ech_mode, + reality_config: None, }, provider: self.provider, time_provider: self.time_provider, @@ -113,6 +114,7 @@ pub(super) mod danger { versions: self.cfg.state.versions, verifier, client_ech_mode: self.cfg.state.client_ech_mode, + reality_config: None, }, provider: self.cfg.provider, time_provider: self.cfg.time_provider, @@ -131,9 +133,36 @@ pub struct WantsClientCert { versions: versions::EnabledVersions, verifier: Arc, client_ech_mode: Option, + reality_config: Option>, } impl ConfigBuilder { + /// Enable 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. + /// + /// # Example + /// + /// ```no_run + /// use watfaq_rustls::client::RealityConfig; + /// # use watfaq_rustls::ClientConfig; + /// # let root_store = watfaq_rustls::RootCertStore::empty(); + /// + /// let server_pubkey = [0u8; 32]; // Server's X25519 public key + /// let short_id = vec![0x12, 0x34, 0x56, 0x78]; + /// let reality = RealityConfig::new(server_pubkey, short_id).unwrap(); + /// + /// let config = ClientConfig::builder() + /// .with_root_certificates(root_store) + /// .with_reality(reality) + /// .with_no_client_auth(); + /// ``` + pub fn with_reality(mut self, config: crate::client::reality::RealityConfig) -> Self { + self.state.reality_config = Some(Arc::new(config)); + self + } + /// Sets a single certificate chain and matching private key for use /// in client authentication. /// @@ -186,6 +215,7 @@ impl ConfigBuilder { cert_compression_cache: Arc::new(compress::CompressionCache::default()), cert_decompressors: compress::default_cert_decompressors().to_vec(), ech_mode: self.state.client_ech_mode, + reality_config: self.state.reality_config, } } } diff --git a/rustls/src/client/client_conn.rs b/rustls/src/client/client_conn.rs index 788e580ba6b..852cf342a20 100644 --- a/rustls/src/client/client_conn.rs +++ b/rustls/src/client/client_conn.rs @@ -264,6 +264,9 @@ pub struct ClientConfig { /// How to offer Encrypted Client Hello (ECH). The default is to not offer ECH. pub(super) ech_mode: Option, + + /// VLESS Reality protocol configuration. The default is None (disabled). + pub(super) reality_config: Option>, } impl ClientConfig { diff --git a/rustls/src/client/hs.rs b/rustls/src/client/hs.rs index c7578f6da82..60bd9896487 100644 --- a/rustls/src/client/hs.rs +++ b/rustls/src/client/hs.rs @@ -10,6 +10,7 @@ use pki_types::ServerName; #[cfg(feature = "tls12")] use super::tls12; use super::Tls12Resumption; +use super::reality; #[cfg(feature = "logging")] use crate::bs_debug; use crate::check::inappropriate_handshake_message; @@ -184,6 +185,15 @@ where _ => None, }; + // Initialize Reality state if configured + let reality_state = config + .reality_config + .as_ref() + .map(|reality_config| { + reality::RealitySessionState::new(Arc::clone(reality_config), &config.provider) + }) + .transpose()?; + emit_client_hello_for_retry( transcript_buffer, None, @@ -205,6 +215,7 @@ where }, cx, ech_state, + reality_state, ) } @@ -245,6 +256,7 @@ fn emit_client_hello_for_retry( mut input: ClientHelloInput, cx: &mut ClientContext<'_>, mut ech_state: Option, + reality_state: Option, ) -> NextStateOrError<'static> where T: Fn(&[u8]) -> [u8; 32], @@ -330,7 +342,11 @@ where (None, false) => {} }; - if let Some(key_share) = &key_share { + // Add key_share extension + // Reality overrides the normal key_share with its own X25519 key + if let Some(ref reality) = reality_state { + exts.push(ClientExtension::KeyShare(vec![reality.key_share_entry()])); + } else if let Some(key_share) = &key_share { debug_assert!(support_tls13); let mut shares = vec![KeyShareEntry::new(key_share.group(), key_share.pub_key())]; @@ -560,6 +576,46 @@ where } } + // Compute Reality session_id if Reality is enabled + if let Some(ref reality) = reality_state { + // Step 1: Set session_id to zero temporarily + let mut buffer = Vec::new(); + match &mut chp.payload { + HandshakePayload::ClientHello(c) => { + c.session_id = SessionId { + len: 32, + data: [0; 32], + }; + } + _ => unreachable!(), + } + + // Step 2: Encode ClientHello with zero session_id + chp.encode(&mut buffer); + + // Step 3: Get HKDF-SHA256 provider + let hkdf = reality::get_hkdf_sha256_from_config(&config.provider.cipher_suites)?; + + // Step 4: Compute Reality session_id + let session_id_data = reality.compute_session_id( + &input.random, + &buffer, + hkdf, + config.time_provider.as_ref(), + )?; + + // Step 5: Update session_id + match &mut chp.payload { + HandshakePayload::ClientHello(c) => { + c.session_id = SessionId { + len: 32, + data: session_id_data, + }; + } + _ => unreachable!(), + } + } + let ch = Message { version: match retryreq { // : @@ -1190,6 +1246,7 @@ impl ExpectServerHelloOrHelloRetryRequest { self.next.input, cx, self.next.ech_state, + None, // Reality state not used in retry ) } } diff --git a/rustls/src/client/reality.rs b/rustls/src/client/reality.rs new file mode 100644 index 00000000000..b7078079a3a --- /dev/null +++ b/rustls/src/client/reality.rs @@ -0,0 +1,556 @@ +//! VLESS Reality protocol implementation +//! +//! 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. +//! +//! # Protocol Overview +//! +//! The Reality protocol works as follows: +//! 1. Client generates ephemeral X25519 keypair +//! 2. Client performs ECDH with server's public key to get shared_secret +//! 3. Client derives auth_key using HKDF-SHA256(shared_secret, hello_random[:20], "REALITY") +//! 4. Client constructs 16-byte plaintext: [version(3) | reserved(1) | timestamp(4) | short_id(8)] +//! 5. Client encrypts plaintext using AES-128-GCM with: +//! - key: auth_key +//! - nonce: hello_random[20..32] +//! - aad: full ClientHello bytes +//! 6. Result (ciphertext + tag = 32 bytes) becomes the session_id +//! 7. Client's public key is injected into the key_share extension + +use alloc::sync::Arc; +use alloc::vec::Vec; + +use crate::crypto::CryptoProvider; +use crate::crypto::tls13::Hkdf; +use crate::error::Error; +use crate::msgs::enums::NamedGroup; +use crate::msgs::handshake::{KeyShareEntry, Random}; +use crate::enums::CipherSuite; +use crate::SupportedCipherSuite; + +/// VLESS Reality protocol configuration +/// +/// This configuration specifies the parameters needed for the Reality protocol, +/// including the server's public key and client identification. +/// +/// # Example +/// +/// ```no_run +/// use watfaq_rustls::client::RealityConfig; +/// +/// let server_pubkey = [0u8; 32]; // Server's X25519 public key +/// let short_id = vec![0x12, 0x34, 0x56, 0x78]; +/// +/// let config = RealityConfig::new(server_pubkey, short_id) +/// .expect("valid configuration"); +/// ``` +/// +/// # Security Considerations +/// +/// - The server public key must be obtained through a secure channel +/// - The short_id serves as a client identifier; keep it confidential +/// - Reality requires X25519 key exchange support in the crypto provider +#[derive(Clone, Debug)] +pub struct RealityConfig { + /// Server's X25519 public key (32 bytes) + server_public_key: [u8; 32], + /// Client identifier (max 8 bytes, zero-padded in protocol) + short_id: Vec, + /// Protocol version (3 bytes, default [0, 0, 0]) + client_version: [u8; 3], +} + +impl RealityConfig { + /// Create a new Reality configuration + /// + /// # Parameters + /// + /// - `server_public_key`: The server's X25519 public key (32 bytes) + /// - `short_id`: Client identifier, must be at most 8 bytes + /// + /// # Errors + /// + /// Returns `RealityConfigError::ShortIdTooLong` if `short_id` exceeds 8 bytes. + /// + /// # Example + /// + /// ```no_run + /// use watfaq_rustls::client::RealityConfig; + /// + /// let server_pk = [0u8; 32]; + /// let short_id = vec![0x12, 0x34, 0x56, 0x78]; + /// let config = RealityConfig::new(server_pk, short_id).unwrap(); + /// ``` + pub fn new(server_public_key: [u8; 32], short_id: Vec) -> Result { + if short_id.len() > 8 { + return Err(RealityConfigError::ShortIdTooLong); + } + Ok(Self { + server_public_key, + short_id, + client_version: [0, 0, 0], + }) + } + + /// Set the client version field + /// + /// The client version is a 3-byte field in the Reality protocol. + /// Default is `[0, 0, 0]`. + pub fn with_client_version(mut self, version: [u8; 3]) -> Self { + self.client_version = version; + self + } +} + +/// Errors that can occur when creating a Reality configuration +#[derive(Debug)] +pub enum RealityConfigError { + /// The short_id exceeds the maximum length of 8 bytes + ShortIdTooLong, + /// A cryptographic operation failed + CryptoError(alloc::string::String), +} + +impl core::fmt::Display for RealityConfigError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RealityConfigError::ShortIdTooLong => { + write!(f, "Reality short_id must be at most 8 bytes") + } + RealityConfigError::CryptoError(msg) => { + write!(f, "Reality crypto error: {}", msg) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RealityConfigError {} + +/// Internal state for Reality protocol during TLS handshake +/// +/// This struct holds the ephemeral keys and shared secret needed to compute +/// the Reality session_id. +pub(crate) struct RealitySessionState { + config: Arc, + /// Client's ephemeral X25519 public key (32 bytes) + client_public: [u8; 32], + /// ECDH shared secret with server (32 bytes) + shared_secret: [u8; 32], +} + +impl RealitySessionState { + /// Initialize Reality state by performing X25519 ECDH + /// + /// This generates an ephemeral X25519 keypair and performs ECDH with + /// the server's public key to derive the shared secret. + pub(crate) fn new( + config: Arc, + crypto_provider: &CryptoProvider, + ) -> Result { + // Perform X25519 ECDH using the unified provider interface + let (client_public, shared_secret) = crypto_provider + .x25519_provider + .x25519_ecdh(&config.server_public_key)?; + + Ok(Self { + config, + client_public, + shared_secret, + }) + } + + /// Generate KeyShareEntry for ClientHello key_share extension + /// + /// Returns a key_share entry containing the client's ephemeral X25519 public key. + pub(crate) fn key_share_entry(&self) -> KeyShareEntry { + KeyShareEntry::new(NamedGroup::X25519, self.client_public.to_vec()) + } + + /// Compute Reality session_id using the full protocol + /// + /// # Parameters + /// + /// - `random`: The ClientHello random value (32 bytes) + /// - `hello_bytes`: The full encoded ClientHello message + /// - `hkdf`: HKDF-SHA256 provider + /// + /// # Returns + /// + /// 32 bytes: ciphertext (16 bytes) + authentication tag (16 bytes) + pub(crate) fn compute_session_id( + &self, + random: &Random, + hello_bytes: &[u8], + hkdf: &dyn Hkdf, + time_provider: &dyn crate::time_provider::TimeProvider, + ) -> Result<[u8; 32], Error> { + // Step 1: Derive auth_key using HKDF-SHA256 + // auth_key = HKDF(shared_secret, salt=hello_random[:20], info="REALITY") + let salt = &random.0[..20]; + let auth_key_expander = hkdf.extract_from_secret(Some(salt), &self.shared_secret); + + let mut auth_key = [0u8; 16]; + auth_key_expander + .expand_slice(&[b"REALITY"], &mut auth_key) + .map_err(|_| Error::General("HKDF expand failed".into()))?; + + // Step 2: Construct plaintext (16 bytes) + let mut plaintext = [0u8; 16]; + + // Bytes [0..3]: client_version (3 bytes) + reserved (1 byte) + plaintext[0..3].copy_from_slice(&self.config.client_version); + plaintext[3] = 0; // reserved + + // Bytes [4..8]: Unix timestamp (big-endian u32) + let timestamp = current_timestamp(time_provider)?; + plaintext[4..8].copy_from_slice(×tamp.to_be_bytes()); + + // Bytes [8..16]: short_id (zero-padded to 8 bytes) + let short_id_len = self.config.short_id.len(); + plaintext[8..8 + short_id_len].copy_from_slice(&self.config.short_id); + // Remaining bytes are already zero + + // Step 3: AES-128-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()))?; + + aes_128_gcm_encrypt(&auth_key, nonce, hello_bytes, &plaintext) + } +} + +// 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 +/// +/// Encrypts 16-byte plaintext and returns ciphertext + tag (32 bytes total). +fn aes_128_gcm_encrypt( + key: &[u8; 16], + nonce: &[u8; 12], + aad: &[u8], + plaintext: &[u8; 16], +) -> Result<[u8; 32], Error> { + #[cfg(feature = "ring")] + { + aes_128_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) + } + + #[cfg(not(any(feature = "ring", feature = "aws_lc_rs")))] + { + Err(Error::General( + "Reality requires either 'ring' or 'aws_lc_rs' feature".into(), + )) + } +} + +/// AES-128-GCM encryption using ring +#[cfg(feature = "ring")] +fn aes_128_gcm_encrypt_ring( + key: &[u8; 16], + 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 sealing_key = aead::LessSafeKey::new(unbound_key); + + let mut in_out = plaintext.to_vec(); + let nonce = aead::Nonce::assume_unique_for_key(*nonce); + let aad = aead::Aad::from(aad); + + sealing_key + .seal_in_place_append_tag(nonce, aad, &mut in_out) + .map_err(|_| Error::General("AES-128-GCM encryption failed".into()))?; + + // in_out now contains: plaintext (16 bytes) + tag (16 bytes) = 32 bytes + let mut result = [0u8; 32]; + result.copy_from_slice(&in_out); + Ok(result) +} + +/// AES-128-GCM encryption using aws-lc-rs +#[cfg(feature = "aws_lc_rs")] +fn aes_128_gcm_encrypt_aws_lc_rs( + key: &[u8; 16], + 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 sealing_key = aead::LessSafeKey::new(unbound_key); + + let mut in_out = plaintext.to_vec(); + let nonce = aead::Nonce::assume_unique_for_key(*nonce); + let aad = aead::Aad::from(aad); + + sealing_key + .seal_in_place_append_tag(nonce, aad, &mut in_out) + .map_err(|_| Error::General("AES-128-GCM encryption failed".into()))?; + + let mut result = [0u8; 32]; + result.copy_from_slice(&in_out); + Ok(result) +} + +/// Get current Unix timestamp as u32 +fn current_timestamp(time_provider: &dyn crate::time_provider::TimeProvider) -> Result { + let now = time_provider + .current_time() + .ok_or_else(|| Error::General("Time unavailable".into()))?; + Ok((now.as_secs() % (1u64 << 32)) as u32) +} + +/// Get HKDF-SHA256 provider from ClientConfig +pub(crate) fn get_hkdf_sha256_from_config( + cipher_suites: &[SupportedCipherSuite], +) -> Result<&'static dyn Hkdf, Error> { + cipher_suites + .iter() + .find_map(|suite| { + if let SupportedCipherSuite::Tls13(tls13) = suite { + if tls13.common.suite == CipherSuite::TLS13_AES_128_GCM_SHA256 { + return Some(tls13.hkdf_provider); + } + } + None + }) + .ok_or_else(|| Error::General("No SHA256 HKDF available for Reality".into())) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_reality_config_creation() { + let server_pk = [1u8; 32]; + let short_id = vec![0x12, 0x34]; + let config = RealityConfig::new(server_pk, short_id).unwrap(); + assert_eq!(config.short_id.len(), 2); + assert_eq!(config.client_version, [0, 0, 0]); + } + + #[test] + fn test_short_id_too_long() { + let server_pk = [1u8; 32]; + let short_id = vec![0u8; 9]; // Too long + assert!(matches!( + RealityConfig::new(server_pk, short_id), + Err(RealityConfigError::ShortIdTooLong) + )); + } + + #[test] + fn test_with_client_version() { + let server_pk = [1u8; 32]; + let short_id = vec![0x12]; + let config = RealityConfig::new(server_pk, short_id) + .unwrap() + .with_client_version([1, 2, 3]); + assert_eq!(config.client_version, [1, 2, 3]); + } + + #[test] + fn test_short_id_max_length() { + let server_pk = [1u8; 32]; + let short_id = vec![0u8; 8]; // Exactly 8 bytes - should work + assert!(RealityConfig::new(server_pk, short_id).is_ok()); + } + + #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] + #[test] + fn test_x25519_ecdh() { + // Install provider + #[cfg(feature = "ring")] + let _ = crate::crypto::ring::default_provider().install_default(); + #[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] + let _ = crate::crypto::aws_lc_rs::default_provider().install_default(); + + let provider = crate::crypto::CryptoProvider::get_default().unwrap(); + + // Test that ECDH produces 32-byte outputs + let server_public = [2u8; 32]; + let result = provider.x25519_provider.x25519_ecdh(&server_public); + assert!(result.is_ok()); + let (client_public, shared_secret) = result.unwrap(); + assert_eq!(client_public.len(), 32); + assert_eq!(shared_secret.len(), 32); + } + + #[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]; + let nonce = [0u8; 12]; + let aad = b"test aad"; + let plaintext = [0u8; 16]; + + let result = aes_128_gcm_encrypt(&key, &nonce, aad, &plaintext); + assert!(result.is_ok()); + let ciphertext_with_tag = result.unwrap(); + assert_eq!(ciphertext_with_tag.len(), 32); + } + + #[test] + fn test_session_id_plaintext_structure() { + // Test that plaintext is constructed correctly + let server_pk = [1u8; 32]; + let short_id = vec![0x12, 0x34, 0x56, 0x78]; + let config = RealityConfig::new(server_pk, short_id) + .unwrap() + .with_client_version([1, 2, 3]); + + // Verify config structure + assert_eq!(config.client_version, [1, 2, 3]); + assert_eq!(config.short_id.len(), 4); + + // Verify plaintext would have correct layout: + // [0..3]: client_version + reserved + // [4..8]: timestamp + // [8..16]: short_id (zero-padded) + let mut plaintext = [0u8; 16]; + plaintext[0..3].copy_from_slice(&config.client_version); + plaintext[3] = 0; // reserved + // Skip timestamp for this test + plaintext[8..12].copy_from_slice(&config.short_id); + // Rest should be zeros (padding) + + assert_eq!(plaintext[0], 1); + assert_eq!(plaintext[1], 2); + assert_eq!(plaintext[2], 3); + assert_eq!(plaintext[3], 0); + assert_eq!(plaintext[8], 0x12); + assert_eq!(plaintext[9], 0x34); + assert_eq!(plaintext[10], 0x56); + assert_eq!(plaintext[11], 0x78); + assert_eq!(plaintext[12], 0); + assert_eq!(plaintext[15], 0); + } + + #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] + #[test] + fn test_reality_session_state_creation() { + use crate::crypto::CryptoProvider; + + // Install default provider + #[cfg(feature = "ring")] + let _ = crate::crypto::ring::default_provider().install_default(); + #[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] + let _ = crate::crypto::aws_lc_rs::default_provider().install_default(); + + let server_pk = [1u8; 32]; + let short_id = vec![0x12, 0x34]; + let config = Arc::new(RealityConfig::new(server_pk, short_id).unwrap()); + + // Get the default crypto provider + let provider = CryptoProvider::get_default() + .expect("No default crypto provider installed"); + + let state = RealitySessionState::new(config, &provider); + assert!(state.is_ok()); + + let state = state.unwrap(); + assert_eq!(state.client_public.len(), 32); + assert_eq!(state.shared_secret.len(), 32); + } + + #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] + #[test] + fn test_key_share_entry_generation() { + use crate::crypto::CryptoProvider; + use crate::msgs::enums::NamedGroup; + + // Install default provider + #[cfg(feature = "ring")] + let _ = crate::crypto::ring::default_provider().install_default(); + #[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] + let _ = crate::crypto::aws_lc_rs::default_provider().install_default(); + + let server_pk = [1u8; 32]; + let short_id = vec![0x12, 0x34]; + let config = Arc::new(RealityConfig::new(server_pk, short_id).unwrap()); + + let provider = CryptoProvider::get_default() + .expect("No default crypto provider installed"); + + let state = RealitySessionState::new(config, &provider).unwrap(); + let key_share = state.key_share_entry(); + + // Verify key_share uses X25519 + assert_eq!(key_share.group, NamedGroup::X25519); + // Verify key_share payload is 32 bytes (X25519 public key) + assert_eq!(key_share.payload.0.len(), 32); + } + + #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] + #[test] + fn test_compute_session_id_output_length() { + use crate::crypto::CryptoProvider; + use crate::msgs::handshake::Random; + use crate::time_provider::TimeProvider; + + #[derive(Debug)] + struct MockTimeProvider; + impl TimeProvider for MockTimeProvider { + fn current_time(&self) -> Option { + Some(pki_types::UnixTime::since_unix_epoch( + core::time::Duration::from_secs(1234567890), + )) + } + } + + // Install default provider + #[cfg(feature = "ring")] + let _ = crate::crypto::ring::default_provider().install_default(); + #[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] + let _ = crate::crypto::aws_lc_rs::default_provider().install_default(); + + let server_pk = [1u8; 32]; + let short_id = vec![0x12, 0x34, 0x56, 0x78]; + let config = Arc::new(RealityConfig::new(server_pk, short_id).unwrap()); + + let provider = CryptoProvider::get_default() + .expect("No default crypto provider installed"); + + let state = RealitySessionState::new(config.clone(), &provider).unwrap(); + + // Create a mock random value + let random = Random([0u8; 32]); + + // Create mock ClientHello bytes + let hello_bytes = vec![0u8; 100]; + + // Get HKDF provider + let hkdf = get_hkdf_sha256_from_config(&provider.cipher_suites); + assert!(hkdf.is_ok()); + let hkdf = hkdf.unwrap(); + + let time_provider = MockTimeProvider; + + // Compute session_id + let session_id = state.compute_session_id(&random, &hello_bytes, hkdf, &time_provider); + assert!(session_id.is_ok()); + + let session_id = session_id.unwrap(); + // Verify session_id is exactly 32 bytes (16 bytes ciphertext + 16 bytes tag) + assert_eq!(session_id.len(), 32); + } +} diff --git a/rustls/src/crypto/aws_lc_rs/mod.rs b/rustls/src/crypto/aws_lc_rs/mod.rs index c31874aa92d..02d9b489fb0 100644 --- a/rustls/src/crypto/aws_lc_rs/mod.rs +++ b/rustls/src/crypto/aws_lc_rs/mod.rs @@ -9,7 +9,7 @@ pub(crate) use aws_lc_rs as ring_like; use pki_types::PrivateKeyDer; use webpki::aws_lc_rs as webpki_algs; -use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom}; +use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom, X25519Provider}; use crate::enums::SignatureScheme; use crate::rand::GetRandomFailed; use crate::sign::SigningKey; @@ -44,6 +44,7 @@ pub fn default_provider() -> CryptoProvider { signature_verification_algorithms: SUPPORTED_SIG_ALGS, secure_random: &AwsLcRs, key_provider: &AwsLcRs, + x25519_provider: &AwsLcRs, } } @@ -92,6 +93,43 @@ impl KeyProvider for AwsLcRs { } } +impl X25519Provider for AwsLcRs { + fn x25519_ecdh(&self, peer_public_key: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error> { + use ring_like::agreement; + + let rng = ring_like::rand::SystemRandom::new(); + + // Generate ephemeral X25519 private key + let private_key = agreement::EphemeralPrivateKey::generate(&agreement::X25519, &rng) + .map_err(unspecified_err)?; + + // Compute public key + let public_key = private_key + .compute_public_key() + .map_err(unspecified_err)?; + + let mut client_public = [0u8; 32]; + client_public.copy_from_slice(public_key.as_ref()); + + // Perform ECDH + let peer_public = + agreement::UnparsedPublicKey::new(&agreement::X25519, peer_public_key); + + let mut shared_secret = [0u8; 32]; + agreement::agree_ephemeral(private_key, &peer_public, (), |key_material| { + shared_secret.copy_from_slice(key_material); + Ok(()) + }) + .map_err(|_| Error::General("X25519 ECDH failed".into()))?; + + Ok((client_public, shared_secret)) + } + + fn fips(&self) -> bool { + fips() + } +} + /// The cipher suite configuration that an application should use by default. /// /// This will be [`ALL_CIPHER_SUITES`] sans any supported cipher suites that diff --git a/rustls/src/crypto/mod.rs b/rustls/src/crypto/mod.rs index 3a39f2d8b6b..0b1d1befab1 100644 --- a/rustls/src/crypto/mod.rs +++ b/rustls/src/crypto/mod.rs @@ -214,6 +214,12 @@ pub struct CryptoProvider { /// Provider for loading private [SigningKey]s from [PrivateKeyDer]. pub key_provider: &'static dyn KeyProvider, + + /// Provider for X25519 key exchange operations. + /// + /// This is used by protocols that need direct X25519 ECDH functionality, + /// such as VLESS Reality protocol. + pub x25519_provider: &'static dyn X25519Provider, } impl CryptoProvider { @@ -294,12 +300,14 @@ impl CryptoProvider { signature_verification_algorithms, secure_random, key_provider, + x25519_provider, } = self; cipher_suites.iter().all(|cs| cs.fips()) && kx_groups.iter().all(|kx| kx.fips()) && signature_verification_algorithms.fips() && secure_random.fips() && key_provider.fips() + && x25519_provider.fips() } } @@ -323,6 +331,38 @@ pub trait SecureRandom: Send + Sync + Debug { } } +/// A provider for X25519 key exchange operations. +/// +/// This trait provides a unified interface for performing X25519 ECDH +/// key exchange, abstracting over different cryptographic backends. +pub trait X25519Provider: Send + Sync + Debug { + /// Perform X25519 ECDH with the given peer public key. + /// + /// This method: + /// 1. Generates an ephemeral X25519 keypair + /// 2. Computes the ECDH shared secret with `peer_public_key` + /// 3. Returns both the client's public key and the shared secret + /// + /// # Arguments + /// * `peer_public_key` - The peer's X25519 public key (32 bytes) + /// + /// # Returns + /// * `Ok((client_public, shared_secret))` on success, where: + /// - `client_public`: The generated ephemeral public key (32 bytes) + /// - `shared_secret`: The ECDH shared secret (32 bytes) + /// * `Err(Error)` if key generation or ECDH fails + /// + /// # Security + /// The ephemeral private key is generated using a cryptographically + /// secure random number generator and is securely erased after use. + fn x25519_ecdh(&self, peer_public_key: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error>; + + /// Return `true` if this is backed by a FIPS-approved implementation. + fn fips(&self) -> bool { + false + } +} + /// A mechanism for loading private [SigningKey]s from [PrivateKeyDer]. /// /// This trait is intended to be used with private key material that is sourced from DER, diff --git a/rustls/src/crypto/ring/mod.rs b/rustls/src/crypto/ring/mod.rs index 8f8ffc0053d..eeb7cf59c03 100644 --- a/rustls/src/crypto/ring/mod.rs +++ b/rustls/src/crypto/ring/mod.rs @@ -4,7 +4,7 @@ use pki_types::PrivateKeyDer; pub(crate) use ring as ring_like; use webpki::ring as webpki_algs; -use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom}; +use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom, X25519Provider}; use crate::enums::SignatureScheme; use crate::rand::GetRandomFailed; use crate::sign::SigningKey; @@ -36,6 +36,7 @@ pub fn default_provider() -> CryptoProvider { signature_verification_algorithms: SUPPORTED_SIG_ALGS, secure_random: &Ring, key_provider: &Ring, + x25519_provider: &Ring, } } @@ -62,6 +63,40 @@ impl KeyProvider for Ring { } } +impl X25519Provider for Ring { + fn x25519_ecdh(&self, peer_public_key: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error> { + use ring_like::agreement; + use ring_like::rand::SecureRandom; + + let rng = ring_like::rand::SystemRandom::new(); + + // Generate ephemeral X25519 private key + let private_key = agreement::EphemeralPrivateKey::generate(&agreement::X25519, &rng) + .map_err(|_| Error::General("X25519 key generation failed".into()))?; + + // Compute public key + let public_key = private_key + .compute_public_key() + .map_err(|_| Error::General("X25519 public key computation failed".into()))?; + + let mut client_public = [0u8; 32]; + client_public.copy_from_slice(public_key.as_ref()); + + // Perform ECDH + let peer_public = + agreement::UnparsedPublicKey::new(&agreement::X25519, peer_public_key); + + let mut shared_secret = [0u8; 32]; + agreement::agree_ephemeral(private_key, &peer_public, |key_material| { + shared_secret.copy_from_slice(key_material); + Ok(()) + }) + .map_err(|_| Error::General("X25519 ECDH failed".into()))?; + + Ok((client_public, shared_secret)) + } +} + /// The cipher suite configuration that an application should use by default. /// /// This will be [`ALL_CIPHER_SUITES`] sans any supported cipher suites that diff --git a/rustls/src/lib.rs b/rustls/src/lib.rs index 81b591065a1..a66bb56ed10 100644 --- a/rustls/src/lib.rs +++ b/rustls/src/lib.rs @@ -563,6 +563,7 @@ pub mod client { mod ech; pub(super) mod handy; mod hs; + pub mod reality; #[cfg(feature = "tls12")] mod tls12; mod tls13; @@ -576,6 +577,7 @@ pub mod client { pub use client_conn::{ClientConnection, WriteEarlyData}; pub use ech::{EchConfig, EchGreaseConfig, EchMode, EchStatus}; pub use handy::AlwaysResolvesClientRawPublicKeys; + pub use reality::{RealityConfig, RealityConfigError}; #[cfg(any(feature = "std", feature = "hashbrown"))] pub use handy::ClientSessionMemoryCache; From 0e01771a894ab0c4c943302c077bd2c9f5796da8 Mon Sep 17 00:00:00 2001 From: chechunchi Date: Thu, 12 Mar 2026 16:54:52 +0800 Subject: [PATCH 2/2] fix reality key exchange --- REALITY.md | 260 ----------------------------- examples/src/bin/reality-client.rs | 219 ++++++------------------ rustls/src/client/hs.rs | 48 ++++-- rustls/src/client/reality.rs | 247 ++++++++++++++++++++++++--- rustls/src/crypto/aws_lc_rs/mod.rs | 40 +---- rustls/src/crypto/mod.rs | 40 ----- rustls/src/crypto/ring/mod.rs | 37 +--- 7 files changed, 307 insertions(+), 584 deletions(-) delete mode 100644 REALITY.md diff --git a/REALITY.md b/REALITY.md deleted file mode 100644 index 4762db03e93..00000000000 --- a/REALITY.md +++ /dev/null @@ -1,260 +0,0 @@ -# VLESS Reality Protocol Implementation - -This document describes the implementation of the VLESS Reality protocol in rustls. - -## Overview - -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 implementation allows rustls clients to establish TLS connections with Reality-enabled servers. - -## Protocol Specification - -The Reality protocol follows these steps: - -1. **Key Generation**: Client generates an ephemeral X25519 keypair (`client_secret`, `client_public`) - -2. **ECDH**: Client performs Elliptic Curve Diffie-Hellman with server's public key: - ``` - shared_secret = client_secret.diffie_hellman(server_public_key) - ``` - -3. **Key Derivation**: Client derives authentication key using HKDF-SHA256: - ``` - auth_key = HKDF-SHA256( - ikm=shared_secret, - salt=hello_random[:20], - info="REALITY" - ) - ``` - -4. **Plaintext Construction**: Client constructs 16-byte plaintext: - ``` - [0..3] = client_version (3 bytes) + reserved (1 byte) - [4..8] = Unix timestamp (big-endian u32) - [8..16] = short_id (zero-padded to 8 bytes) - ``` - -5. **Encryption**: Client encrypts plaintext using AES-128-GCM: - ``` - session_id = AES-128-GCM( - key=auth_key, - nonce=hello_random[20..32], - aad=full_ClientHello_bytes, - plaintext=plaintext - ) - ``` - The result is 32 bytes: ciphertext (16 bytes) + authentication tag (16 bytes) - -6. **Key Share Injection**: Client's public key (`client_public`) is injected into the ClientHello `key_share` extension as an X25519 key exchange - -## API Usage - -### Basic Example - -```rust -use std::sync::Arc; -use watfaq_rustls::client::RealityConfig; -use watfaq_rustls::{ClientConfig, RootCertStore}; - -// Server's X25519 public key (obtained securely out-of-band) -let server_pubkey = [0u8; 32]; // Replace with actual key - -// Client identifier -let short_id = vec![0x12, 0x34, 0x56, 0x78]; - -// Create Reality configuration -let reality = RealityConfig::new(server_pubkey, short_id)?; - -// Build client configuration with Reality -let config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_reality(reality) - .with_no_client_auth(); - -// Use normally -let conn = ClientConnection::new(Arc::new(config), server_name)?; -``` - -### Advanced Configuration - -```rust -// Customize client version -let reality = RealityConfig::new(server_pubkey, short_id)? - .with_client_version([1, 2, 3]); -``` - -### Running the Example - -The `reality-client` example demonstrates a complete Reality connection: - -```bash -cd examples -cargo run --bin reality-client -- \ - example.com:443 \ - 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \ - 12345678 -``` - -Arguments: -- `example.com:443` - Server address and port -- Second argument - Server's X25519 public key (64 hex characters = 32 bytes) -- Third argument - Short ID (up to 16 hex characters = 8 bytes) - -## Architecture - -### Module Structure - -``` -rustls/src/client/ -├── reality.rs # Core Reality implementation -├── hs.rs # Handshake integration -├── client_conn.rs # ClientConfig with reality_config field -└── builder.rs # Builder pattern (with_reality method) -``` - -### Key Components - -#### `RealityConfig` (Public API) - -```rust -pub struct RealityConfig { - server_public_key: [u8; 32], // Server's X25519 public key - short_id: Vec, // Client identifier (max 8 bytes) - client_version: [u8; 3], // Protocol version -} -``` - -#### `RealitySessionState` (Internal) - -```rust -pub(crate) struct RealitySessionState { - config: Arc, - client_public: [u8; 32], // Client's ephemeral public key - shared_secret: [u8; 32], // ECDH shared secret -} -``` - -### Handshake Integration - -The Reality protocol is integrated into the TLS handshake at key points: - -1. **Initialization** (`hs.rs:start_handshake`): - - Creates `RealitySessionState` if Reality is configured - - Performs X25519 ECDH to derive shared secret - -2. **Key Share Injection** (`hs.rs:emit_client_hello_for_retry`): - - Replaces normal key_share with Reality X25519 public key - - Ensures ClientHello contains the correct key exchange - -3. **Session ID Computation** (`hs.rs:emit_client_hello_for_retry`): - - After ClientHello is constructed with zero session_id - - Encodes ClientHello to get full bytes - - Computes encrypted session_id using Reality protocol - - Updates ClientHello with computed session_id - -## Cryptographic Implementation - -### Crypto Provider Support - -Reality implementation supports both `ring` and `aws-lc-rs` crypto providers through conditional compilation: - -```rust -#[cfg(feature = "ring")] -fn perform_x25519_ecdh(...) { /* ring implementation */ } - -#[cfg(feature = "aws_lc_rs")] -fn perform_x25519_ecdh(...) { /* aws-lc-rs implementation */ } -``` - -### Cryptographic Operations - -1. **X25519 ECDH**: Uses provider's X25519 implementation -2. **HKDF-SHA256**: Obtained from TLS13_AES_128_GCM_SHA256 cipher suite -3. **AES-128-GCM**: Uses provider's AEAD implementation -4. **Randomness**: Uses provider's secure random generator - -## Security Considerations - -### Requirements - -- **Server Public Key Security**: The server's X25519 public key must be obtained through a secure, authenticated channel. An attacker with the ability to substitute the server public key can break the Reality protocol's privacy guarantees. - -- **Short ID Confidentiality**: The `short_id` serves as a client identifier. Keep it confidential to prevent tracking. - -- **Crypto Provider**: Reality requires X25519 and AES-128-GCM support. Use either `ring` or `aws-lc-rs` features. - -- **TLS Version**: Reality is compatible with both TLS 1.2 and TLS 1.3, but works best with TLS 1.3. - -### Threat Model - -Reality protects against: -- **Passive Observation**: Session IDs are encrypted, preventing observers from correlating connections -- **Active Probing**: Without the correct server private key, attackers cannot decrypt or forge valid session IDs - -Reality does NOT protect against: -- **Compromised Server**: If the server's private key is compromised, past session IDs can be decrypted -- **Traffic Analysis**: Connection timing and size metadata may still leak information - -## Testing - -### Unit Tests - -```bash -cargo test --package watfaq-rustls --lib reality -``` - -Tests include: -- Configuration validation -- X25519 ECDH correctness -- AES-128-GCM encryption -- Session ID plaintext structure -- Full session ID computation pipeline - -### Integration Tests - -Integration tests verify: -- RealitySessionState creation -- Key share entry generation -- Complete session_id computation with both crypto providers -- Builder pattern integration - -## Implementation Notes - -### Design Decisions - -1. **Inline Computation**: Session ID is computed inline during handshake emission rather than using a callback, providing access to `Random` value and full `ClientHello` bytes. - -2. **State Management**: Reality state is passed through the handshake state machine to maintain access to cryptographic materials. - -3. **Provider Abstraction**: Uses rustls's existing crypto provider system for all cryptographic operations. - -4. **No Protocol Breaking Changes**: Reality is implemented as an optional extension without modifying core TLS protocol handling. - -### Limitations - -- **One-Time Use**: The ephemeral X25519 keypair is generated per connection and not reused. - -- **No HelloRetryRequest**: Reality state is not carried through HelloRetryRequest flows (set to `None` on retry). - -- **Fixed Crypto**: Currently requires HKDF-SHA256 (from TLS13_AES_128_GCM_SHA256) and AES-128-GCM. - -## References - -- [VLESS Protocol Specification](https://github.com/XTLS/REALITY) -- [RFC 7748: Elliptic Curves for Security (X25519)](https://datatracker.ietf.org/doc/html/rfc7748) -- [RFC 5869: HKDF-SHA256](https://datatracker.ietf.org/doc/html/rfc5869) -- [RFC 5116: AES-GCM](https://datatracker.ietf.org/doc/html/rfc5116) - -## Changelog - -### Version 0.23.21 - -- Initial implementation of VLESS Reality protocol -- Support for both `ring` and `aws-lc-rs` crypto providers -- Public API: `RealityConfig`, `RealityConfigError` -- Builder method: `ConfigBuilder::with_reality()` -- Example: `reality-client` binary -- Comprehensive unit and integration tests - -## License - -This implementation follows the same license as rustls: Apache-2.0 OR ISC OR MIT. diff --git a/examples/src/bin/reality-client.rs b/examples/src/bin/reality-client.rs index 2260b2bf3d1..84a236b89fc 100644 --- a/examples/src/bin/reality-client.rs +++ b/examples/src/bin/reality-client.rs @@ -24,20 +24,6 @@ //! Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs \ //! 1bc2c1ef1c //! ``` -//! -//! Note: You can directly copy-paste the values from airport Reality config! -//! -//! ## Method 2: Parsing airport Reality configuration format -//! -//! Many airports/VPN providers give Reality config in YAML format like: -//! ```yaml -//! reality-opts: -//! public-key: Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs -//! short-id: 1bc2c1ef1c -//! ``` -//! -//! See the `parse_airport_reality_config()` function below for how to parse this format. -//! The public-key is Base64 encoded and short-id is hexadecimal. use std::env; use std::io::{stdout, Read, Write}; @@ -51,31 +37,36 @@ use watfaq_rustls::RootCertStore; fn main() { // Parse command line arguments let args: Vec = env::args().collect(); - if args.len() != 4 { - eprintln!("Usage: {} ", args[0]); + if args.len() != 5 { + eprintln!("Usage: {} ", args[0]); eprintln!(); eprintln!("Parameters:"); - eprintln!(" Server address (e.g., example.com:443)"); + 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 airport Reality config):"); - eprintln!(" {} example.com:443 \\", args[0]); + 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"); - eprintln!(); - eprintln!("If you have airport Reality config in this format:"); - eprintln!(" reality-opts:"); - eprintln!(" public-key: Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"); - eprintln!(" short-id: 1bc2c1ef1c"); - eprintln!(); - eprintln!("Just copy and paste the values directly!"); std::process::exit(1); } let server_addr = args[1].clone(); - let public_key_base64 = args[2].clone(); - let short_id_hex = args[3].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) @@ -107,6 +98,8 @@ fn main() { 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| { @@ -117,6 +110,10 @@ fn main() { 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 { @@ -132,17 +129,14 @@ fn main() { // Allow using SSLKEYLOGFILE for debugging config.key_log = Arc::new(watfaq_rustls::KeyLogFile::new()); - println!("\nConnecting to {}...", &server_addr); + println!("\nConnecting to {} (SNI: {})...", &server_addr, &sni_servername); - // Extract server name from address - let server_name: pki_types::ServerName<'static> = server_addr - .split(':') - .next() - .unwrap() + // 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 server name: {:?}", e); + eprintln!("Error parsing SNI servername: {:?}", e); std::process::exit(1); }); @@ -161,42 +155,36 @@ fn main() { let mut tls = watfaq_rustls::Stream::new(&mut conn, &mut sock); - // Complete the handshake first - println!("Performing TLS handshake..."); - tls.conn.complete_io(&mut sock).unwrap_or_else(|e| { - eprintln!("Error during TLS handshake: {}", e); - std::process::exit(1); - }); - - println!("✓ TLS handshake completed successfully with Reality protocol!"); - - // Print negotiated cipher suite - if let Some(ciphersuite) = tls.conn.negotiated_cipher_suite() { - println!("✓ Cipher suite: {:?}", ciphersuite.suite()); - } - - // Print protocol version - if let Some(version) = tls.conn.protocol_version() { - println!("✓ Protocol version: {:?}", version); - } - // Send a simple HTTP request - println!("\nSending 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", - server_addr.split(':').next().unwrap() + sni_servername ); - tls.write_all(request.as_bytes()) - .unwrap_or_else(|e| { - eprintln!("Error sending request: {}", e); - std::process::exit(1); - }); + 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 @@ -236,79 +224,9 @@ fn bytes_to_hex(bytes: &[u8]) -> String { .join("") } -/// Example: Parse airport Reality configuration format -/// -/// Many airports/VPN providers give Reality config in this format: -/// ```yaml -/// reality-opts: -/// public-key: Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs -/// short-id: 1bc2c1ef1c -/// ``` -/// -/// This function demonstrates how to parse such configuration into RealityConfig. -/// -/// # Example -/// -/// ```no_run -/// use watfaq_rustls::client::RealityConfig; -/// -/// // Configuration from airport -/// let public_key_base64 = "Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"; -/// let short_id_hex = "1bc2c1ef1c"; -/// -/// // Parse public key from Base64 -/// let public_key_bytes = base64_to_bytes(public_key_base64) -/// .expect("Invalid base64 public key"); -/// let mut public_key = [0u8; 32]; -/// public_key.copy_from_slice(&public_key_bytes); -/// -/// // Parse short_id from hex -/// let short_id = hex_to_bytes(short_id_hex) -/// .expect("Invalid hex short_id"); -/// -/// // Create RealityConfig -/// let reality_config = RealityConfig::new(public_key, short_id) -/// .expect("Invalid Reality configuration"); -/// ``` -#[allow(dead_code)] -fn parse_airport_reality_config( - public_key_base64: &str, - short_id_hex: &str, -) -> Result { - // Parse public key from Base64 - let public_key_bytes = base64_to_bytes(public_key_base64) - .map_err(|e| format!("Failed to decode public key: {}", e))?; - - if public_key_bytes.len() != 32 { - return Err(format!( - "Invalid public key length: expected 32 bytes, got {}", - public_key_bytes.len() - )); - } - - let mut public_key = [0u8; 32]; - public_key.copy_from_slice(&public_key_bytes); - - // Parse short_id from hex - let short_id = hex_to_bytes(short_id_hex) - .map_err(|e| format!("Failed to decode short_id: {}", e))?; - - if short_id.len() > 8 { - return Err(format!( - "short_id too long: expected max 8 bytes, got {}", - short_id.len() - )); - } - - // Create RealityConfig - RealityConfig::new(public_key, short_id) - .map_err(|e| format!("Failed to create RealityConfig: {}", e)) -} - /// Helper function to decode Base64 string to bytes /// /// Supports both standard Base64 and Base64 URL-safe encoding. -#[allow(dead_code)] fn base64_to_bytes(base64_str: &str) -> Result, String> { use base64::Engine; @@ -322,42 +240,3 @@ fn base64_to_bytes(base64_str: &str) -> Result, String> { .decode(base64_str) .map_err(|e| format!("Invalid Base64: {}", e)) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_airport_reality_config() { - // Example configuration from an airport - let public_key_base64 = "Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"; - let short_id_hex = "1bc2c1ef1c"; - - let config = parse_airport_reality_config(public_key_base64, short_id_hex); - assert!(config.is_ok(), "Should parse valid airport config"); - - let config = config.unwrap(); - // Verify we can use it (this just checks it was created successfully) - drop(config); - } - - #[test] - fn test_base64_to_bytes() { - // Standard Base64 - let result = base64_to_bytes("SGVsbG8gV29ybGQ="); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), b"Hello World"); - - // URL-safe Base64 - let result = base64_to_bytes("Vc8ycAgKqfRvtXjvGP0ry_U91o5wgrQlqOhHq72HYRs"); - assert!(result.is_ok()); - assert_eq!(result.unwrap().len(), 32); // X25519 public key is 32 bytes - } - - #[test] - fn test_hex_to_bytes() { - let result = hex_to_bytes("1bc2c1ef1c"); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), vec![0x1b, 0xc2, 0xc1, 0xef, 0x1c]); - } -} diff --git a/rustls/src/client/hs.rs b/rustls/src/client/hs.rs index 60bd9896487..43f36d5d054 100644 --- a/rustls/src/client/hs.rs +++ b/rustls/src/client/hs.rs @@ -30,7 +30,7 @@ use crate::log::{debug, trace}; use crate::msgs::base::Payload; use crate::msgs::codec::Codec; use crate::msgs::enums::{ - CertificateType, Compression, ECPointFormat, ExtensionType, PSKKeyExchangeMode, + CertificateType, Compression, ECPointFormat, ExtensionType, NamedGroup, PSKKeyExchangeMode, }; use crate::msgs::handshake::{ CertificateStatusRequest, ClientExtension, ClientHelloPayload, ClientSessionTicket, @@ -117,7 +117,25 @@ where let mut resuming = find_session(&server_name, &config, cx); - let key_share = if config.supports_version(ProtocolVersion::TLSv1_3) { + // Initialize Reality state if configured + let reality_state = config + .reality_config + .as_ref() + .map(|reality_config| { + reality::RealitySessionState::new(Arc::clone(reality_config), &config.provider) + }) + .transpose()?; + + // For Reality, use Reality's X25519 key exchange; otherwise use normal TLS + let key_share = if reality_state.is_some() { + // Reality provides its own key exchange (X25519) + // Set kx_state to X25519 group for Reality + let x25519_group = config + .find_kx_group(NamedGroup::X25519, ProtocolVersion::TLSv1_3) + .expect("X25519 group required for Reality"); + cx.common.kx_state = KxState::Start(x25519_group); + None // Will be set later from reality_state + } else if config.supports_version(ProtocolVersion::TLSv1_3) { Some(tls13::initial_key_share( &config, &server_name, @@ -185,15 +203,6 @@ where _ => None, }; - // Initialize Reality state if configured - let reality_state = config - .reality_config - .as_ref() - .map(|reality_config| { - reality::RealitySessionState::new(Arc::clone(reality_config), &config.provider) - }) - .transpose()?; - emit_client_hello_for_retry( transcript_buffer, None, @@ -261,6 +270,16 @@ fn emit_client_hello_for_retry( where T: Fn(&[u8]) -> [u8; 32], { + // If Reality is enabled, convert Reality state into ActiveKeyExchange + // This ensures the Reality shared secret is used for TLS key schedule + let (key_share, reality_key_share_entry) = if let Some(ref reality) = reality_state { + let entry = reality.key_share_entry(); + let kx = reality.clone().into_key_exchange(); + (Some(kx), Some(entry)) + } else { + (key_share, None) + }; + let config = &input.config; // Defense in depth: the ECH state should be None if ECH is disabled based on config // builder semantics. @@ -343,9 +362,10 @@ where }; // Add key_share extension - // Reality overrides the normal key_share with its own X25519 key - if let Some(ref reality) = reality_state { - exts.push(ClientExtension::KeyShare(vec![reality.key_share_entry()])); + // If Reality is enabled, use Reality's key_share entry + // Otherwise use normal TLS key_share + if let Some(reality_entry) = reality_key_share_entry { + exts.push(ClientExtension::KeyShare(vec![reality_entry])); } else if let Some(key_share) = &key_share { debug_assert!(support_tls13); let mut shares = vec![KeyShareEntry::new(key_share.group(), key_share.pub_key())]; diff --git a/rustls/src/client/reality.rs b/rustls/src/client/reality.rs index b7078079a3a..3e3cab48371 100644 --- a/rustls/src/client/reality.rs +++ b/rustls/src/client/reality.rs @@ -2,26 +2,37 @@ //! //! 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. +//! server's static public key. //! //! # Protocol Overview //! -//! The Reality protocol works as follows: -//! 1. Client generates ephemeral X25519 keypair -//! 2. Client performs ECDH with server's public key to get shared_secret -//! 3. Client derives auth_key using HKDF-SHA256(shared_secret, hello_random[:20], "REALITY") +//! The Reality protocol uses a single X25519 keypair for two purposes: +//! +//! ## 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") //! 4. Client constructs 16-byte plaintext: [version(3) | reserved(1) | timestamp(4) | short_id(8)] //! 5. Client encrypts plaintext using AES-128-GCM with: //! - key: auth_key //! - nonce: hello_random[20..32] //! - aad: full ClientHello bytes //! 6. Result (ciphertext + tag = 32 bytes) becomes the session_id -//! 7. Client's public key is injected into the key_share extension +//! +//! ## 2. TLS Key Exchange (standard TLS 1.3 ECDHE) +//! 7. Client's public key (client_public) is sent in the ClientHello key_share extension +//! 8. Server responds with ServerHello containing its ephemeral public key +//! 9. Client performs ECDH with server's **ephemeral public key**: tls_shared_secret = ECDH(client_private, server_hello_public_key) +//! 10. tls_shared_secret is used for the TLS 1.3 key schedule (handshake and application traffic keys) +//! +//! **Key Point**: The same client_private is used for both ECDH operations, but with different +//! server public keys, resulting in two different shared secrets for different purposes. +use alloc::boxed::Box; use alloc::sync::Arc; use alloc::vec::Vec; -use crate::crypto::CryptoProvider; +use crate::crypto::{ActiveKeyExchange, CryptoProvider, SecureRandom, SharedSecret}; use crate::crypto::tls13::Hkdf; use crate::error::Error; use crate::msgs::enums::NamedGroup; @@ -128,36 +139,135 @@ impl core::fmt::Display for RealityConfigError { #[cfg(feature = "std")] impl std::error::Error for RealityConfigError {} +/// Generate X25519 keypair using ring +#[cfg(all(feature = "ring", not(feature = "aws_lc_rs")))] +fn x25519_generate_keypair( + secure_random: &dyn SecureRandom, +) -> Result<([u8; 32], [u8; 32]), Error> { + use ring::agreement; + + // Generate random private key + let mut private_bytes = [0u8; 32]; + secure_random.fill(&mut private_bytes)?; + + // Compute public key from private key using PrivateKey + let private_key = agreement::PrivateKey::from_private_key(&agreement::X25519, &private_bytes) + .map_err(|_| Error::General("X25519 private key creation failed".into()))?; + + let public_key_bytes = private_key + .compute_public_key() + .map_err(|_| Error::General("X25519 public key computation failed".into()))?; + + let mut public = [0u8; 32]; + public.copy_from_slice(public_key_bytes.as_ref()); + + Ok((private_bytes, public)) +} + +/// Perform X25519 ECDH using ring +#[cfg(all(feature = "ring", not(feature = "aws_lc_rs")))] +fn x25519_ecdh(private_key: &[u8; 32], peer_public_key: &[u8; 32]) -> Result<[u8; 32], Error> { + use ring::agreement; + + let private_key = agreement::PrivateKey::from_private_key(&agreement::X25519, private_key) + .map_err(|_| Error::General("X25519 private key creation failed".into()))?; + + let peer_public = + agreement::UnparsedPublicKey::new(&agreement::X25519, peer_public_key.as_ref()); + + let mut shared_secret = [0u8; 32]; + agreement::agree(&private_key, &peer_public, |key_material| { + shared_secret.copy_from_slice(key_material); + Ok(()) + }) + .map_err(|_| Error::General("X25519 ECDH failed".into()))?; + + Ok(shared_secret) +} + +/// Generate X25519 keypair using aws-lc-rs +#[cfg(feature = "aws_lc_rs")] +fn x25519_generate_keypair( + secure_random: &dyn SecureRandom, +) -> Result<([u8; 32], [u8; 32]), Error> { + use aws_lc_rs::agreement; + + // Generate random private key + let mut private_bytes = [0u8; 32]; + secure_random.fill(&mut private_bytes)?; + + // Compute public key from private key using PrivateKey + let private_key = agreement::PrivateKey::from_private_key(&agreement::X25519, &private_bytes) + .map_err(|_| Error::General("X25519 private key creation failed".into()))?; + + let public_key_bytes = private_key + .compute_public_key() + .map_err(|_| Error::General("X25519 public key computation failed".into()))?; + + let mut public = [0u8; 32]; + public.copy_from_slice(public_key_bytes.as_ref()); + + Ok((private_bytes, public)) +} + +/// Perform X25519 ECDH using aws-lc-rs +#[cfg(feature = "aws_lc_rs")] +fn x25519_ecdh(private_key: &[u8; 32], peer_public_key: &[u8; 32]) -> Result<[u8; 32], Error> { + use aws_lc_rs::agreement; + + let private_key = agreement::PrivateKey::from_private_key(&agreement::X25519, private_key) + .map_err(|_| Error::General("X25519 private key creation failed".into()))?; + + let peer_public = + agreement::UnparsedPublicKey::new(&agreement::X25519, peer_public_key.as_ref()); + + let mut shared_secret = [0u8; 32]; + agreement::agree(&private_key, &peer_public, (), |key_material| { + shared_secret.copy_from_slice(key_material); + Ok(()) + }) + .map_err(|_| Error::General("X25519 ECDH failed".into()))?; + + Ok(shared_secret) +} + /// Internal state for Reality protocol during TLS handshake /// /// This struct holds the ephemeral keys and shared secret needed to compute /// the Reality session_id. +#[derive(Clone)] pub(crate) struct RealitySessionState { config: Arc, + /// Client's ephemeral X25519 private key (32 bytes) + client_private: [u8; 32], /// Client's ephemeral X25519 public key (32 bytes) client_public: [u8; 32], - /// ECDH shared secret with server (32 bytes) - shared_secret: [u8; 32], + /// ECDH shared secret with server's static public key (32 bytes) + /// Used for Reality authentication (session_id encryption) + auth_shared_secret: [u8; 32], } impl RealitySessionState { /// Initialize Reality state by performing X25519 ECDH /// /// This generates an ephemeral X25519 keypair and performs ECDH with - /// the server's public key to derive the shared secret. + /// the server's static public key to derive the auth shared secret. pub(crate) fn new( config: Arc, crypto_provider: &CryptoProvider, ) -> Result { - // Perform X25519 ECDH using the unified provider interface - let (client_public, shared_secret) = crypto_provider - .x25519_provider - .x25519_ecdh(&config.server_public_key)?; + // Step 1: Generate X25519 keypair + let (client_private, client_public) = + x25519_generate_keypair(crypto_provider.secure_random)?; + + // Step 2: Perform ECDH with server's static public key (for Reality authentication) + let auth_shared_secret = x25519_ecdh(&client_private, &config.server_public_key)?; Ok(Self { config, + client_private, client_public, - shared_secret, + auth_shared_secret, }) } @@ -168,6 +278,16 @@ impl RealitySessionState { KeyShareEntry::new(NamedGroup::X25519, self.client_public.to_vec()) } + /// Convert Reality state into an ActiveKeyExchange for TLS handshake + /// + /// This wraps the Reality X25519 keypair so it can be used in the TLS key schedule. + pub(crate) fn into_key_exchange(self) -> Box { + Box::new(RealityKeyExchange { + client_private: self.client_private, + client_public: self.client_public, + }) + } + /// Compute Reality session_id using the full protocol /// /// # Parameters @@ -187,9 +307,9 @@ impl RealitySessionState { time_provider: &dyn crate::time_provider::TimeProvider, ) -> Result<[u8; 32], Error> { // Step 1: Derive auth_key using HKDF-SHA256 - // auth_key = HKDF(shared_secret, salt=hello_random[:20], info="REALITY") + // auth_key = HKDF(auth_shared_secret, salt=hello_random[:20], info="REALITY") let salt = &random.0[..20]; - let auth_key_expander = hkdf.extract_from_secret(Some(salt), &self.shared_secret); + let auth_key_expander = hkdf.extract_from_secret(Some(salt), &self.auth_shared_secret); let mut auth_key = [0u8; 16]; auth_key_expander @@ -219,7 +339,48 @@ impl RealitySessionState { .try_into() .map_err(|_| Error::General("Invalid nonce length".into()))?; - aes_128_gcm_encrypt(&auth_key, nonce, hello_bytes, &plaintext) + let result = aes_128_gcm_encrypt(&auth_key, nonce, hello_bytes, &plaintext)?; + + Ok(result) + } +} + +/// ActiveKeyExchange implementation for Reality protocol +/// +/// This wraps the Reality X25519 keypair to provide it to the TLS key schedule. +/// Reality uses the same X25519 keypair for both: +/// 1. Authentication ECDH with server's static public key (for session_id encryption) +/// 2. TLS ECDH with server's ephemeral public key from ServerHello (for TLS key schedule) +struct RealityKeyExchange { + client_private: [u8; 32], + client_public: [u8; 32], +} + +impl ActiveKeyExchange for RealityKeyExchange { + /// Complete the key exchange + /// + /// Performs ECDH with the server's ephemeral public key from ServerHello + /// to derive the TLS shared secret for the key schedule. + fn complete(self: Box, peer_pub_key: &[u8]) -> Result { + // Convert peer_pub_key to [u8; 32] + let peer_public: [u8; 32] = peer_pub_key + .try_into() + .map_err(|_| Error::General("Invalid peer public key length".into()))?; + + // Perform ECDH with ServerHello's ephemeral public key (for TLS key schedule) + let tls_shared_secret = x25519_ecdh(&self.client_private, &peer_public)?; + + Ok(SharedSecret::from(&tls_shared_secret[..])) + } + + /// Return the client's public key + fn pub_key(&self) -> &[u8] { + &self.client_public + } + + /// Return the named group (always X25519 for Reality) + fn group(&self) -> NamedGroup { + NamedGroup::X25519 } } @@ -376,7 +537,7 @@ mod tests { #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] #[test] - fn test_x25519_ecdh() { + fn test_x25519_keypair_and_ecdh() { // Install provider #[cfg(feature = "ring")] let _ = crate::crypto::ring::default_provider().install_default(); @@ -385,15 +546,50 @@ mod tests { let provider = crate::crypto::CryptoProvider::get_default().unwrap(); - // Test that ECDH produces 32-byte outputs - let server_public = [2u8; 32]; - let result = provider.x25519_provider.x25519_ecdh(&server_public); + // Test keypair generation + let result = x25519_generate_keypair(provider.secure_random); assert!(result.is_ok()); - let (client_public, shared_secret) = result.unwrap(); - assert_eq!(client_public.len(), 32); + let (private_key, public_key) = result.unwrap(); + assert_eq!(private_key.len(), 32); + assert_eq!(public_key.len(), 32); + + // Test ECDH with a test peer public key + let peer_public = [2u8; 32]; + let result = x25519_ecdh(&private_key, &peer_public); + assert!(result.is_ok()); + let shared_secret = result.unwrap(); assert_eq!(shared_secret.len(), 32); } + #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] + #[test] + fn test_reality_two_ecdh_operations() { + // Install provider + #[cfg(feature = "ring")] + let _ = crate::crypto::ring::default_provider().install_default(); + #[cfg(all(not(feature = "ring"), feature = "aws_lc_rs"))] + let _ = crate::crypto::aws_lc_rs::default_provider().install_default(); + + let provider = crate::crypto::CryptoProvider::get_default().unwrap(); + + // Simulate server's static and ephemeral public keys + let server_static_pubkey = [0xAAu8; 32]; + let server_ephemeral_pubkey = [0xBBu8; 32]; + + // Generate client keypair + let (client_private, _client_public) = + x25519_generate_keypair(provider.secure_random).unwrap(); + + // Perform ECDH with server's static public key (for Reality authentication) + let auth_secret = x25519_ecdh(&client_private, &server_static_pubkey).unwrap(); + + // Perform ECDH with server's ephemeral public key (for TLS key schedule) + let tls_secret = x25519_ecdh(&client_private, &server_ephemeral_pubkey).unwrap(); + + // The two shared secrets should be different + assert_ne!(auth_secret, tls_secret); + } + #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] #[test] fn test_aes_128_gcm_encryption() { @@ -468,8 +664,9 @@ mod tests { assert!(state.is_ok()); let state = state.unwrap(); + assert_eq!(state.client_private.len(), 32); assert_eq!(state.client_public.len(), 32); - assert_eq!(state.shared_secret.len(), 32); + assert_eq!(state.auth_shared_secret.len(), 32); } #[cfg(any(feature = "ring", feature = "aws_lc_rs"))] diff --git a/rustls/src/crypto/aws_lc_rs/mod.rs b/rustls/src/crypto/aws_lc_rs/mod.rs index 02d9b489fb0..c31874aa92d 100644 --- a/rustls/src/crypto/aws_lc_rs/mod.rs +++ b/rustls/src/crypto/aws_lc_rs/mod.rs @@ -9,7 +9,7 @@ pub(crate) use aws_lc_rs as ring_like; use pki_types::PrivateKeyDer; use webpki::aws_lc_rs as webpki_algs; -use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom, X25519Provider}; +use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom}; use crate::enums::SignatureScheme; use crate::rand::GetRandomFailed; use crate::sign::SigningKey; @@ -44,7 +44,6 @@ pub fn default_provider() -> CryptoProvider { signature_verification_algorithms: SUPPORTED_SIG_ALGS, secure_random: &AwsLcRs, key_provider: &AwsLcRs, - x25519_provider: &AwsLcRs, } } @@ -93,43 +92,6 @@ impl KeyProvider for AwsLcRs { } } -impl X25519Provider for AwsLcRs { - fn x25519_ecdh(&self, peer_public_key: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error> { - use ring_like::agreement; - - let rng = ring_like::rand::SystemRandom::new(); - - // Generate ephemeral X25519 private key - let private_key = agreement::EphemeralPrivateKey::generate(&agreement::X25519, &rng) - .map_err(unspecified_err)?; - - // Compute public key - let public_key = private_key - .compute_public_key() - .map_err(unspecified_err)?; - - let mut client_public = [0u8; 32]; - client_public.copy_from_slice(public_key.as_ref()); - - // Perform ECDH - let peer_public = - agreement::UnparsedPublicKey::new(&agreement::X25519, peer_public_key); - - let mut shared_secret = [0u8; 32]; - agreement::agree_ephemeral(private_key, &peer_public, (), |key_material| { - shared_secret.copy_from_slice(key_material); - Ok(()) - }) - .map_err(|_| Error::General("X25519 ECDH failed".into()))?; - - Ok((client_public, shared_secret)) - } - - fn fips(&self) -> bool { - fips() - } -} - /// The cipher suite configuration that an application should use by default. /// /// This will be [`ALL_CIPHER_SUITES`] sans any supported cipher suites that diff --git a/rustls/src/crypto/mod.rs b/rustls/src/crypto/mod.rs index 0b1d1befab1..3a39f2d8b6b 100644 --- a/rustls/src/crypto/mod.rs +++ b/rustls/src/crypto/mod.rs @@ -214,12 +214,6 @@ pub struct CryptoProvider { /// Provider for loading private [SigningKey]s from [PrivateKeyDer]. pub key_provider: &'static dyn KeyProvider, - - /// Provider for X25519 key exchange operations. - /// - /// This is used by protocols that need direct X25519 ECDH functionality, - /// such as VLESS Reality protocol. - pub x25519_provider: &'static dyn X25519Provider, } impl CryptoProvider { @@ -300,14 +294,12 @@ impl CryptoProvider { signature_verification_algorithms, secure_random, key_provider, - x25519_provider, } = self; cipher_suites.iter().all(|cs| cs.fips()) && kx_groups.iter().all(|kx| kx.fips()) && signature_verification_algorithms.fips() && secure_random.fips() && key_provider.fips() - && x25519_provider.fips() } } @@ -331,38 +323,6 @@ pub trait SecureRandom: Send + Sync + Debug { } } -/// A provider for X25519 key exchange operations. -/// -/// This trait provides a unified interface for performing X25519 ECDH -/// key exchange, abstracting over different cryptographic backends. -pub trait X25519Provider: Send + Sync + Debug { - /// Perform X25519 ECDH with the given peer public key. - /// - /// This method: - /// 1. Generates an ephemeral X25519 keypair - /// 2. Computes the ECDH shared secret with `peer_public_key` - /// 3. Returns both the client's public key and the shared secret - /// - /// # Arguments - /// * `peer_public_key` - The peer's X25519 public key (32 bytes) - /// - /// # Returns - /// * `Ok((client_public, shared_secret))` on success, where: - /// - `client_public`: The generated ephemeral public key (32 bytes) - /// - `shared_secret`: The ECDH shared secret (32 bytes) - /// * `Err(Error)` if key generation or ECDH fails - /// - /// # Security - /// The ephemeral private key is generated using a cryptographically - /// secure random number generator and is securely erased after use. - fn x25519_ecdh(&self, peer_public_key: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error>; - - /// Return `true` if this is backed by a FIPS-approved implementation. - fn fips(&self) -> bool { - false - } -} - /// A mechanism for loading private [SigningKey]s from [PrivateKeyDer]. /// /// This trait is intended to be used with private key material that is sourced from DER, diff --git a/rustls/src/crypto/ring/mod.rs b/rustls/src/crypto/ring/mod.rs index eeb7cf59c03..8f8ffc0053d 100644 --- a/rustls/src/crypto/ring/mod.rs +++ b/rustls/src/crypto/ring/mod.rs @@ -4,7 +4,7 @@ use pki_types::PrivateKeyDer; pub(crate) use ring as ring_like; use webpki::ring as webpki_algs; -use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom, X25519Provider}; +use crate::crypto::{CryptoProvider, KeyProvider, SecureRandom}; use crate::enums::SignatureScheme; use crate::rand::GetRandomFailed; use crate::sign::SigningKey; @@ -36,7 +36,6 @@ pub fn default_provider() -> CryptoProvider { signature_verification_algorithms: SUPPORTED_SIG_ALGS, secure_random: &Ring, key_provider: &Ring, - x25519_provider: &Ring, } } @@ -63,40 +62,6 @@ impl KeyProvider for Ring { } } -impl X25519Provider for Ring { - fn x25519_ecdh(&self, peer_public_key: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error> { - use ring_like::agreement; - use ring_like::rand::SecureRandom; - - let rng = ring_like::rand::SystemRandom::new(); - - // Generate ephemeral X25519 private key - let private_key = agreement::EphemeralPrivateKey::generate(&agreement::X25519, &rng) - .map_err(|_| Error::General("X25519 key generation failed".into()))?; - - // Compute public key - let public_key = private_key - .compute_public_key() - .map_err(|_| Error::General("X25519 public key computation failed".into()))?; - - let mut client_public = [0u8; 32]; - client_public.copy_from_slice(public_key.as_ref()); - - // Perform ECDH - let peer_public = - agreement::UnparsedPublicKey::new(&agreement::X25519, peer_public_key); - - let mut shared_secret = [0u8; 32]; - agreement::agree_ephemeral(private_key, &peer_public, |key_material| { - shared_secret.copy_from_slice(key_material); - Ok(()) - }) - .map_err(|_| Error::General("X25519 ECDH failed".into()))?; - - Ok((client_public, shared_secret)) - } -} - /// The cipher suite configuration that an application should use by default. /// /// This will be [`ALL_CIPHER_SUITES`] sans any supported cipher suites that