mirror of
https://github.com/kristoferssolo/tls-pq-bench.git
synced 2026-03-22 00:36:21 +00:00
feat(runner): add TLS 1.3 client with X25519
- Configure rustls client with aws_lc_rs, X25519-only key exchange - Skip certificate verification for benchmarking (NoVerifier) - Measure TLS handshake latency (TCP + TLS combined) - TLS 1.3 protocol enforced
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -680,7 +680,9 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"common",
|
"common",
|
||||||
"miette",
|
"miette",
|
||||||
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ impl fmt::Display for BenchRecord {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bench_record_serializes_to_ndjson() {
|
fn bench_record_serializes_to_ndjson() {
|
||||||
@@ -73,7 +74,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn key_exchange_mode_from_str() {
|
fn key_exchange_mode_from_str() {
|
||||||
use std::str::FromStr;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KeyExchangeMode::from_str("x25519").expect("should parse"),
|
KeyExchangeMode::from_str("x25519").expect("should parse"),
|
||||||
KeyExchangeMode::X25519
|
KeyExchangeMode::X25519
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ authors.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common.workspace = true
|
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
common.workspace = true
|
||||||
miette.workspace = true
|
miette.workspace = true
|
||||||
|
rustls.workspace = true
|
||||||
|
tokio-rustls.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -12,14 +12,23 @@ use common::{
|
|||||||
protocol::{read_payload, write_request},
|
protocol::{read_payload, write_request},
|
||||||
};
|
};
|
||||||
use miette::miette;
|
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::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufWriter, Write, stdout},
|
io::{BufWriter, Write, stdout},
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_rustls::TlsConnector;
|
||||||
|
|
||||||
/// TLS benchmark runner.
|
/// TLS benchmark runner.
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -60,25 +69,108 @@ struct IterationResult {
|
|||||||
ttlb_ns: u64,
|
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<ServerCertVerified, rustls::Error> {
|
||||||
|
Ok(ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_tls12_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_tls13_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||||
|
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<Arc<ClientConfig>> {
|
||||||
|
// 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
|
#[allow(clippy::cast_possible_truncation)] // nanoseconds won't overflow u64 for reasonable durations
|
||||||
async fn run_iteration(server: SocketAddr, payload_bytes: u64) -> miette::Result<IterationResult> {
|
async fn run_iteration(
|
||||||
|
server: SocketAddr,
|
||||||
|
payload_bytes: u64,
|
||||||
|
tls_connector: &TlsConnector,
|
||||||
|
server_name: &ServerName<'static>,
|
||||||
|
) -> miette::Result<IterationResult> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Connect (this is the "handshake" for plain TCP)
|
// TCP connect
|
||||||
let mut stream = TcpStream::connect(server)
|
let stream = TcpStream::connect(server)
|
||||||
.await
|
.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;
|
let handshake_ns = start.elapsed().as_nanos() as u64;
|
||||||
|
|
||||||
// Send request
|
// Send request
|
||||||
write_request(&mut stream, payload_bytes)
|
write_request(&mut tls_stream, payload_bytes)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| miette!("write request failed: {e}"))?;
|
.map_err(|e| miette!("write request failed: {e}"))?;
|
||||||
|
|
||||||
// Read response
|
// Read response
|
||||||
read_payload(&mut stream, payload_bytes)
|
read_payload(&mut tls_stream, payload_bytes)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| miette!("read payload failed: {e}"))?;
|
.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;
|
let total_iters = args.warmup + args.iters;
|
||||||
|
|
||||||
// Open output file or use stdout
|
// Open output file or use stdout
|
||||||
@@ -104,7 +200,7 @@ async fn run_benchmark(args: Args) -> miette::Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Running {} warmup + {} measured iterations (concurrency: {}, TLS disabled)",
|
"Running {} warmup + {} measured iterations (concurrency: {}, TLS 1.3)",
|
||||||
args.warmup, args.iters, args.concurrency
|
args.warmup, args.iters, args.concurrency
|
||||||
);
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
@@ -113,7 +209,13 @@ async fn run_benchmark(args: Args) -> miette::Result<()> {
|
|||||||
for i in 0..total_iters {
|
for i in 0..total_iters {
|
||||||
let is_warmup = i < args.warmup;
|
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 {
|
if !is_warmup {
|
||||||
let record = BenchRecord {
|
let record = BenchRecord {
|
||||||
@@ -159,5 +261,13 @@ async fn main() -> miette::Result<()> {
|
|||||||
);
|
);
|
||||||
eprintln!();
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ authors.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common.workspace = true
|
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
common.workspace = true
|
||||||
miette.workspace = true
|
miette.workspace = true
|
||||||
rustls.workspace = true
|
rustls.workspace = true
|
||||||
tokio.workspace = true
|
|
||||||
tokio-rustls.workspace = true
|
tokio-rustls.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use common::{
|
|||||||
use miette::miette;
|
use miette::miette;
|
||||||
use rustls::{
|
use rustls::{
|
||||||
ServerConfig,
|
ServerConfig,
|
||||||
crypto::aws_lc_rs::{self, kx_group},
|
crypto::aws_lc_rs::{self, kx_group::X25519},
|
||||||
pki_types::{CertificateDer, PrivateKeyDer},
|
pki_types::{CertificateDer, PrivateKeyDer},
|
||||||
server::Acceptor,
|
server::Acceptor,
|
||||||
version::TLS13,
|
version::TLS13,
|
||||||
@@ -46,7 +46,7 @@ fn build_tls_config(
|
|||||||
// Select crypto provider with appropriate key exchange groups
|
// Select crypto provider with appropriate key exchange groups
|
||||||
let mut provider = aws_lc_rs::default_provider();
|
let mut provider = aws_lc_rs::default_provider();
|
||||||
provider.kx_groups = match mode {
|
provider.kx_groups = match mode {
|
||||||
KeyExchangeMode::X25519 => vec![kx_group::X25519],
|
KeyExchangeMode::X25519 => vec![X25519],
|
||||||
KeyExchangeMode::X25519Mlkem768 => {
|
KeyExchangeMode::X25519Mlkem768 => {
|
||||||
todo!("Configure hybrid PQ key exchange")
|
todo!("Configure hybrid PQ key exchange")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user