refactor: rename crates

This commit is contained in:
2026-01-26 16:17:04 +02:00
parent 5accec5da4
commit d128816956
11 changed files with 50 additions and 51 deletions

125
common/src/cert.rs Normal file
View 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
View 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
View 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());
}
}