aethel_core/sampling.rs
1//! # Enclave Constant-Time Rejection Sampling Module
2//!
3//! This module provides two complete implementations:
4//!
5//! 1. **Enclave Constant-Time Rejection Sampling** (`#![no_std]`, written to be bare-metal
6//! enclave-compatible, though nothing in this repo runs it inside an actual hardware
7//! enclave/TEE, and it has had no independent audit; see the README's audit
8//! status): constant-time rejection sampling enforcing a 16-iteration padded
9//! execution loop, constant-time norm checking, bitwise CMOV selection, and volatile
10//! memory zeroization via compiler barriers.
11//!
12//! 2. **M-LWE Centered Binomial Distribution Sampler (CBD η=2)**: Constant-time
13//! Centered Binomial Distribution polynomial sampler for Module-LWE ring
14//! `R_q = Z_q[X]/(X^256 + 1)`, including infinity norm checking for rejection
15//! sampling.
16//!
17//! ## Key Structures
18//!
19//! - [`Polynomial`](crate::sampling::Polynomial) — Cache-line-aligned ring element (64-byte aligned)
20//! - [`VectorK`](crate::sampling::VectorK) — k-dimensional module vector
21//! - [`PlpProof`](crate::sampling::PlpProof) — Output proof structure with response vector and iteration counter
22//! - [`RejectionError`](crate::sampling::RejectionError) — Error type for exhausted iteration ceiling
23//! - [`PolyRq`](crate::sampling::PolyRq) — Ring polynomial for the CBD sampler; `Copy`, so zeroization
24//! is on-demand via `Zeroize::zeroize()`, not automatic on drop
25//!
26//! ## Key Functions
27//!
28//! - [`enclave_explicit_zeroize`](crate::sampling::enclave_explicit_zeroize) — Volatile memory zeroization with compiler barrier
29//! - [`ct_check_norm_bound`](crate::sampling::ct_check_norm_bound) — Constant-time infinity norm bound check
30//! - [`ct_cond_copy`](crate::sampling::ct_cond_copy) — Constant-time conditional byte copy (CMOV equivalent)
31//! - [`enclave_plp_prove_fixed_time`](crate::sampling::enclave_plp_prove_fixed_time) — Fixed 16-iteration padded proof generation
32//! - [`poly_cbd_eta2`](crate::sampling::poly_cbd_eta2) — CBD η=2 polynomial sampler
33//! - [`poly_check_infinity_norm`](crate::sampling::poly_check_infinity_norm) — Constant-time infinity norm check
34//!
35//! ## Parameters
36//!
37//! - RING_N=256, MODULE_K=4, PARAM_Q=8_380_417
38//! - PARAM_GAMMA1=131_072, PARAM_BETA=78
39//! - FIXED_ITERATION_CEILING=16
40
41// Constant-time code: its index loops are left exactly as written, since an
42// iterator rewrite can change the generated code this module relies on.
43#![allow(clippy::needless_range_loop)]
44
45// ── Enclave Constant-Time Rejection Sampling (aethel-enclave-plp) ────────────
46
47use core::sync::atomic::{compiler_fence, Ordering};
48use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
49
50/// Ring degree `N`.
51pub const RING_N: usize = 256;
52/// Module rank `k`.
53pub const MODULE_K: usize = 4;
54/// Prime modulus `q`.
55pub const PARAM_Q: i32 = 8_380_417;
56/// Masking bound `γ₁`.
57pub const PARAM_GAMMA1: i32 = 131_072;
58/// Rejection margin `β`.
59pub const PARAM_BETA: i32 = 78;
60/// `γ₁ − β`: a response is released only if every coefficient is below this.
61pub const REJECTION_BOUND: i32 = PARAM_GAMMA1 - PARAM_BETA;
62/// Iterations the fixed-time prover always runs, whichever one is accepted.
63pub const FIXED_ITERATION_CEILING: usize = 16;
64
65/// Cache-line-aligned polynomial in R_q.
66#[derive(Copy, Clone)]
67#[repr(align(64))]
68pub struct Polynomial {
69 /// Coefficients, lowest degree first.
70 pub coeffs: [i32; RING_N],
71}
72
73impl Polynomial {
74 /// The zero polynomial.
75 pub const fn zero() -> Self {
76 Self { coeffs: [0i32; RING_N] }
77 }
78}
79
80/// k-dimensional module vector.
81#[derive(Copy, Clone)]
82#[repr(align(64))]
83pub struct VectorK {
84 /// The `k` polynomials.
85 pub vec: [Polynomial; MODULE_K],
86}
87
88impl VectorK {
89 /// The zero vector.
90 pub const fn zero() -> Self {
91 Self { vec: [Polynomial { coeffs: [0i32; RING_N] }; MODULE_K] }
92 }
93}
94
95/// Output proof structure.
96#[derive(Copy, Clone)]
97#[repr(C)]
98pub struct PlpProof {
99 /// The response vector `z`.
100 pub z: VectorK,
101 /// The iteration whose response was accepted.
102 pub iteration_counter: u32,
103}
104
105impl PlpProof {
106 /// An all-zero proof, used as the output buffer before any iteration runs.
107 pub const fn zero() -> Self {
108 Self {
109 z: VectorK { vec: [Polynomial { coeffs: [0; RING_N] }; MODULE_K] },
110 iteration_counter: 0,
111 }
112 }
113}
114
115// `Polynomial`/`VectorK`/`PlpProof` are `Copy`, so none of them can implement
116// `Drop` — meaning no `ZeroizeOnDrop`. They already have a correct, audited,
117// constant-time volatile-write zeroization path in `enclave_explicit_zeroize`
118// (used by `enclave_plp_prove_fixed_time` below); these impls give that same
119// path a standard `zeroize::Zeroize` interface, matching the rest of the
120// crate (P3-08), without introducing new `unsafe` code or changing behavior.
121
122impl zeroize::Zeroize for Polynomial {
123 fn zeroize(&mut self) {
124 enclave_explicit_zeroize(self);
125 }
126}
127
128impl zeroize::Zeroize for VectorK {
129 fn zeroize(&mut self) {
130 enclave_explicit_zeroize(self);
131 }
132}
133
134impl zeroize::Zeroize for PlpProof {
135 fn zeroize(&mut self) {
136 enclave_explicit_zeroize(self);
137 }
138}
139
140/// Volatile memory zeroization with compiler barrier.
141pub fn enclave_explicit_zeroize<T: Sized>(data: &mut T) {
142 let ptr = data as *mut T as *mut u8;
143 let len = core::mem::size_of::<T>();
144 // SAFETY: `ptr` is derived from `&mut T`, so it is non-null, aligned, and valid for
145 // `len = size_of::<T>()` bytes for the duration of this loop; `0..len` never advances
146 // past that allocation. Byte-wise writes need no alignment beyond `u8`. `write_volatile`
147 // (rather than a plain store) and the `compiler_fence` below prevent the compiler from
148 // eliding or reordering this zeroization around the secret's last use.
149 unsafe {
150 for i in 0..len {
151 core::ptr::write_volatile(ptr.add(i), 0u8);
152 }
153 }
154 compiler_fence(Ordering::SeqCst);
155}
156
157#[inline(always)]
158fn ct_abs(coeff: i32) -> i32 {
159 let mask = coeff >> 31;
160 (coeff + mask) ^ mask
161}
162
163#[inline(always)]
164fn ct_is_out_of_bounds(coeff: i32, bound: i32) -> u32 {
165 let abs_coeff = ct_abs(coeff);
166 let diff = (bound - 1) - abs_coeff;
167 ((diff >> 31) & 1) as u32 * 0xFFFFFFFF
168}
169
170/// Constant-time infinity norm bound check.
171///
172/// Returns 0 if all coefficients satisfy |coeff| < bound,
173/// or 0xFFFFFFFF if any coefficient violates the bound.
174pub fn ct_check_norm_bound(z: &VectorK, bound: i32) -> u32 {
175 let mut bad_coeff_mask: u32 = 0;
176 for k in 0..MODULE_K {
177 for n in 0..RING_N {
178 let coeff = z.vec[k].coeffs[n];
179 bad_coeff_mask |= ct_is_out_of_bounds(coeff, bound);
180 }
181 }
182 bad_coeff_mask
183}
184
185/// Constant-time conditional byte copy (CMOV equivalent).
186///
187/// If mask == 0xFFFFFFFF: `dst[i] = src[i]`
188/// If mask == 0x00000000: `dst[i]` unchanged
189#[inline(always)]
190pub fn ct_cond_copy(dst: &mut [u8], src: &[u8], mask: u32) {
191 let mask_u8 = mask as u8;
192 for i in 0..dst.len() {
193 dst[i] ^= mask_u8 & (src[i] ^ dst[i]);
194 }
195}
196
197/// Fixed 16-iteration padded proof generation.
198///
199/// Executes exactly FIXED_ITERATION_CEILING iterations regardless of when
200/// a valid candidate is found. Uses constant-time CMOV selection to capture
201/// the first valid proof without timing leaks.
202pub fn enclave_plp_prove_fixed_time(
203 proof_out: &mut PlpProof,
204 s: &VectorK,
205 tau: &[u8; 32],
206) -> Result<(), RejectionError> {
207 let mut proof_captured_mask: u32 = 0;
208 let mut candidate_z = VectorK::zero();
209 let mut candidate_proof = PlpProof::zero();
210 let mut dummy_buffer = PlpProof::zero();
211
212 for iter in 0..FIXED_ITERATION_CEILING {
213 generate_candidate_z(&mut candidate_z, s, tau, iter);
214 candidate_proof.z = candidate_z;
215 candidate_proof.iteration_counter = iter as u32;
216 let reject_mask = ct_check_norm_bound(&candidate_z, REJECTION_BOUND);
217 let capture_mask = (!reject_mask) & (!proof_captured_mask);
218 // SAFETY: `candidate_proof` is a live local of type `PlpProof`, so the pointer is
219 // non-null, aligned, and valid for exactly `size_of::<PlpProof>()` bytes — `PlpProof`
220 // is `#[repr(C)]` with no padding-sensitive invariants, so reinterpreting it as a
221 // byte slice is sound. The slice only borrows for this statement and is read-only.
222 let cand_bytes = unsafe {
223 core::slice::from_raw_parts(
224 &candidate_proof as *const PlpProof as *const u8,
225 core::mem::size_of::<PlpProof>(),
226 )
227 };
228 // SAFETY: `proof_out` is the `&mut PlpProof` this function was called with, valid
229 // and uniquely borrowed for `size_of::<PlpProof>()` bytes; it aliases neither
230 // `candidate_proof` nor `dummy_buffer` (distinct objects), which is what lets the
231 // read of `cand_bytes` above and the writes below coexist safely.
232 let target_bytes = unsafe {
233 core::slice::from_raw_parts_mut(
234 proof_out as *mut PlpProof as *mut u8,
235 core::mem::size_of::<PlpProof>(),
236 )
237 };
238 // SAFETY: `dummy_buffer` is a live local of type `PlpProof`, distinct from
239 // `candidate_proof` and `*proof_out`; same size/alignment/no-alias reasoning as
240 // `target_bytes` above. Its only purpose is to receive the discarded ct_cond_copy
241 // branch so both branches take equal time.
242 let dummy_bytes = unsafe {
243 core::slice::from_raw_parts_mut(
244 &mut dummy_buffer as *mut PlpProof as *mut u8,
245 core::mem::size_of::<PlpProof>(),
246 )
247 };
248 ct_cond_copy(target_bytes, cand_bytes, capture_mask);
249 ct_cond_copy(dummy_bytes, cand_bytes, !capture_mask);
250 proof_captured_mask |= capture_mask;
251 }
252
253 enclave_explicit_zeroize(&mut candidate_z);
254 enclave_explicit_zeroize(&mut candidate_proof);
255 enclave_explicit_zeroize(&mut dummy_buffer);
256
257 if proof_captured_mask != 0 {
258 Ok(())
259 } else {
260 enclave_explicit_zeroize(proof_out);
261 Err(RejectionError::AllIterationsRejected)
262 }
263}
264
265/// Error type for exhausted iteration ceiling.
266#[derive(Debug, Copy, Clone, PartialEq, Eq)]
267pub enum RejectionError {
268 /// Every iteration was rejected, so no response is released.
269 AllIterationsRejected,
270}
271
272// ── Fiat-Shamir sigma protocol step ──────────────────────────────────────────
273
274/// Sample a masking vector y from uniform [-γ₁, γ₁]^k using SHAKE-256.
275///
276/// y ← SHAKE-256("AETHEL_MASK_V1" ∥ seed ∥ nonce)
277fn sample_masking_vector(seed: &[u8; 32], nonce: u8) -> VectorK {
278 let mut hasher = Shake256::default();
279 hasher.update(b"AETHEL_MASK_V1");
280 hasher.update(seed);
281 hasher.update(&[nonce]);
282 let mut xof = hasher.finalize_xof();
283
284 let mut y = VectorK::zero();
285 let range = 2 * PARAM_GAMMA1 as u32 + 1; // 262145
286
287 for k in 0..MODULE_K {
288 let mut coeff_idx = 0usize;
289 while coeff_idx < RING_N {
290 let mut buf = [0u8; 3];
291 xof.read(&mut buf);
292 let val = (buf[0] as u32)
293 | ((buf[1] as u32) << 8)
294 | ((buf[2] as u32 & 0x7F) << 16);
295 if val < range {
296 // Center: val in [0, 2*γ₁] → coeff in [-γ₁, γ₁]
297 let centered = val as i32 - PARAM_GAMMA1;
298 y.vec[k].coeffs[coeff_idx] = centered;
299 coeff_idx += 1;
300 }
301 }
302 }
303 y
304}
305
306/// Derive the context matrix A_τ row for a given (tau, row_idx) using SHAKE-256.
307///
308/// A_τ[row] ← SHAKE-256("AETHEL_PLP_CTX_V1" ∥ tau ∥ row_idx)
309fn derive_context_row(tau: &[u8; 32], row_idx: usize) -> [Polynomial; MODULE_K] {
310 let mut row = [Polynomial::zero(); MODULE_K];
311 for col in 0..MODULE_K {
312 let mut hasher = Shake256::default();
313 hasher.update(b"AETHEL_PLP_CTX_V1");
314 hasher.update(tau);
315 hasher.update(&[row_idx as u8, col as u8]);
316 let mut xof = hasher.finalize_xof();
317
318 let mut coeff_idx = 0usize;
319 while coeff_idx < RING_N {
320 let mut buf = [0u8; 3];
321 xof.read(&mut buf);
322 let val = (buf[0] as u32)
323 | ((buf[1] as u32) << 8)
324 | ((buf[2] as u32 & 0x7F) << 16);
325 if (val as i32) < PARAM_Q {
326 // Store as centered representative
327 let centered = if val > (PARAM_Q as u32 / 2) {
328 val as i32 - PARAM_Q
329 } else {
330 val as i32
331 };
332 row[col].coeffs[coeff_idx] = centered;
333 coeff_idx += 1;
334 }
335 }
336 }
337 row
338}
339
340/// Polynomial multiplication in R_q = Z_q[X]/(X^N + 1), schoolbook O(N²).
341/// Operates on centered i32 coefficients.
342fn poly_mul_centered(a: &Polynomial, b: &Polynomial) -> Polynomial {
343 let mut tmp = [0i64; 2 * RING_N];
344 for i in 0..RING_N {
345 for j in 0..RING_N {
346 tmp[i + j] += (a.coeffs[i] as i64) * (b.coeffs[j] as i64);
347 }
348 }
349 let mut res = Polynomial::zero();
350 let q = PARAM_Q as i64;
351 for i in 0..RING_N {
352 let v = (tmp[i] - tmp[i + RING_N]).rem_euclid(q);
353 // Center
354 res.coeffs[i] = if v > q / 2 { (v - q) as i32 } else { v as i32 };
355 }
356 res
357}
358
359/// Polynomial addition mod q (centered).
360fn poly_add_centered(a: &Polynomial, b: &Polynomial) -> Polynomial {
361 let mut res = Polynomial::zero();
362 let q = PARAM_Q as i64;
363 for i in 0..RING_N {
364 let v = ((a.coeffs[i] as i64) + (b.coeffs[i] as i64)).rem_euclid(q);
365 res.coeffs[i] = if v > q / 2 { (v - q) as i32 } else { v as i32 };
366 }
367 res
368}
369
370/// Hash-to-challenge: sparse ternary polynomial with exactly 60 ±1 coefficients.
371///
372/// c = HashToPoly(SHAKE-256("AETHEL_SAAP_CHALLENGE_V1" ∥ w ∥ tau))
373fn hash_to_challenge_sampling(w: &VectorK, tau: &[u8; 32]) -> Polynomial {
374 let mut hasher = Shake256::default();
375 hasher.update(b"AETHEL_SAAP_CHALLENGE_V1");
376 for k in 0..MODULE_K {
377 for n in 0..RING_N {
378 hasher.update(&w.vec[k].coeffs[n].to_le_bytes());
379 }
380 }
381 hasher.update(tau);
382 let mut xof = hasher.finalize_xof();
383
384 let mut c_poly = Polynomial::zero();
385
386 let mut signs = [0u8; 8];
387 xof.read(&mut signs);
388 let mut sign_bit = 0usize;
389
390 let mut count = 0usize;
391 let mut used = [false; RING_N];
392 let mut pos_buf = [0u8; 1];
393
394 while count < 60 {
395 xof.read(&mut pos_buf);
396 let pos = pos_buf[0] as usize;
397 if pos >= RING_N {
398 continue;
399 }
400 if used[pos] {
401 continue;
402 }
403 used[pos] = true;
404 let sign_byte = signs[sign_bit / 8];
405 let sign: i32 = if (sign_byte >> (sign_bit % 8)) & 1 == 0 { 1 } else { -1 };
406 sign_bit += 1;
407 if sign_bit >= 64 {
408 xof.read(&mut signs);
409 sign_bit = 0;
410 }
411 c_poly.coeffs[pos] = sign;
412 count += 1;
413 }
414 c_poly
415}
416
417/// Generate a candidate response vector z for iteration `iter`.
418///
419/// Full Fiat-Shamir sigma protocol step:
420/// 1. Sample masking vector y ← S_{γ₁}^k
421/// 2. Compute commitment w = A_τ · y
422/// 3. Compute challenge c = HashToPoly(w, τ)
423/// 4. Compute z = y + c · s
424/// 5. Rejection check: ||z||∞ ≤ γ₁ - β
425fn generate_candidate_z(z: &mut VectorK, s: &VectorK, tau: &[u8; 32], iter: usize) {
426 // 1. Sample masking vector y
427 let y = sample_masking_vector(tau, iter as u8);
428
429 // 2. Compute commitment w = A_τ · y (k×k matrix × k vector)
430 let mut w = VectorK::zero();
431 for i in 0..MODULE_K {
432 let row = derive_context_row(tau, i);
433 for j in 0..MODULE_K {
434 let prod = poly_mul_centered(&row[j], &y.vec[j]);
435 w.vec[i] = poly_add_centered(&w.vec[i], &prod);
436 }
437 }
438
439 // 3. Compute challenge c = HashToPoly(w, τ)
440 let c = hash_to_challenge_sampling(&w, tau);
441
442 // 4. Compute z = y + c · s
443 for k in 0..MODULE_K {
444 let cs_k = poly_mul_centered(&c, &s.vec[k]);
445 z.vec[k] = poly_add_centered(&y.vec[k], &cs_k);
446 }
447}
448
449// ── M-LWE Centered Binomial Distribution Sampler (CBD η=2) ───────────────────
450
451/// Ring degree for the CBD sampler.
452pub const N_DEGREE: usize = 256;
453
454/// Prime modulus for the CBD sampler.
455pub const Q_MODULUS: i32 = 8_380_417;
456
457/// Ring polynomial `R_q = Z_q[X]/(X^N + 1)`.
458///
459/// P3-08 correction: despite this type's original doc claim, it cannot be
460/// "zeroize-on-drop" — it derives `Copy`, and `Copy` and `Drop` are mutually
461/// exclusive in Rust. Callers must call `zeroize()` (via `zeroize::Zeroize`) explicitly.
462/// Note also: nothing in this crate currently constructs a `PolyRq` outside
463/// its own tests (`poly_cbd_eta2` has no caller elsewhere in the crate), so
464/// this zeroization has no live call site to exercise yet — implemented and
465/// documented now so it's correct and available the moment something uses it.
466#[derive(Copy, Clone)]
467pub struct PolyRq {
468 /// Coefficients, lowest degree first.
469 pub coeffs: [i32; N_DEGREE],
470}
471
472impl PolyRq {
473 /// Create a zero polynomial.
474 pub const fn zero() -> Self {
475 Self { coeffs: [0i32; N_DEGREE] }
476 }
477}
478
479impl zeroize::Zeroize for PolyRq {
480 /// Zeroize all coefficients using volatile writes.
481 fn zeroize(&mut self) {
482 for i in 0..N_DEGREE {
483 // SAFETY: `&mut self.coeffs[i]` is a valid, uniquely-borrowed, aligned `i32`
484 // reference for the duration of this call; `write_volatile` and the
485 // `compiler_fence` below prevent the compiler from eliding this zeroization as
486 // a dead store now that `self.coeffs` has no further reads in this scope.
487 unsafe {
488 core::ptr::write_volatile(&mut self.coeffs[i], 0i32);
489 }
490 }
491 compiler_fence(Ordering::SeqCst);
492 }
493}
494
495/// Sample a polynomial from the Centered Binomial Distribution with η=2.
496///
497/// For each coefficient position, samples 4 bits (a₀, a₁, b₀, b₁) from
498/// the provided byte stream and computes:
499///
500/// ```text
501/// coeff = (a₀ + a₁) - (b₀ + b₁) ∈ {-2, -1, 0, 1, 2}
502/// ```
503pub fn poly_cbd_eta2(seed_bytes: &[u8]) -> PolyRq {
504 let mut poly = PolyRq::zero();
505 for i in 0..N_DEGREE {
506 let byte = seed_bytes[i % seed_bytes.len()];
507 let a0 = (byte & 0x01) as i32;
508 let a1 = ((byte >> 1) & 0x01) as i32;
509 let b0 = ((byte >> 2) & 0x01) as i32;
510 let b1 = ((byte >> 3) & 0x01) as i32;
511 let coeff = (a0 + a1) - (b0 + b1);
512 // Store as centered representative mod Q
513 poly.coeffs[i] = (coeff + Q_MODULUS) % Q_MODULUS;
514 }
515 poly
516}
517
518/// Check the infinity norm of a polynomial against a bound.
519///
520/// Returns `true` if all coefficients satisfy `|coeff| < bound` (centered),
521/// `false` if any coefficient violates the bound.
522///
523/// This check is performed in constant time: no early exit on violation.
524pub fn poly_check_infinity_norm(poly: &PolyRq, bound: i32) -> bool {
525 let mut all_ok: u32 = 0xFFFF_FFFF;
526 for i in 0..N_DEGREE {
527 let coeff = poly.coeffs[i];
528 let centered = if coeff > Q_MODULUS / 2 {
529 coeff - Q_MODULUS
530 } else {
531 coeff
532 };
533 let sign_mask = centered >> 31;
534 let abs_coeff = (centered + sign_mask) ^ sign_mask;
535 let diff = (bound - 1) - abs_coeff;
536 let violation_mask = (diff >> 31) as u32;
537 all_ok &= !violation_mask;
538 }
539 all_ok != 0
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_ct_check_norm_bound_accept() {
548 let mut z = VectorK::zero();
549 // Fill with values well within bound
550 for k in 0..MODULE_K {
551 for n in 0..RING_N {
552 z.vec[k].coeffs[n] = 1000;
553 }
554 }
555 let result = ct_check_norm_bound(&z, REJECTION_BOUND);
556 assert_eq!(result, 0, "should accept: all coefficients within bound");
557 }
558
559 #[test]
560 fn test_ct_check_norm_bound_reject() {
561 let mut z = VectorK::zero();
562 z.vec[0].coeffs[0] = REJECTION_BOUND + 1; // out of bounds
563 let result = ct_check_norm_bound(&z, REJECTION_BOUND);
564 assert_ne!(result, 0, "should reject: coefficient out of bound");
565 }
566
567 #[test]
568 fn test_poly_cbd_eta2_range() {
569 let seed = [0x5Au8; 256];
570 let poly = poly_cbd_eta2(&seed);
571 for &c in poly.coeffs.iter() {
572 // Centered representative should be in {-2,-1,0,1,2} mod Q
573 let centered = if c > Q_MODULUS / 2 { c - Q_MODULUS } else { c };
574 assert!((-2..=2).contains(¢ered), "CBD coefficient out of range: {}", centered);
575 }
576 }
577
578 #[test]
579 fn test_enclave_plp_prove_fixed_time() {
580 let mut s = VectorK::zero();
581 for k in 0..MODULE_K {
582 for n in 0..RING_N {
583 s.vec[k].coeffs[n] = (n % 5) as i32 - 2;
584 }
585 }
586 let tau = [0x5Au8; 32];
587 let mut proof_out = PlpProof::zero();
588 let result = enclave_plp_prove_fixed_time(&mut proof_out, &s, &tau);
589 assert!(result.is_ok(), "proof generation should succeed: {:?}", result);
590 }
591
592 #[test]
593 fn test_masking_vector_range() {
594 let seed = [0x42u8; 32];
595 let y = sample_masking_vector(&seed, 0);
596 for k in 0..MODULE_K {
597 for n in 0..RING_N {
598 let c = y.vec[k].coeffs[n];
599 assert!(
600 (-PARAM_GAMMA1..=PARAM_GAMMA1).contains(&c),
601 "masking coefficient {} out of range [-γ₁, γ₁]", c
602 );
603 }
604 }
605 }
606}