mirror of
https://github.com/kristoferssolo/tls-pq-bench.git
synced 2026-03-22 00:36:21 +00:00
refactor: rename crates
This commit is contained in:
15
common/Cargo.toml
Normal file
15
common/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "common"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rcgen.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
125
common/src/cert.rs
Normal file
125
common/src/cert.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//! Self-signed certificate generation for local testing.
|
||||
//!
|
||||
//! Generates a CA certificate and server certificate for TLS benchmarking.
|
||||
//! These certificates are NOT suitable for production use.
|
||||
|
||||
use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, Issuer, KeyPair, SanType};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
/// Generated certificate material for TLS server.
|
||||
#[derive(Clone)]
|
||||
pub struct ServerCertificate {
|
||||
/// DER-encoded certificate chain (server cert, then CA cert).
|
||||
pub cert_chain_der: Vec<Vec<u8>>,
|
||||
/// DER-encoded private key.
|
||||
pub private_key_der: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Generated CA certificate for client verification.
|
||||
pub struct CaCertificate {
|
||||
/// DER-encoded CA certificate.
|
||||
pub cert_der: Vec<u8>,
|
||||
/// The CA key pair for signing.
|
||||
key_pair: KeyPair,
|
||||
/// The CA certificate params for creating an Issuer.
|
||||
params: CertificateParams,
|
||||
}
|
||||
|
||||
impl CaCertificate {
|
||||
/// Generate a new self-signed CA certificate.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if certificate generation fails.
|
||||
pub fn generate() -> Result<Self, rcgen::Error> {
|
||||
let mut params = CertificateParams::default();
|
||||
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "tls-pq-bench CA");
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::OrganizationName, "tls-pq-bench");
|
||||
|
||||
let key_pair = KeyPair::generate()?;
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
|
||||
Ok(Self {
|
||||
cert_der: cert.der().to_vec(),
|
||||
key_pair,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a server certificate signed by this CA.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `server_name` - The server's DNS name (e.g., "localhost").
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if certificate generation fails.
|
||||
pub fn sign_server_cert(&self, server_name: &str) -> Result<ServerCertificate, rcgen::Error> {
|
||||
let mut params = CertificateParams::default();
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, server_name);
|
||||
params.subject_alt_names = vec![
|
||||
SanType::DnsName(server_name.try_into()?),
|
||||
SanType::DnsName("localhost".try_into()?),
|
||||
SanType::IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
SanType::IpAddress(IpAddr::V6(Ipv6Addr::LOCALHOST)),
|
||||
];
|
||||
|
||||
let server_key = KeyPair::generate()?;
|
||||
let issuer = Issuer::from_params(&self.params, &self.key_pair);
|
||||
let server_cert = params.signed_by(&server_key, &issuer)?;
|
||||
|
||||
Ok(ServerCertificate {
|
||||
cert_chain_der: vec![server_cert.der().to_vec(), self.cert_der.clone()],
|
||||
private_key_der: server_key.serialize_der(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a complete certificate pair (CA + server) for testing.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `server_name` - The server's DNS name (e.g., "localhost").
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if certificate generation fails.
|
||||
pub fn generate_test_certs(
|
||||
server_name: &str,
|
||||
) -> Result<(CaCertificate, ServerCertificate), rcgen::Error> {
|
||||
let ca = CaCertificate::generate()?;
|
||||
let server = ca.sign_server_cert(server_name)?;
|
||||
Ok((ca, server))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_ca_certificate() {
|
||||
let ca = CaCertificate::generate().expect("CA generation should succeed");
|
||||
assert!(!ca.cert_der.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_server_certificate() {
|
||||
let ca = CaCertificate::generate().expect("CA generation should succeed");
|
||||
let server = ca
|
||||
.sign_server_cert("localhost")
|
||||
.expect("server cert generation should succeed");
|
||||
assert_eq!(server.cert_chain_der.len(), 2);
|
||||
assert!(!server.private_key_der.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_test_certs_helper() {
|
||||
let (ca, server) =
|
||||
generate_test_certs("test.local").expect("test cert generation should succeed");
|
||||
assert!(!ca.cert_der.is_empty());
|
||||
assert_eq!(server.cert_chain_der.len(), 2);
|
||||
}
|
||||
}
|
||||
86
common/src/lib.rs
Normal file
86
common/src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! Common types and utilities for the TLS benchmark harness.
|
||||
|
||||
pub mod cert;
|
||||
pub mod protocol;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
/// TLS key exchange mode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString, Display)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum KeyExchangeMode {
|
||||
/// Classical X25519 ECDH.
|
||||
X25519,
|
||||
/// Hybrid post-quantum: X25519 + ML-KEM-768.
|
||||
X25519Mlkem768,
|
||||
}
|
||||
|
||||
/// A single benchmark measurement record, output as NDJSON.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BenchRecord {
|
||||
/// Iteration number (0-indexed, excludes warmup).
|
||||
pub iteration: u64,
|
||||
/// Key exchange mode used.
|
||||
pub mode: KeyExchangeMode,
|
||||
/// Payload size in bytes.
|
||||
pub payload_bytes: u64,
|
||||
/// Handshake latency in nanoseconds.
|
||||
pub handshake_ns: u64,
|
||||
/// Time-to-last-byte in nanoseconds (from connection start).
|
||||
pub ttlb_ns: u64,
|
||||
}
|
||||
|
||||
impl BenchRecord {
|
||||
/// Serialize this record as a single NDJSON line (no trailing newline).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if serialization fails.
|
||||
pub fn to_ndjson(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BenchRecord {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.to_ndjson() {
|
||||
Ok(json) => write!(f, "{json}"),
|
||||
Err(e) => write!(f, r#"{{"error": "{e}"}}"#),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bench_record_serializes_to_ndjson() {
|
||||
let record = BenchRecord {
|
||||
iteration: 0,
|
||||
mode: KeyExchangeMode::X25519,
|
||||
payload_bytes: 1024,
|
||||
handshake_ns: 1_000_000,
|
||||
ttlb_ns: 2_000_000,
|
||||
};
|
||||
let json = record.to_ndjson().expect("serialization should succeed");
|
||||
assert!(json.contains(r#""iteration":0"#));
|
||||
assert!(json.contains(r#""mode":"x25519""#));
|
||||
assert!(json.contains(r#""payload_bytes":1024"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_exchange_mode_from_str() {
|
||||
use std::str::FromStr;
|
||||
assert_eq!(
|
||||
KeyExchangeMode::from_str("x25519").expect("should parse"),
|
||||
KeyExchangeMode::X25519
|
||||
);
|
||||
assert_eq!(
|
||||
KeyExchangeMode::from_str("x25519mlkem768").expect("should parse"),
|
||||
KeyExchangeMode::X25519Mlkem768
|
||||
);
|
||||
}
|
||||
}
|
||||
145
common/src/protocol.rs
Normal file
145
common/src/protocol.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
//! Benchmark protocol implementation.
|
||||
//!
|
||||
//! Protocol specification:
|
||||
//! 1. Client sends 8-byte little-endian u64: requested payload size N
|
||||
//! 2. Server responds with exactly N bytes (deterministic pattern)
|
||||
//!
|
||||
//! The deterministic pattern is a repeating sequence of bytes 0x00..0xFF.
|
||||
|
||||
// Casts are intentional: MAX_PAYLOAD_SIZE (16 MiB) fits in usize on 64-bit,
|
||||
// and byte patterns are explicitly masked to 0xFF before casting.
|
||||
#![allow(clippy::cast_possible_truncation)]
|
||||
|
||||
use std::io;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
/// Size of the request header (u64 payload size).
|
||||
pub const REQUEST_SIZE: usize = 8;
|
||||
|
||||
/// Maximum allowed payload size (16 MiB).
|
||||
pub const MAX_PAYLOAD_SIZE: u64 = 16 * 1024 * 1024;
|
||||
|
||||
/// Read the payload size request from a stream.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if reading fails or payload size exceeds maximum.
|
||||
pub async fn read_request<R: AsyncReadExt + Unpin>(reader: &mut R) -> io::Result<u64> {
|
||||
let mut buf = [0u8; REQUEST_SIZE];
|
||||
reader.read_exact(&mut buf).await?;
|
||||
let size = u64::from_le_bytes(buf);
|
||||
|
||||
if size > MAX_PAYLOAD_SIZE {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("payload size {size} exceeds maximum {MAX_PAYLOAD_SIZE}"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// Write a payload size request to a stream.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if writing fails.
|
||||
pub async fn write_request<W: AsyncWriteExt + Unpin>(writer: &mut W, size: u64) -> io::Result<()> {
|
||||
let buf = size.to_le_bytes();
|
||||
writer.write_all(&buf).await
|
||||
}
|
||||
|
||||
/// Generate deterministic payload of the given size.
|
||||
///
|
||||
/// The pattern is a repeating sequence: 0x00, 0x01, ..., 0xFF, 0x00, ...
|
||||
#[must_use]
|
||||
pub fn generate_payload(size: u64) -> Vec<u8> {
|
||||
let size = size as usize;
|
||||
let mut payload = Vec::with_capacity(size);
|
||||
for i in 0..size {
|
||||
payload.push((i & 0xFF) as u8);
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
/// Write deterministic payload to a stream.
|
||||
///
|
||||
/// Writes in chunks to avoid allocating large buffers.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if writing fails.
|
||||
pub async fn write_payload<W: AsyncWriteExt + Unpin>(writer: &mut W, size: u64) -> io::Result<()> {
|
||||
const CHUNK_SIZE: usize = 64 * 1024;
|
||||
let mut remaining = size as usize;
|
||||
let mut offset = 0usize;
|
||||
|
||||
while remaining > 0 {
|
||||
let chunk_len = remaining.min(CHUNK_SIZE);
|
||||
let chunk: Vec<u8> = (0..chunk_len)
|
||||
.map(|i| ((offset + i) & 0xFF) as u8)
|
||||
.collect();
|
||||
writer.write_all(&chunk).await?;
|
||||
remaining -= chunk_len;
|
||||
offset += chunk_len;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read and discard payload from a stream, returning the number of bytes read.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if reading fails.
|
||||
pub async fn read_payload<R: AsyncReadExt + Unpin>(
|
||||
reader: &mut R,
|
||||
expected_size: u64,
|
||||
) -> io::Result<u64> {
|
||||
const CHUNK_SIZE: usize = 64 * 1024;
|
||||
let mut buf = vec![0u8; CHUNK_SIZE];
|
||||
let mut total_read = 0u64;
|
||||
|
||||
while total_read < expected_size {
|
||||
let to_read = ((expected_size - total_read) as usize).min(CHUNK_SIZE);
|
||||
reader.read_exact(&mut buf[..to_read]).await?;
|
||||
total_read += to_read as u64;
|
||||
}
|
||||
|
||||
Ok(total_read)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn generate_payload_pattern() {
|
||||
let payload = generate_payload(300);
|
||||
assert_eq!(payload.len(), 300);
|
||||
assert_eq!(payload[0], 0x00);
|
||||
assert_eq!(payload[255], 0xFF);
|
||||
assert_eq!(payload[256], 0x00);
|
||||
assert_eq!(payload[299], 43);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn roundtrip_request() {
|
||||
let mut buf = Vec::new();
|
||||
write_request(&mut buf, 12345)
|
||||
.await
|
||||
.expect("write should succeed");
|
||||
assert_eq!(buf.len(), REQUEST_SIZE);
|
||||
|
||||
let mut cursor = Cursor::new(buf);
|
||||
let size = read_request(&mut cursor)
|
||||
.await
|
||||
.expect("read should succeed");
|
||||
assert_eq!(size, 12345);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reject_oversized_request() {
|
||||
let buf = (MAX_PAYLOAD_SIZE + 1).to_le_bytes();
|
||||
let mut cursor = Cursor::new(buf);
|
||||
let result = read_request(&mut cursor).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user