diff --git a/Cargo.lock b/Cargo.lock index cb98a53..c2639f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,7 +680,9 @@ dependencies = [ "clap", "common", "miette", + "rustls", "tokio", + "tokio-rustls", ] [[package]] diff --git a/common/src/lib.rs b/common/src/lib.rs index 389f263..50e627f 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -55,6 +55,7 @@ impl fmt::Display for BenchRecord { #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; #[test] fn bench_record_serializes_to_ndjson() { @@ -73,7 +74,6 @@ mod tests { #[test] fn key_exchange_mode_from_str() { - use std::str::FromStr; assert_eq!( KeyExchangeMode::from_str("x25519").expect("should parse"), KeyExchangeMode::X25519 diff --git a/runner/Cargo.toml b/runner/Cargo.toml index c898463..44df097 100644 --- a/runner/Cargo.toml +++ b/runner/Cargo.toml @@ -5,9 +5,11 @@ authors.workspace = true edition.workspace = true [dependencies] -common.workspace = true clap.workspace = true +common.workspace = true miette.workspace = true +rustls.workspace = true +tokio-rustls.workspace = true tokio.workspace = true [lints] diff --git a/runner/src/main.rs b/runner/src/main.rs index 0fa5bbe..a5adeb7 100644 --- a/runner/src/main.rs +++ b/runner/src/main.rs @@ -12,14 +12,23 @@ use common::{ protocol::{read_payload, write_request}, }; use miette::miette; +use rustls::{ + ClientConfig, DigitallySignedStruct, SignatureScheme, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::aws_lc_rs::{self, kx_group::X25519}, + pki_types::{CertificateDer, ServerName, UnixTime}, + version::TLS13, +}; use std::{ fs::File, io::{BufWriter, Write, stdout}, net::SocketAddr, path::PathBuf, + sync::Arc, time::Instant, }; use tokio::net::TcpStream; +use tokio_rustls::TlsConnector; /// TLS benchmark runner. #[derive(Debug, Parser)] @@ -60,25 +69,108 @@ struct IterationResult { ttlb_ns: u64, } -/// Run a single benchmark iteration over plain TCP. +/// Certificate verifier that accepts any certificate. +/// Used for benchmarking where we don't need to verify the server's identity. +#[derive(Debug)] +struct NoVerifier; + +impl ServerCertVerifier for NoVerifier { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::ED25519, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + ] + } +} + +/// Build TLS client config for the given key exchange mode. +fn build_tls_config(mode: KeyExchangeMode) -> miette::Result> { + // Select crypto provider with appropriate key exchange groups + let mut provider = aws_lc_rs::default_provider(); + provider.kx_groups = match mode { + KeyExchangeMode::X25519 => vec![X25519], + KeyExchangeMode::X25519Mlkem768 => { + todo!("Configure hybrid PQ key exchange") + } + }; + + let config = ClientConfig::builder_with_provider(Arc::new(provider)) + .with_protocol_versions(&[&TLS13]) + .map_err(|e| miette!("failed to set TLS versions: {e}"))? + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerifier)) + .with_no_client_auth(); + + Ok(Arc::new(config)) +} + +/// Run a single benchmark iteration over TLS. #[allow(clippy::cast_possible_truncation)] // nanoseconds won't overflow u64 for reasonable durations -async fn run_iteration(server: SocketAddr, payload_bytes: u64) -> miette::Result { +async fn run_iteration( + server: SocketAddr, + payload_bytes: u64, + tls_connector: &TlsConnector, + server_name: &ServerName<'static>, +) -> miette::Result { let start = Instant::now(); - // Connect (this is the "handshake" for plain TCP) - let mut stream = TcpStream::connect(server) + // TCP connect + let stream = TcpStream::connect(server) .await - .map_err(|e| miette!("connection failed: {e}"))?; + .map_err(|e| miette!("TCP connection failed: {e}"))?; + + // TLS handshake + let mut tls_stream = tls_connector + .connect(server_name.clone(), stream) + .await + .map_err(|e| miette!("TLS handshake failed: {e}"))?; let handshake_ns = start.elapsed().as_nanos() as u64; // Send request - write_request(&mut stream, payload_bytes) + write_request(&mut tls_stream, payload_bytes) .await .map_err(|e| miette!("write request failed: {e}"))?; // Read response - read_payload(&mut stream, payload_bytes) + read_payload(&mut tls_stream, payload_bytes) .await .map_err(|e| miette!("read payload failed: {e}"))?; @@ -90,7 +182,11 @@ async fn run_iteration(server: SocketAddr, payload_bytes: u64) -> miette::Result }) } -async fn run_benchmark(args: Args) -> miette::Result<()> { +async fn run_benchmark( + args: Args, + tls_connector: TlsConnector, + server_name: ServerName<'static>, +) -> miette::Result<()> { let total_iters = args.warmup + args.iters; // Open output file or use stdout @@ -104,7 +200,7 @@ async fn run_benchmark(args: Args) -> miette::Result<()> { }; eprintln!( - "Running {} warmup + {} measured iterations (concurrency: {}, TLS disabled)", + "Running {} warmup + {} measured iterations (concurrency: {}, TLS 1.3)", args.warmup, args.iters, args.concurrency ); eprintln!(); @@ -113,7 +209,13 @@ async fn run_benchmark(args: Args) -> miette::Result<()> { for i in 0..total_iters { let is_warmup = i < args.warmup; - let result = run_iteration(args.server, args.payload_bytes).await?; + let result = run_iteration( + args.server, + args.payload_bytes, + &tls_connector, + &server_name, + ) + .await?; if !is_warmup { let record = BenchRecord { @@ -159,5 +261,13 @@ async fn main() -> miette::Result<()> { ); eprintln!(); - run_benchmark(args).await + // Build TLS config (skips certificate verification for benchmarking) + let tls_config = build_tls_config(args.mode)?; + let tls_connector = TlsConnector::from(tls_config); + + // Server name for TLS (use "localhost" for local testing) + let server_name = ServerName::try_from("localhost".to_string()) + .map_err(|e| miette!("invalid server name: {e}"))?; + + run_benchmark(args, tls_connector, server_name).await } diff --git a/server/Cargo.toml b/server/Cargo.toml index 6c1d9b7..993fb12 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,12 +5,12 @@ authors.workspace = true edition.workspace = true [dependencies] -common.workspace = true clap.workspace = true +common.workspace = true miette.workspace = true rustls.workspace = true -tokio.workspace = true tokio-rustls.workspace = true +tokio.workspace = true [lints] workspace = true diff --git a/server/src/main.rs b/server/src/main.rs index 9e0bf06..27d9614 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -13,7 +13,7 @@ use common::{ use miette::miette; use rustls::{ ServerConfig, - crypto::aws_lc_rs::{self, kx_group}, + crypto::aws_lc_rs::{self, kx_group::X25519}, pki_types::{CertificateDer, PrivateKeyDer}, server::Acceptor, version::TLS13, @@ -46,7 +46,7 @@ fn build_tls_config( // Select crypto provider with appropriate key exchange groups let mut provider = aws_lc_rs::default_provider(); provider.kx_groups = match mode { - KeyExchangeMode::X25519 => vec![kx_group::X25519], + KeyExchangeMode::X25519 => vec![X25519], KeyExchangeMode::X25519Mlkem768 => { todo!("Configure hybrid PQ key exchange") }