#![allow(clippy::needless_range_loop)]
use core::sync::atomic::{compiler_fence, Ordering};
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
pub const RING_N: usize = 256;
pub const MODULE_K: usize = 4;
pub const PARAM_Q: i32 = 8_380_417;
pub const PARAM_GAMMA1: i32 = 131_072;
pub const PARAM_BETA: i32 = 78;
pub const REJECTION_BOUND: i32 = PARAM_GAMMA1 - PARAM_BETA;
pub const FIXED_ITERATION_CEILING: usize = 16;
#[derive(Copy, Clone)]
#[repr(align(64))]
pub struct Polynomial {
pub coeffs: [i32; RING_N],
}
impl Polynomial {
pub const fn zero() -> Self {
Self { coeffs: [0i32; RING_N] }
}
}
#[derive(Copy, Clone)]
#[repr(align(64))]
pub struct VectorK {
pub vec: [Polynomial; MODULE_K],
}
impl VectorK {
pub const fn zero() -> Self {
Self { vec: [Polynomial { coeffs: [0i32; RING_N] }; MODULE_K] }
}
}
#[derive(Copy, Clone)]
#[repr(C)]
pub struct PlpProof {
pub z: VectorK,
pub iteration_counter: u32,
}
impl PlpProof {
pub const fn zero() -> Self {
Self {
z: VectorK { vec: [Polynomial { coeffs: [0; RING_N] }; MODULE_K] },
iteration_counter: 0,
}
}
}
impl zeroize::Zeroize for Polynomial {
fn zeroize(&mut self) {
enclave_explicit_zeroize(self);
}
}
impl zeroize::Zeroize for VectorK {
fn zeroize(&mut self) {
enclave_explicit_zeroize(self);
}
}
impl zeroize::Zeroize for PlpProof {
fn zeroize(&mut self) {
enclave_explicit_zeroize(self);
}
}
pub fn enclave_explicit_zeroize<T: Sized>(data: &mut T) {
let ptr = data as *mut T as *mut u8;
let len = core::mem::size_of::<T>();
unsafe {
for i in 0..len {
core::ptr::write_volatile(ptr.add(i), 0u8);
}
}
compiler_fence(Ordering::SeqCst);
}
#[inline(always)]
fn ct_abs(coeff: i32) -> i32 {
let mask = coeff >> 31;
(coeff + mask) ^ mask
}
#[inline(always)]
fn ct_is_out_of_bounds(coeff: i32, bound: i32) -> u32 {
let abs_coeff = ct_abs(coeff);
let diff = (bound - 1) - abs_coeff;
((diff >> 31) & 1) as u32 * 0xFFFFFFFF
}
pub fn ct_check_norm_bound(z: &VectorK, bound: i32) -> u32 {
let mut bad_coeff_mask: u32 = 0;
for k in 0..MODULE_K {
for n in 0..RING_N {
let coeff = z.vec[k].coeffs[n];
bad_coeff_mask |= ct_is_out_of_bounds(coeff, bound);
}
}
bad_coeff_mask
}
#[inline(always)]
pub fn ct_cond_copy(dst: &mut [u8], src: &[u8], mask: u32) {
let mask_u8 = mask as u8;
for i in 0..dst.len() {
dst[i] ^= mask_u8 & (src[i] ^ dst[i]);
}
}
pub fn enclave_plp_prove_fixed_time(
proof_out: &mut PlpProof,
s: &VectorK,
tau: &[u8; 32],
) -> Result<(), RejectionError> {
let mut proof_captured_mask: u32 = 0;
let mut candidate_z = VectorK::zero();
let mut candidate_proof = PlpProof::zero();
let mut dummy_buffer = PlpProof::zero();
for iter in 0..FIXED_ITERATION_CEILING {
generate_candidate_z(&mut candidate_z, s, tau, iter);
candidate_proof.z = candidate_z;
candidate_proof.iteration_counter = iter as u32;
let reject_mask = ct_check_norm_bound(&candidate_z, REJECTION_BOUND);
let capture_mask = (!reject_mask) & (!proof_captured_mask);
let cand_bytes = unsafe {
core::slice::from_raw_parts(
&candidate_proof as *const PlpProof as *const u8,
core::mem::size_of::<PlpProof>(),
)
};
let target_bytes = unsafe {
core::slice::from_raw_parts_mut(
proof_out as *mut PlpProof as *mut u8,
core::mem::size_of::<PlpProof>(),
)
};
let dummy_bytes = unsafe {
core::slice::from_raw_parts_mut(
&mut dummy_buffer as *mut PlpProof as *mut u8,
core::mem::size_of::<PlpProof>(),
)
};
ct_cond_copy(target_bytes, cand_bytes, capture_mask);
ct_cond_copy(dummy_bytes, cand_bytes, !capture_mask);
proof_captured_mask |= capture_mask;
}
enclave_explicit_zeroize(&mut candidate_z);
enclave_explicit_zeroize(&mut candidate_proof);
enclave_explicit_zeroize(&mut dummy_buffer);
if proof_captured_mask != 0 {
Ok(())
} else {
enclave_explicit_zeroize(proof_out);
Err(RejectionError::AllIterationsRejected)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum RejectionError {
AllIterationsRejected,
}
fn sample_masking_vector(seed: &[u8; 32], nonce: u8) -> VectorK {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_MASK_V1");
hasher.update(seed);
hasher.update(&[nonce]);
let mut xof = hasher.finalize_xof();
let mut y = VectorK::zero();
let range = 2 * PARAM_GAMMA1 as u32 + 1;
for k in 0..MODULE_K {
let mut coeff_idx = 0usize;
while coeff_idx < RING_N {
let mut buf = [0u8; 3];
xof.read(&mut buf);
let val = (buf[0] as u32)
| ((buf[1] as u32) << 8)
| ((buf[2] as u32 & 0x7F) << 16);
if val < range {
let centered = val as i32 - PARAM_GAMMA1;
y.vec[k].coeffs[coeff_idx] = centered;
coeff_idx += 1;
}
}
}
y
}
fn derive_context_row(tau: &[u8; 32], row_idx: usize) -> [Polynomial; MODULE_K] {
let mut row = [Polynomial::zero(); MODULE_K];
for col in 0..MODULE_K {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_PLP_CTX_V1");
hasher.update(tau);
hasher.update(&[row_idx as u8, col as u8]);
let mut xof = hasher.finalize_xof();
let mut coeff_idx = 0usize;
while coeff_idx < RING_N {
let mut buf = [0u8; 3];
xof.read(&mut buf);
let val = (buf[0] as u32)
| ((buf[1] as u32) << 8)
| ((buf[2] as u32 & 0x7F) << 16);
if (val as i32) < PARAM_Q {
let centered = if val > (PARAM_Q as u32 / 2) {
val as i32 - PARAM_Q
} else {
val as i32
};
row[col].coeffs[coeff_idx] = centered;
coeff_idx += 1;
}
}
}
row
}
fn poly_mul_centered(a: &Polynomial, b: &Polynomial) -> Polynomial {
let mut tmp = [0i64; 2 * RING_N];
for i in 0..RING_N {
for j in 0..RING_N {
tmp[i + j] += (a.coeffs[i] as i64) * (b.coeffs[j] as i64);
}
}
let mut res = Polynomial::zero();
let q = PARAM_Q as i64;
for i in 0..RING_N {
let v = (tmp[i] - tmp[i + RING_N]).rem_euclid(q);
res.coeffs[i] = if v > q / 2 { (v - q) as i32 } else { v as i32 };
}
res
}
fn poly_add_centered(a: &Polynomial, b: &Polynomial) -> Polynomial {
let mut res = Polynomial::zero();
let q = PARAM_Q as i64;
for i in 0..RING_N {
let v = ((a.coeffs[i] as i64) + (b.coeffs[i] as i64)).rem_euclid(q);
res.coeffs[i] = if v > q / 2 { (v - q) as i32 } else { v as i32 };
}
res
}
fn hash_to_challenge_sampling(w: &VectorK, tau: &[u8; 32]) -> Polynomial {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_SAAP_CHALLENGE_V1");
for k in 0..MODULE_K {
for n in 0..RING_N {
hasher.update(&w.vec[k].coeffs[n].to_le_bytes());
}
}
hasher.update(tau);
let mut xof = hasher.finalize_xof();
let mut c_poly = Polynomial::zero();
let mut signs = [0u8; 8];
xof.read(&mut signs);
let mut sign_bit = 0usize;
let mut count = 0usize;
let mut used = [false; RING_N];
let mut pos_buf = [0u8; 1];
while count < 60 {
xof.read(&mut pos_buf);
let pos = pos_buf[0] as usize;
if pos >= RING_N {
continue;
}
if used[pos] {
continue;
}
used[pos] = true;
let sign_byte = signs[sign_bit / 8];
let sign: i32 = if (sign_byte >> (sign_bit % 8)) & 1 == 0 { 1 } else { -1 };
sign_bit += 1;
if sign_bit >= 64 {
xof.read(&mut signs);
sign_bit = 0;
}
c_poly.coeffs[pos] = sign;
count += 1;
}
c_poly
}
fn generate_candidate_z(z: &mut VectorK, s: &VectorK, tau: &[u8; 32], iter: usize) {
let y = sample_masking_vector(tau, iter as u8);
let mut w = VectorK::zero();
for i in 0..MODULE_K {
let row = derive_context_row(tau, i);
for j in 0..MODULE_K {
let prod = poly_mul_centered(&row[j], &y.vec[j]);
w.vec[i] = poly_add_centered(&w.vec[i], &prod);
}
}
let c = hash_to_challenge_sampling(&w, tau);
for k in 0..MODULE_K {
let cs_k = poly_mul_centered(&c, &s.vec[k]);
z.vec[k] = poly_add_centered(&y.vec[k], &cs_k);
}
}
pub const N_DEGREE: usize = 256;
pub const Q_MODULUS: i32 = 8_380_417;
#[derive(Copy, Clone)]
pub struct PolyRq {
pub coeffs: [i32; N_DEGREE],
}
impl PolyRq {
pub const fn zero() -> Self {
Self { coeffs: [0i32; N_DEGREE] }
}
}
impl zeroize::Zeroize for PolyRq {
fn zeroize(&mut self) {
for i in 0..N_DEGREE {
unsafe {
core::ptr::write_volatile(&mut self.coeffs[i], 0i32);
}
}
compiler_fence(Ordering::SeqCst);
}
}
pub fn poly_cbd_eta2(seed_bytes: &[u8]) -> PolyRq {
let mut poly = PolyRq::zero();
for i in 0..N_DEGREE {
let byte = seed_bytes[i % seed_bytes.len()];
let a0 = (byte & 0x01) as i32;
let a1 = ((byte >> 1) & 0x01) as i32;
let b0 = ((byte >> 2) & 0x01) as i32;
let b1 = ((byte >> 3) & 0x01) as i32;
let coeff = (a0 + a1) - (b0 + b1);
poly.coeffs[i] = (coeff + Q_MODULUS) % Q_MODULUS;
}
poly
}
pub fn poly_check_infinity_norm(poly: &PolyRq, bound: i32) -> bool {
let mut all_ok: u32 = 0xFFFF_FFFF;
for i in 0..N_DEGREE {
let coeff = poly.coeffs[i];
let centered = if coeff > Q_MODULUS / 2 {
coeff - Q_MODULUS
} else {
coeff
};
let sign_mask = centered >> 31;
let abs_coeff = (centered + sign_mask) ^ sign_mask;
let diff = (bound - 1) - abs_coeff;
let violation_mask = (diff >> 31) as u32;
all_ok &= !violation_mask;
}
all_ok != 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ct_check_norm_bound_accept() {
let mut z = VectorK::zero();
for k in 0..MODULE_K {
for n in 0..RING_N {
z.vec[k].coeffs[n] = 1000;
}
}
let result = ct_check_norm_bound(&z, REJECTION_BOUND);
assert_eq!(result, 0, "should accept: all coefficients within bound");
}
#[test]
fn test_ct_check_norm_bound_reject() {
let mut z = VectorK::zero();
z.vec[0].coeffs[0] = REJECTION_BOUND + 1; let result = ct_check_norm_bound(&z, REJECTION_BOUND);
assert_ne!(result, 0, "should reject: coefficient out of bound");
}
#[test]
fn test_poly_cbd_eta2_range() {
let seed = [0x5Au8; 256];
let poly = poly_cbd_eta2(&seed);
for &c in poly.coeffs.iter() {
let centered = if c > Q_MODULUS / 2 { c - Q_MODULUS } else { c };
assert!((-2..=2).contains(¢ered), "CBD coefficient out of range: {}", centered);
}
}
#[test]
fn test_enclave_plp_prove_fixed_time() {
let mut s = VectorK::zero();
for k in 0..MODULE_K {
for n in 0..RING_N {
s.vec[k].coeffs[n] = (n % 5) as i32 - 2;
}
}
let tau = [0x5Au8; 32];
let mut proof_out = PlpProof::zero();
let result = enclave_plp_prove_fixed_time(&mut proof_out, &s, &tau);
assert!(result.is_ok(), "proof generation should succeed: {:?}", result);
}
#[test]
fn test_masking_vector_range() {
let seed = [0x42u8; 32];
let y = sample_masking_vector(&seed, 0);
for k in 0..MODULE_K {
for n in 0..RING_N {
let c = y.vec[k].coeffs[n];
assert!(
(-PARAM_GAMMA1..=PARAM_GAMMA1).contains(&c),
"masking coefficient {} out of range [-γ₁, γ₁]", c
);
}
}
}
}