#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha8Rng;
use sntrup761::*;
use std::hint::black_box;
use std::time::Instant;
const SAMPLES_PER_CLASS: usize = 2000;
const WARMUP_ITERS: usize = 500;
const T_THRESHOLD: f64 = 4.5;
const TRIM_LO: f64 = 0.05; const TRIM_HI: f64 = 0.95;
fn trim_outliers(data: &[f64]) -> Vec<f64> {
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).expect("NaN in timing data"));
let lo = (sorted.len() as f64 * TRIM_LO) as usize;
let hi = (sorted.len() as f64 * TRIM_HI) as usize;
sorted[lo..hi].to_vec()
}
fn mean(data: &[f64]) -> f64 {
let n = data.len() as f64;
data.iter().sum::<f64>() / n
}
fn variance(data: &[f64]) -> f64 {
let m = mean(data);
let n = data.len() as f64;
data.iter().map(|&x| (x - m) * (x - m)).sum::<f64>() / (n - 1.0)
}
fn welch_t(a: &[f64], b: &[f64]) -> f64 {
let mean_a = mean(a);
let mean_b = mean(b);
let var_a = variance(a);
let var_b = variance(b);
let na = a.len() as f64;
let nb = b.len() as f64;
let se = (var_a / na + var_b / nb).sqrt();
if se == 0.0 {
return 0.0;
}
(mean_a - mean_b) / se
}
fn measure_interleaved<A, B, FA, FB>(
name: &str,
inputs_a: &[A],
inputs_b: &[B],
run_a: FA,
run_b: FB,
) -> bool
where
FA: Fn(&A),
FB: Fn(&B),
{
assert!(inputs_a.len() >= SAMPLES_PER_CLASS);
assert!(inputs_b.len() >= SAMPLES_PER_CLASS);
let total = SAMPLES_PER_CLASS * 2;
let mut schedule: Vec<(bool, usize)> = Vec::with_capacity(total);
for i in 0..SAMPLES_PER_CLASS {
schedule.push((true, i)); schedule.push((false, i)); }
let mut rng = ChaCha8Rng::seed_from_u64(0xDEAD_BEEF_CAFE_1234);
for i in (1..schedule.len()).rev() {
let j = rng.random_range(0..=i);
schedule.swap(i, j);
}
for i in 0..WARMUP_ITERS {
let idx = i % inputs_a.len().min(inputs_b.len());
run_a(black_box(&inputs_a[idx]));
run_b(black_box(&inputs_b[idx]));
}
let mut times_a = Vec::with_capacity(SAMPLES_PER_CLASS);
let mut times_b = Vec::with_capacity(SAMPLES_PER_CLASS);
for &(is_class_a, idx) in &schedule {
if is_class_a {
let start = Instant::now();
run_a(black_box(&inputs_a[idx]));
let elapsed = start.elapsed().as_nanos() as f64;
times_a.push(black_box(elapsed));
} else {
let start = Instant::now();
run_b(black_box(&inputs_b[idx]));
let elapsed = start.elapsed().as_nanos() as f64;
times_b.push(black_box(elapsed));
}
}
let trimmed_a = trim_outliers(×_a);
let trimmed_b = trim_outliers(×_b);
let mean_a = mean(&trimmed_a);
let mean_b = mean(&trimmed_b);
let std_a = variance(&trimmed_a).sqrt();
let std_b = variance(&trimmed_b).sqrt();
let t = welch_t(&trimmed_a, &trimmed_b);
let pass = t.abs() < T_THRESHOLD;
eprintln!("--- {name} ---");
eprintln!(
" Class A: mean = {mean_a:.1} ns, stddev = {std_a:.1} ns (n = {})",
trimmed_a.len()
);
eprintln!(
" Class B: mean = {mean_b:.1} ns, stddev = {std_b:.1} ns (n = {})",
trimmed_b.len()
);
eprintln!(" t-statistic = {t:.4} (threshold = +/-{T_THRESHOLD})");
eprintln!(" Result: {}", if pass { "PASS" } else { "FAIL" });
eprintln!();
pass
}
fn make_seed(base: u8, index: usize) -> [u8; 32] {
let mut seed = [base; 32];
let idx_bytes = (index as u64).to_le_bytes();
seed[..8].copy_from_slice(&idx_bytes);
seed
}
#[cfg(all(feature = "kgen", feature = "ecap", feature = "dcap"))]
#[test]
#[ignore]
fn timing_decap_valid_vs_invalid() {
let (pk, sk) = generate_key_from_seed([0x11; 32]);
let valid_cts: Vec<Ciphertext> = (0..SAMPLES_PER_CLASS)
.map(|i| {
let seed = make_seed(0xAA, i);
let (ct, _ss) = pk.encapsulate_deterministic(seed);
ct
})
.collect();
let invalid_cts: Vec<Ciphertext> = (0..SAMPLES_PER_CLASS)
.map(|i| {
let seed = make_seed(0xBB, i);
let (ct, _ss) = pk.encapsulate_deterministic(seed);
let mut bytes = [0u8; CIPHERTEXT_SIZE];
bytes.copy_from_slice(ct.as_ref());
bytes[0] ^= 0xFF;
bytes[100] ^= 0x42;
bytes[500] ^= 0x13;
bytes[900] ^= 0x7E;
Ciphertext::from(bytes)
})
.collect();
let pass = measure_interleaved(
"decap valid vs invalid (bit-flipped)",
&valid_cts,
&invalid_cts,
|ct| {
let _ = black_box(sk.decapsulate(black_box(ct)));
},
|ct| {
let _ = black_box(sk.decapsulate(black_box(ct)));
},
);
assert!(
pass,
"Timing difference detected between valid and invalid ciphertexts!"
);
}
#[cfg(all(feature = "kgen", feature = "ecap", feature = "dcap"))]
#[test]
#[ignore]
fn timing_decap_valid_vs_garbage() {
let (pk, sk) = generate_key_from_seed([0x22; 32]);
let valid_cts: Vec<Ciphertext> = (0..SAMPLES_PER_CLASS)
.map(|i| {
let seed = make_seed(0xCC, i);
let (ct, _ss) = pk.encapsulate_deterministic(seed);
ct
})
.collect();
let garbage_cts: Vec<Ciphertext> = (0..SAMPLES_PER_CLASS)
.map(|i| {
let pattern = ((i % 256) as u8).wrapping_add(0x10);
let mut bytes = [pattern; CIPHERTEXT_SIZE];
let idx_bytes = (i as u64).to_le_bytes();
bytes[..8].copy_from_slice(&idx_bytes);
Ciphertext::from(bytes)
})
.collect();
let pass = measure_interleaved(
"decap valid vs garbage",
&valid_cts,
&garbage_cts,
|ct| {
let _ = black_box(sk.decapsulate(black_box(ct)));
},
|ct| {
let _ = black_box(sk.decapsulate(black_box(ct)));
},
);
assert!(
pass,
"Timing difference detected between valid and garbage ciphertexts!"
);
}
#[cfg(all(feature = "kgen", feature = "ecap", feature = "dcap"))]
#[test]
#[ignore]
fn timing_decap_different_valid() {
let (pk, sk) = generate_key_from_seed([0x33; 32]);
let cts_a: Vec<Ciphertext> = (0..SAMPLES_PER_CLASS)
.map(|i| {
let seed = make_seed(0xDD, i);
let (ct, _ss) = pk.encapsulate_deterministic(seed);
ct
})
.collect();
let cts_b: Vec<Ciphertext> = (0..SAMPLES_PER_CLASS)
.map(|i| {
let seed = make_seed(0xEE, i);
let (ct, _ss) = pk.encapsulate_deterministic(seed);
ct
})
.collect();
let pass = measure_interleaved(
"decap different valid CTs (group A vs group B)",
&cts_a,
&cts_b,
|ct| {
let _ = black_box(sk.decapsulate(black_box(ct)));
},
|ct| {
let _ = black_box(sk.decapsulate(black_box(ct)));
},
);
assert!(
pass,
"Timing difference detected between different valid ciphertexts!"
);
}
#[cfg(feature = "kgen")]
#[test]
#[ignore]
fn timing_keygen_different_seeds() {
let seeds_a: Vec<[u8; 32]> = (0..SAMPLES_PER_CLASS).map(|i| make_seed(0xA0, i)).collect();
let seeds_b: Vec<[u8; 32]> = (0..SAMPLES_PER_CLASS).map(|i| make_seed(0xB0, i)).collect();
let pass = measure_interleaved(
"keygen different seeds (group A vs group B)",
&seeds_a,
&seeds_b,
|seed| {
let _ = black_box(generate_key_from_seed(black_box(*seed)));
},
|seed| {
let _ = black_box(generate_key_from_seed(black_box(*seed)));
},
);
assert!(
pass,
"Timing difference detected between different keygen seeds!"
);
}
#[cfg(all(feature = "kgen", feature = "ecap"))]
#[test]
#[ignore]
fn timing_encap_different_seeds() {
let (pk, _sk) = generate_key_from_seed([0x55; 32]);
let seeds_a: Vec<[u8; 32]> = (0..SAMPLES_PER_CLASS).map(|i| make_seed(0xC0, i)).collect();
let seeds_b: Vec<[u8; 32]> = (0..SAMPLES_PER_CLASS).map(|i| make_seed(0xD0, i)).collect();
let pass = measure_interleaved(
"encap different seeds (group A vs group B)",
&seeds_a,
&seeds_b,
|seed| {
let _ = black_box(pk.encapsulate_deterministic(black_box(*seed)));
},
|seed| {
let _ = black_box(pk.encapsulate_deterministic(black_box(*seed)));
},
);
assert!(
pass,
"Timing difference detected between different encap seeds!"
);
}