krypteia-silentops 0.1.0

Side-channel countermeasure toolkit: constant-time primitives, dudect-style timing leakage verifier, and shared SCA helpers for the krypteia workspace.
Documentation
//! Dudect-style constant-time verification helpers.
//!
//! This module provides a small, dependency-free toolkit to detect
//! timing side-channels statistically. It implements the methodology
//! described by Reparaz, Balasch, and Verbauwhede in
//! *"dude, is my code constant time?"* (2017):
//!
//! 1. Build two **input classes**: "fixed" (worst-case) and "random".
//! 2. Interleave measurements of both classes randomly.
//! 3. Feed each measurement into a running **Welch's t-test**.
//! 4. **Verdict**: if `|t| > 4.5` after enough samples, there is
//!    strong evidence of a timing side channel (`p < 10⁻⁵`).
//!
//! The helpers are crate-agnostic: callers from `quantica` or
//! `arcana` (or any downstream user) build their own
//! measurement loops on top of [`TTest`], [`Xorshift64`], and
//! [`measure_ns`], then call [`report`] to print the verdict.
//!
//! See `silentops/examples/ct_verify_pqc.rs` for a complete usage
//! example covering ML-KEM and ML-DSA timing tests.

use std::println;
use std::time::Instant;

/// Default decision threshold: `|t| < 4.5` ⇒ no detectable timing leak.
pub const T_THRESHOLD: f64 = 4.5;

/// Welch's t-test on two streaming sample populations.
///
/// Uses Welford's incremental algorithm for the running mean and
/// variance, so it does not need to store individual samples.
#[derive(Default)]
pub struct TTest {
    n0: f64,
    n1: f64,
    mean0: f64,
    mean1: f64,
    /// Sum of squared deviations from the mean (class 0).
    m2_0: f64,
    /// Sum of squared deviations from the mean (class 1).
    m2_1: f64,
}

impl TTest {
    /// Create an empty t-test.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a new measurement. `class` must be 0 or 1.
    pub fn push(&mut self, value: f64, class: u8) {
        if class == 0 {
            self.n0 += 1.0;
            let delta = value - self.mean0;
            self.mean0 += delta / self.n0;
            let delta2 = value - self.mean0;
            self.m2_0 += delta * delta2;
        } else {
            self.n1 += 1.0;
            let delta = value - self.mean1;
            self.mean1 += delta / self.n1;
            let delta2 = value - self.mean1;
            self.m2_1 += delta * delta2;
        }
    }

    /// Compute Welch's t-statistic. Returns 0.0 if not enough samples.
    pub fn t_value(&self) -> f64 {
        if self.n0 < 2.0 || self.n1 < 2.0 {
            return 0.0;
        }
        let var0 = self.m2_0 / (self.n0 - 1.0);
        let var1 = self.m2_1 / (self.n1 - 1.0);
        let se = (var0 / self.n0 + var1 / self.n1).sqrt();
        if se == 0.0 {
            return 0.0;
        }
        (self.mean0 - self.mean1) / se
    }
}

/// Tiny xorshift64 PRNG used to interleave classes during measurement.
///
/// **Not cryptographic** — only used to schedule which class is
/// measured next and to fill random buffers in the test fixtures.
pub struct Xorshift64 {
    state: u64,
}

impl Xorshift64 {
    /// Create a PRNG seeded with `seed` (must be non-zero).
    pub fn new(seed: u64) -> Self {
        Self { state: seed }
    }

    /// Return the next 64-bit pseudo-random word.
    pub fn next(&mut self) -> u64 {
        let mut x = self.state;
        x ^= x << 13;
        x ^= x >> 7;
        x ^= x << 17;
        self.state = x;
        x
    }

    /// Return a pseudo-random boolean (used as the class selector).
    pub fn next_bool(&mut self) -> bool {
        self.next() & 1 == 1
    }

    /// Fill `buf` with pseudo-random bytes.
    pub fn fill_bytes(&mut self, buf: &mut [u8]) {
        for chunk in buf.chunks_mut(8) {
            let val = self.next();
            for (i, b) in chunk.iter_mut().enumerate() {
                *b = (val >> (i * 8)) as u8;
            }
        }
    }
}

/// Measure a closure's execution time in nanoseconds.
///
/// Marked `#[inline(never)]` to keep the measurement boundary stable
/// across optimization levels.
#[inline(never)]
pub fn measure_ns<F: FnMut()>(mut f: F) -> f64 {
    let start = Instant::now();
    f();
    start.elapsed().as_nanos() as f64
}

/// Print a single test result with PASS/FAIL verdict against
/// [`T_THRESHOLD`].
pub fn report(name: &str, t: &TTest) {
    let t_val = t.t_value();
    let verdict = if t_val.abs() < T_THRESHOLD { "PASS" } else { "FAIL" };
    println!("  {:<45} t={:>8.2}  [{}]", name, t_val, verdict);
}