Skip to main content

lib_q_sca_test/
lib.rs

1//! Statistical helpers for TVLA-style and timing-based leakage smoke tests.
2#![forbid(unsafe_code)]
3
4pub mod dudect;
5pub mod evaluation;
6pub mod tvla_synthetic;
7
8#[cfg(feature = "privacy")]
9pub mod privacy_workloads;
10
11/// Configuration for TVLA-style *t*-tests.
12#[derive(Clone, Debug)]
13pub struct TvlaConfig {
14    /// Absolute *t* threshold (e.g. 4.5 for first-order screening).
15    pub abs_t_threshold: f64,
16}
17
18impl Default for TvlaConfig {
19    fn default() -> Self {
20        Self {
21            abs_t_threshold: 4.5,
22        }
23    }
24}
25
26/// Welch’s *t*-statistic for two samples (unequal variances).
27pub fn welch_t_statistic(a: &[f64], b: &[f64]) -> Option<f64> {
28    let na = a.len() as f64;
29    let nb = b.len() as f64;
30    if na < 2.0 || nb < 2.0 {
31        return None;
32    }
33    let mean_a = a.iter().sum::<f64>() / na;
34    let mean_b = b.iter().sum::<f64>() / nb;
35    let var_a = a.iter().map(|x| (x - mean_a).powi(2)).sum::<f64>() / (na - 1.0);
36    let var_b = b.iter().map(|x| (x - mean_b).powi(2)).sum::<f64>() / (nb - 1.0);
37    let se = (var_a / na + var_b / nb).sqrt();
38    if se == 0.0 {
39        return None;
40    }
41    Some((mean_a - mean_b) / se)
42}
43
44/// Returns `true` if \\(|t| < \\) `cfg.abs_t_threshold`.
45pub fn tvla_passes(cfg: &TvlaConfig, fixed: &[f64], random: &[f64]) -> bool {
46    match welch_t_statistic(fixed, random) {
47        Some(t) => t.abs() < cfg.abs_t_threshold,
48        None => false,
49    }
50}
51
52/// Collect `n` timing samples using `std::time::Instant` (wall clock).
53pub fn sample_wall_times<F: FnMut()>(mut f: F, n: usize) -> Vec<f64> {
54    let mut out = Vec::with_capacity(n);
55    for _ in 0..n {
56        let t0 = std::time::Instant::now();
57        f();
58        out.push(t0.elapsed().as_secs_f64());
59    }
60    out
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn welch_near_identical_means_small_t() {
69        let a: Vec<f64> = (0..100).map(|i| i as f64 * 1e-9).collect();
70        let b: Vec<f64> = (0..100).map(|i| i as f64 * 1e-9 + 1e-12).collect();
71        let t = welch_t_statistic(&a, &b).expect("t");
72        assert!(t.abs() < 4.5, "t={t}");
73    }
74
75    #[test]
76    fn tvla_config_default_threshold() {
77        let cfg = TvlaConfig::default();
78        let fixed = vec![1.0, 1.01, 0.99, 1.02];
79        let random = vec![1.0, 1.0, 1.0, 1.0];
80        assert!(tvla_passes(&cfg, &fixed, &random));
81    }
82}
83
84#[cfg(all(test, feature = "mlkem"))]
85mod mlkem_smoke {
86    use lib_q_ml_kem::{
87        Decapsulate,
88        Encapsulate,
89        KemCore,
90        MlKem768,
91    };
92
93    #[test]
94    fn kem_round_trip_hardened() {
95        let mut rng = lib_q_random::LibQRng::new_secure().expect("secure rng");
96        let (dk, ek) = MlKem768::generate(&mut rng);
97        let (ct, ss1) = ek.encapsulate(&mut rng).unwrap();
98        let ss2 = dk.decapsulate(&ct).unwrap();
99        assert_eq!(ss1, ss2);
100    }
101}
102
103#[cfg(all(test, feature = "privacy"))]
104mod privacy_smoke {
105    use lib_q_lattice_zkp::sigma::opening::sample_random_opening;
106    use lib_q_lattice_zkp::{
107        AjtaiCommitmentKey,
108        AjtaiOpening,
109        AjtaiParameters,
110        BlindIssuance,
111        BlindRequest,
112        commit,
113    };
114    use lib_q_ring::{
115        ModuleVec,
116        Poly,
117    };
118    use lib_q_ring_sig::{
119        MemberIssuerKey,
120        RingSigParams,
121        sign_federation_message,
122    };
123    use rand_chacha::ChaCha8Rng;
124    use rand_core::SeedableRng;
125
126    use crate::privacy_workloads::{
127        touch_blind_verify,
128        touch_federation_digest,
129        touch_federation_verify,
130        touch_nullifier,
131        touch_witness_nullifier,
132    };
133
134    #[inline]
135    fn test_seed32(tag: u64) -> [u8; 32] {
136        let mut seed = [0u8; 32];
137        seed[0..8].copy_from_slice(&tag.to_le_bytes());
138        seed
139    }
140
141    #[test]
142    fn privacy_workloads_run() {
143        let key = AjtaiCommitmentKey {
144            seed: [0x55u8; 32],
145            params: AjtaiParameters::new(2, 1),
146        };
147        let o = AjtaiOpening {
148            message: ModuleVec(vec![Poly::zero(), Poly::zero()]),
149            randomness: ModuleVec(vec![Poly::zero()]),
150        };
151        let c = commit(&key, &o);
152        let c2 = commit(&key, &o);
153        let _ = touch_nullifier(&c, b"tvla-realm");
154        let _ = touch_witness_nullifier(&o, b"tvla-realm");
155        let _ = touch_federation_digest(core::slice::from_ref(&c));
156        let _ = touch_federation_digest(&[c, c2]);
157    }
158
159    #[test]
160    fn touch_blind_verify_accepts_round_trip_bundle() {
161        let key = AjtaiCommitmentKey {
162            seed: [0x6Au8; 32],
163            params: AjtaiParameters::new(2, 1),
164        };
165        let p = RingSigParams::mldsa65_pilot();
166
167        let user_opening = AjtaiOpening {
168            message: ModuleVec(vec![Poly::zero(), Poly::zero()]),
169            randomness: ModuleVec(vec![Poly::zero()]),
170        };
171        let mut rng = ChaCha8Rng::from_seed(test_seed32(0x10A5_BEEF_u64));
172        let (_req, st) =
173            BlindIssuance::request(&mut rng, &key, user_opening).expect("blind request");
174        let issuer_opening = sample_random_opening(&mut rng, &key);
175        let blind_req = BlindRequest {
176            com_blinded: st.com_blinded.clone(),
177        };
178        let resp = BlindIssuance::issuer_sign(
179            &mut rng,
180            &key,
181            &blind_req,
182            &issuer_opening,
183            b"sca-blind-realm",
184            p.tau,
185            p.z_inf_bound,
186            p.max_prove_attempts,
187        )
188        .expect("issuer sign");
189        let bundle = BlindIssuance::finalize(st, resp).expect("finalize");
190
191        touch_blind_verify(&key, &bundle, b"sca-blind-realm", p.tau, p.z_inf_bound)
192            .expect("blind verify workload");
193    }
194
195    #[test]
196    fn touch_federation_verify_accepts_signed_message() {
197        let key = AjtaiCommitmentKey {
198            seed: [0x77u8; 32],
199            params: AjtaiParameters::new(2, 1),
200        };
201        let p = RingSigParams::mldsa65_pilot();
202        let mut rng = ChaCha8Rng::from_seed(test_seed32(0xFEED_FACE_u64));
203
204        let a = MemberIssuerKey::from_opening(
205            &key,
206            AjtaiOpening {
207                message: ModuleVec(vec![Poly::zero(), Poly::zero()]),
208                randomness: ModuleVec(vec![Poly::zero()]),
209            },
210        )
211        .expect("member a");
212        let mut m_b = vec![Poly::zero(), Poly::zero()];
213        m_b[0].coeffs[0] = 4;
214        let b = MemberIssuerKey::from_opening(
215            &key,
216            AjtaiOpening {
217                message: ModuleVec(m_b),
218                randomness: ModuleVec(vec![Poly::zero()]),
219            },
220        )
221        .expect("member b");
222        let ring = [a.commitment.clone(), b.commitment.clone()];
223        let proof = sign_federation_message(
224            &mut rng,
225            &key,
226            &b.opening,
227            &b.commitment,
228            &ring,
229            b"sca-fed-msg",
230            p.tau,
231            p.z_inf_bound,
232            p.max_prove_attempts,
233        )
234        .expect("sign federation");
235
236        touch_federation_verify(&key, &ring, 1, b"sca-fed-msg", &proof, p.tau, p.z_inf_bound)
237            .expect("federation verify workload");
238    }
239}
240
241#[cfg(all(test, feature = "mldsa"))]
242mod mldsa_smoke {
243    use lib_q_ml_dsa::ml_dsa_44::portable;
244
245    #[test]
246    fn sign_verify_smoke() {
247        let kp = portable::generate_key_pair([0xA5u8; 32]);
248        let msg = b"sca-test smoke";
249        let sig = portable::sign(&kp.signing_key, msg, b"", [0x3Cu8; 32]).expect("sign");
250        portable::verify(&kp.verification_key, msg, b"", &sig).expect("verify");
251    }
252}