From 5accec5da436c790926b66c9951e6805fb4d3229 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 26 Jan 2026 16:09:27 +0200 Subject: [PATCH] feat(bench-server): add TLS 1.3 with X25519 key exchange - Generate self-signed certificates on startup using rcgen - Configure rustls with aws_lc_rs crypto provider - Filter key exchange groups to X25519-only for mode=x25519 - Print CA certificate for client trust configuration - TLS 1.3 protocol enforced --- Cargo.lock | 140 +++++++++++++++++++++++++++++++++- Cargo.toml | 3 + bench-common/src/cert.rs | 3 +- bench-runner/src/main.rs | 18 +++-- bench-server/Cargo.toml | 2 + bench-server/src/main.rs | 158 +++++++++++++++++++++++++++++++++++---- 6 files changed, 298 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f081297..2eaeca1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -170,7 +192,9 @@ dependencies = [ "bench-common", "clap", "miette", + "rustls", "tokio", + "tokio-rustls", ] [[package]] @@ -192,6 +216,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -241,6 +267,15 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -287,6 +322,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "errno" version = "0.3.14" @@ -303,6 +344,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "getrandom" version = "0.2.17" @@ -314,6 +361,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gimli" version = "0.32.3" @@ -344,6 +403,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -495,6 +564,12 @@ dependencies = [ "asn1-rs", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -570,6 +645,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rcgen" version = "0.14.7" @@ -601,7 +682,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -635,6 +716,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -644,6 +739,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -752,6 +859,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "3.0.2" @@ -894,6 +1007,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -936,6 +1059,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -1098,6 +1230,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + [[package]] name = "x509-parser" version = "0.18.0" diff --git a/Cargo.toml b/Cargo.toml index e9b83f1..eb2cb09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,17 +8,20 @@ authors = ["Kristofers Solo "] edition = "2024" [workspace.dependencies] +aws-lc-rs = "1" bench-common = { path = "bench-common" } claims = "0.8" clap = { version = "4.5", features = ["derive"] } miette = { version = "7", features = ["fancy"] } rcgen = "0.14" rstest = "0.26" +rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "aws_lc_rs"] } serde = { version = "1", features = ["derive"] } serde_json = "1" strum = { version = "0.27", features = ["derive"] } thiserror = "2" tokio = { version = "1", features = ["full"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] } [workspace.lints.clippy] nursery = "warn" diff --git a/bench-common/src/cert.rs b/bench-common/src/cert.rs index 04afe8c..a85b46f 100644 --- a/bench-common/src/cert.rs +++ b/bench-common/src/cert.rs @@ -3,9 +3,8 @@ //! Generates a CA certificate and server certificate for TLS benchmarking. //! These certificates are NOT suitable for production use. -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, Issuer, KeyPair, SanType}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; /// Generated certificate material for TLS server. #[derive(Clone)] diff --git a/bench-runner/src/main.rs b/bench-runner/src/main.rs index ec539dd..d2ff68d 100644 --- a/bench-runner/src/main.rs +++ b/bench-runner/src/main.rs @@ -6,15 +6,19 @@ //! //! Outputs NDJSON records to stdout or a file. -use bench_common::protocol::{read_payload, write_request}; -use bench_common::{BenchRecord, KeyExchangeMode}; +use bench_common::{ + BenchRecord, KeyExchangeMode, + protocol::{read_payload, write_request}, +}; use clap::Parser; use miette::miette; -use std::fs::File; -use std::io::{BufWriter, Write, stdout}; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::time::Instant; +use std::{ + fs::File, + io::{BufWriter, Write, stdout}, + net::SocketAddr, + path::PathBuf, + time::Instant, +}; use tokio::net::TcpStream; /// TLS benchmark runner. diff --git a/bench-server/Cargo.toml b/bench-server/Cargo.toml index ff48213..73d8fd4 100644 --- a/bench-server/Cargo.toml +++ b/bench-server/Cargo.toml @@ -8,7 +8,9 @@ edition.workspace = true bench-common.workspace = true clap.workspace = true miette.workspace = true +rustls.workspace = true tokio.workspace = true +tokio-rustls.workspace = true [lints] workspace = true diff --git a/bench-server/src/main.rs b/bench-server/src/main.rs index ccf3f14..a07f2a4 100644 --- a/bench-server/src/main.rs +++ b/bench-server/src/main.rs @@ -1,14 +1,29 @@ //! TLS benchmark server. //! -//! Listens for connections and serves the benchmark protocol: +//! Listens for TLS connections and serves the benchmark protocol: //! - Reads 8-byte little-endian u64 (requested payload size N) //! - Responds with exactly N bytes (deterministic pattern) -use bench_common::protocol::{read_request, write_payload}; -use bench_common::KeyExchangeMode; +use bench_common::{ + KeyExchangeMode, + cert::{CaCertificate, ServerCertificate}, + protocol::{read_request, write_payload}, +}; use clap::Parser; -use std::net::SocketAddr; -use tokio::net::{TcpListener, TcpStream}; +use miette::miette; +use rustls::{ + ServerConfig, + crypto::aws_lc_rs::{self, kx_group}, + pki_types::{CertificateDer, PrivateKeyDer}, + server::Acceptor, + version::TLS13, +}; +use std::{fmt::Write, io::ErrorKind, net::SocketAddr, sync::Arc}; +use tokio::{ + io::AsyncWriteExt, + net::{TcpListener, TcpStream}, +}; +use tokio_rustls::LazyConfigAcceptor; /// TLS benchmark server. #[derive(Debug, Parser)] @@ -23,12 +38,66 @@ struct Args { listen: SocketAddr, } -async fn handle_connection(mut stream: TcpStream, peer: SocketAddr) { +/// Build TLS server config for the given key exchange mode. +fn build_tls_config( + mode: KeyExchangeMode, + server_cert: &ServerCertificate, +) -> 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![kx_group::X25519], + KeyExchangeMode::X25519Mlkem768 => { + // TODO: Configure hybrid PQ key exchange + return Err(miette!("X25519Mlkem768 not yet implemented")); + } + }; + + // Convert certificate chain + let certs: Vec> = server_cert + .cert_chain_der + .iter() + .map(|der| CertificateDer::from(der.clone())) + .collect(); + + // Convert private key + let key = PrivateKeyDer::try_from(server_cert.private_key_der.clone()) + .map_err(|e| miette!("invalid private key: {e}"))?; + + let config = ServerConfig::builder_with_provider(Arc::new(provider)) + .with_protocol_versions(&[&TLS13]) + .map_err(|e| miette!("failed to set TLS versions: {e}"))? + .with_no_client_auth() + .with_single_cert(certs, key) + .map_err(|e| miette!("failed to configure certificate: {e}"))?; + + Ok(Arc::new(config)) +} + +async fn handle_connection(stream: TcpStream, peer: SocketAddr, tls_config: Arc) { + // Perform TLS handshake + let acceptor = LazyConfigAcceptor::new(Acceptor::default(), stream); + let start_handshake = match acceptor.await { + Ok(sh) => sh, + Err(e) => { + eprintln!("[{peer}] TLS accept error: {e}"); + return; + } + }; + + let mut tls_stream = match start_handshake.into_stream(tls_config).await { + Ok(s) => s, + Err(e) => { + eprintln!("[{peer}] TLS handshake error: {e}"); + return; + } + }; + + // Handle protocol loop { - let payload_size = match read_request(&mut stream).await { + let payload_size = match read_request(&mut tls_stream).await { Ok(size) => size, - Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { - // Client closed connection + Err(e) if e.kind() == ErrorKind::UnexpectedEof => { break; } Err(e) => { @@ -37,20 +106,28 @@ async fn handle_connection(mut stream: TcpStream, peer: SocketAddr) { } }; - if let Err(e) = write_payload(&mut stream, payload_size).await { + if let Err(e) = write_payload(&mut tls_stream, payload_size).await { eprintln!("[{peer}] write error: {e}"); break; } + + // Flush to ensure data is sent + if let Err(e) = tls_stream.flush().await { + eprintln!("[{peer}] flush error: {e}"); + break; + } } } -async fn run_server(args: Args) -> miette::Result<()> { +async fn run_server(args: Args, tls_config: Arc) -> miette::Result<()> { let listener = TcpListener::bind(args.listen) .await - .map_err(|e| miette::miette!("failed to bind to {}: {e}", args.listen))?; + .map_err(|e| miette!("failed to bind to {}: {e}", args.listen))?; - eprintln!("Listening on {} (TCP, TLS disabled)", args.listen); - eprintln!("Mode: {} (not yet implemented)", args.mode); + eprintln!( + "Listening on {} (TLS 1.3, mode: {})", + args.listen, args.mode + ); loop { let (stream, peer) = match listener.accept().await { @@ -61,7 +138,8 @@ async fn run_server(args: Args) -> miette::Result<()> { } }; - tokio::spawn(handle_connection(stream, peer)); + let config = tls_config.clone(); + tokio::spawn(handle_connection(stream, peer, config)); } } @@ -74,5 +152,53 @@ async fn main() -> miette::Result<()> { eprintln!(" listen: {}", args.listen); eprintln!(); - run_server(args).await + // Generate certificates + eprintln!("Generating self-signed certificates..."); + let ca = CaCertificate::generate().map_err(|e| miette!("failed to generate CA: {e}"))?; + let server_cert = ca + .sign_server_cert("localhost") + .map_err(|e| miette!("failed to generate server cert: {e}"))?; + + // Build TLS config + let tls_config = build_tls_config(args.mode, &server_cert)?; + + // Print CA certificate for client configuration + eprintln!("CA certificate (base64 DER):"); + eprintln!("{}", base64_encode(&ca.cert_der)); + eprintln!(); + + run_server(args, tls_config).await +} + +/// Simple base64 encoding for certificate display. +fn base64_encode(data: &[u8]) -> String { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + let mut result = String::new(); + for chunk in data.chunks(3) { + let mut n = 0u32; + for (i, &byte) in chunk.iter().enumerate() { + n |= u32::from(byte) << (16 - 8 * i); + } + + for i in 0..=chunk.len() { + let idx = ((n >> (18 - 6 * i)) & 0x3F) as usize; + result.push(ALPHABET[idx] as char); + } + + for _ in chunk.len()..3 { + result.push('='); + } + } + + // Wrap at 76 characters + let mut wrapped = String::new(); + for (i, c) in result.chars().enumerate() { + if i > 0 && i % 76 == 0 { + let _ = writeln!(wrapped); + } + wrapped.push(c); + } + + wrapped }