1#![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#[derive(Clone, Debug)]
13pub struct TvlaConfig {
14 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
26pub 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
44pub 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
52pub 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}