From 36c99c020e968c9d0ac9c418317d0ab7b3ba938f Mon Sep 17 00:00:00 2001 From: Ilka Schulz Date: Wed, 28 Feb 2024 12:00:24 +0100 Subject: [PATCH] implement test to statistically check constant run time of memcmp (feature: constant_time_tests) --- Cargo.lock | 1 + constant-time/Cargo.toml | 6 +++ constant-time/src/lib.rs | 89 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 657d11d..ddd651e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,6 +1168,7 @@ name = "rosenpass-constant-time" version = "0.1.0" dependencies = [ "memsec", + "rand", "rosenpass-to", ] diff --git a/constant-time/Cargo.toml b/constant-time/Cargo.toml index 497f0f4..b07e4ef 100644 --- a/constant-time/Cargo.toml +++ b/constant-time/Cargo.toml @@ -11,6 +11,12 @@ readme = "readme.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +constant_time_tests = [] + [dependencies] rosenpass-to = { workspace = true } memsec = { workspace = true } + +[dev-dependencies] +rand = "0.8.5" diff --git a/constant-time/src/lib.rs b/constant-time/src/lib.rs index b5ea516..1d19563 100644 --- a/constant-time/src/lib.rs +++ b/constant-time/src/lib.rs @@ -77,3 +77,92 @@ pub fn increment(v: &mut [u8]) { *black_box(&mut carry) = black_box(black_box(c) as u8); } } + +#[cfg(all(test, feature = "constant_time_tests"))] +mod constant_time_tests { + use super::*; + use rand::seq::SliceRandom; + use rand::thread_rng; + use std::time::Instant; + + #[test] + /// tests whether [memcmp] actually runs in constant time + /// + /// This test function will run an equal amount of comparisons on two different sets of parameters: + /// - completely equal slices + /// - completely unequal slices. + /// All comparisons are executed in a randomized order. The test will fail if one of the + /// two sets is checked for equality significantly faster than the other set + /// (absolute correlation coefficient ≥ 0.01) + fn memcmp_runs_in_constant_time() { + // prepare data to compare + let n: usize = 1E6 as usize; // number of comparisons to run + let len = 1024; // length of each slice passed as parameters to the tested comparison function + let a1 = "a".repeat(len); + let a2 = a1.clone(); + let b = "b".repeat(len); + + let a1 = a1.as_bytes(); + let a2 = a2.as_bytes(); + let b = b.as_bytes(); + + // vector representing all timing tests + // + // Each element is a tuple of: + // 0: whether the test compared two equal slices + // 1: the duration needed for the comparison to run + let mut tests = (0..n) + .map(|i| (i < n / 2, std::time::Duration::ZERO)) + .collect::>(); + tests.shuffle(&mut thread_rng()); + + // run comparisons / call function to test + for test in tests.iter_mut() { + let now = Instant::now(); + if test.0 { + memcmp(a1, a2); + } else { + memcmp(a1, b); + } + test.1 = now.elapsed(); + // println!("eq: {}, elapsed: {:.2?}", test.0, test.1); + } + + // sort by execution time and calculate Pearson correlation coefficient + tests.sort_by_key(|v| v.1); + let tests = tests + .iter() + .map(|t| (if t.0 { 1_f64 } else { 0_f64 }, t.1.as_nanos() as f64)) + .collect::>(); + // averages + let (avg_x, avg_y): (f64, f64) = ( + tests.iter().map(|t| t.0).sum::() / n as f64, + tests.iter().map(|t| t.1).sum::() / n as f64, + ); + assert!((avg_x - 0.5).abs() < 1E-12); + // standard deviations + let sd_x = 0.5; + let sd_y = (1_f64 / n as f64 + * tests + .iter() + .map(|t| { + let difference = t.1 - avg_y; + difference * difference + }) + .sum::()) + .sqrt(); + // covariance + let cv = 1_f64 / n as f64 + * tests + .iter() + .map(|t| (t.0 - avg_x) * (t.1 - avg_y)) + .sum::(); + // Pearson correlation + let correlation = cv / (sd_x * sd_y); + println!("correlation: {:.6?}", correlation); + assert!( + correlation.abs() < 0.01, + "execution time correlates with result" + ) + } +}