extern crate alloc;
use alloc::vec::Vec;
use sha3::{Shake256, digest::{Update, ExtendableOutput, XofReader}};
use crate::identity_error::IdentityError;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const N: usize = 256;
pub const Q: u32 = 8_380_417;
pub const ETA: i32 = 2;
pub const GAMMA1: i32 = 131_072;
pub const BETA: i32 = 78;
pub const REJECTION_THRESHOLD: i32 = GAMMA1 - BETA;
const ZETA: [u32; 128] = [
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
];
const NTT_ZETAS: [i32; 128] = [
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
1753, 1753, 1753, 1753, 1753, 1753, 1753, 1753,
];
#[derive(Clone, Copy, PartialEq, Zeroize)]
pub struct Poly {
pub(crate) coeffs: [u32; N],
}
impl Poly {
pub const fn zero() -> Self {
Self { coeffs: [0u32; N] }
}
pub fn coeffs(&self) -> &[u32; N] {
&self.coeffs
}
#[inline]
pub fn add(&self, other: &Self) -> Self {
let mut res = Self::zero();
for i in 0..N {
res.coeffs[i] = add_mod(self.coeffs[i], other.coeffs[i]);
}
res
}
#[inline]
pub fn sub(&self, other: &Self) -> Self {
let mut res = Self::zero();
for i in 0..N {
res.coeffs[i] = sub_mod(self.coeffs[i], other.coeffs[i]);
}
res
}
pub fn mul_schoolbook(&self, other: &Self) -> Self {
let mut tmp = [0i64; 2 * N];
for i in 0..N {
for j in 0..N {
tmp[i + j] += (self.coeffs[i] as i64) * (other.coeffs[j] as i64);
}
}
let mut res = Self::zero();
for i in 0..N {
let v = (tmp[i] - tmp[i + N]).rem_euclid(Q as i64);
res.coeffs[i] = v as u32;
}
res
}
pub fn infinity_norm(&self) -> i64 {
let mut max_val = 0i64;
for &c in self.coeffs.iter() {
let centered = if c > Q / 2 { Q as i64 - c as i64 } else { c as i64 };
if centered > max_val {
max_val = centered;
}
}
max_val
}
pub fn center(&self) -> [i32; N] {
let mut out = [0i32; N];
for i in 0..N {
let c = self.coeffs[i];
out[i] = if c > Q / 2 { c as i32 - Q as i32 } else { c as i32 };
}
out
}
}
#[inline(always)]
fn add_mod(a: u32, b: u32) -> u32 {
let s = a + b;
if s >= Q { s - Q } else { s }
}
#[inline(always)]
fn sub_mod(a: u32, b: u32) -> u32 {
if a >= b { a - b } else { a + Q - b }
}
#[inline(always)]
fn mul_mod(a: u32, b: u32) -> u32 {
((a as u64 * b as u64) % Q as u64) as u32
}
fn pow_mod(mut base: u64, mut exp: u64, modulus: u64) -> u64 {
let mut result = 1u64;
base %= modulus;
while exp > 0 {
if exp & 1 == 1 {
result = result * base % modulus;
}
exp >>= 1;
base = base * base % modulus;
}
result
}
fn bit_reverse(mut x: usize, bits: usize) -> usize {
let mut result = 0usize;
for _ in 0..bits {
result = (result << 1) | (x & 1);
x >>= 1;
}
result
}
pub fn ntt_forward(poly: &mut Poly) {
let q = Q as u64;
let zeta: u64 = 1753;
let n = N;
let log2n = 8; for i in 0..n {
let j = bit_reverse(i, log2n);
if i < j {
poly.coeffs.swap(i, j);
}
}
let mut len = 1usize;
let mut k = n / 2; while len < n {
let twiddle_exp = (n / (2 * len)) as u64;
let w = pow_mod(zeta, twiddle_exp, q);
let mut wn = 1u64;
for j in 0..len {
for i in (j..n).step_by(2 * len) {
let u = poly.coeffs[i] as u64;
let v = poly.coeffs[i + len] as u64 * wn % q;
poly.coeffs[i] = ((u + v) % q) as u32;
poly.coeffs[i + len] = ((u + q - v) % q) as u32;
}
wn = wn * w % q;
}
len *= 2;
k /= 2;
}
let _ = k;
}
pub fn ntt_inverse(poly: &mut Poly) {
let q = Q as u64;
let zeta: u64 = 1753;
let n = N;
let log2n = 8;
for i in 0..n {
let j = bit_reverse(i, log2n);
if i < j {
poly.coeffs.swap(i, j);
}
}
let mut len = n / 2;
while len >= 1 {
let twiddle_exp = (n / (2 * len)) as u64;
let inv_twiddle_exp = (q - 1 - twiddle_exp % (q - 1)) % (q - 1);
let w = pow_mod(zeta, inv_twiddle_exp, q);
let mut wn = 1u64;
for j in 0..len {
for i in (j..n).step_by(2 * len) {
let u = poly.coeffs[i] as u64;
let v = poly.coeffs[i + len] as u64;
poly.coeffs[i] = ((u + v) % q) as u32;
poly.coeffs[i + len] = ((u + q - v) % q * wn % q) as u32;
}
wn = wn * w % q;
}
len /= 2;
}
let n_inv = pow_mod(n as u64, q - 2, q); for c in poly.coeffs.iter_mut() {
*c = (*c as u64 * n_inv % q) as u32;
}
}
pub fn poly_mul_ntt(a: &Poly, b: &Poly) -> Poly {
let mut fa = *a;
let mut fb = *b;
ntt_forward(&mut fa);
ntt_forward(&mut fb);
let mut fc = Poly::zero();
for i in 0..N {
fc.coeffs[i] = mul_mod(fa.coeffs[i], fb.coeffs[i]);
}
ntt_inverse(&mut fc);
fc
}
pub fn derive_context_matrix_k1(tau: &[u8]) -> Poly {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_PLP_CTX_V1");
hasher.update(tau);
let mut xof = hasher.finalize_xof();
let mut poly = Poly::zero();
let mut coeff_idx = 0usize;
while coeff_idx < 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 < Q {
poly.coeffs[coeff_idx] = val;
coeff_idx += 1;
}
}
poly
}
fn sample_cbd_eta2_from_xof(xof: &mut impl XofReader) -> Poly {
let mut poly = Poly::zero();
let mut buf = [0u8; N]; xof.read(&mut buf);
for i in 0..N {
let byte = buf[i];
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.rem_euclid(Q as i32)) as u32;
}
poly
}
fn sample_mask_from_xof(xof: &mut impl XofReader) -> Poly {
let mut poly = Poly::zero();
let range = 2 * GAMMA1 as u32 + 1; let mut coeff_idx = 0usize;
while coeff_idx < 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 - GAMMA1;
poly.coeffs[coeff_idx] = centered.rem_euclid(Q as i32) as u32;
coeff_idx += 1;
}
}
poly
}
pub fn hash_to_challenge(w: &Poly, ctx: u64) -> Poly {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_PLP_CHALLENGE_V1");
for &c in w.coeffs.iter() {
hasher.update(&c.to_le_bytes());
}
hasher.update(&ctx.to_le_bytes());
let mut xof = hasher.finalize_xof();
let mut c_poly = Poly::zero();
let mut signs = [0u8; 8]; xof.read(&mut signs);
let mut sign_bit = 0usize;
let mut count = 0usize;
let mut used = [false; N];
let mut pos_buf = [0u8; 1];
while count < 60 {
xof.read(&mut pos_buf);
let pos = pos_buf[0] as usize;
if pos >= 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.rem_euclid(Q as i32) as u32;
count += 1;
}
c_poly
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct MasterIdentity {
secret_key: Poly,
}
impl MasterIdentity {
pub fn from_seed(seed: &[u8; 32]) -> Self {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_MASTER_KEY_V1");
hasher.update(seed);
let mut xof = hasher.finalize_xof();
let secret_key = sample_cbd_eta2_from_xof(&mut xof);
Self { secret_key }
}
pub fn project_at_context(&self, tau: &[u8]) -> EphemeralProjection {
let matrix_a = derive_context_matrix_k1(tau);
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_ERROR_V1");
hasher.update(tau);
let mut xof = hasher.finalize_xof();
let mut e_tau = sample_cbd_eta2_from_xof(&mut xof);
let public_b = matrix_a.mul_schoolbook(&self.secret_key).add(&e_tau);
e_tau.zeroize();
EphemeralProjection {
tau: {
let mut t = [0u8; 32];
let len = tau.len().min(32);
t[..len].copy_from_slice(&tau[..len]);
t
},
matrix_a,
public_b,
}
}
}
pub fn checked_project_at_context(
secret: &[u8],
tau: &[u8],
) -> Result<EphemeralProjection, IdentityError> {
if secret.len() != 32 {
return Err(IdentityError::InvalidInputLength);
}
let mut seed = [0u8; 32];
seed.copy_from_slice(secret);
let identity = MasterIdentity::from_seed(&seed);
Ok(identity.project_at_context(tau))
}
pub const EPHEMERAL_PROJECTION_BYTE_LEN: usize = 32 + N * 4 + N * 4;
#[derive(Clone)]
pub struct EphemeralProjection {
pub tau: [u8; 32],
pub matrix_a: Poly,
pub public_b: Poly,
}
impl EphemeralProjection {
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = alloc::vec![0u8; EPHEMERAL_PROJECTION_BYTE_LEN];
out[..32].copy_from_slice(&self.tau);
for (i, &c) in self.matrix_a.coeffs.iter().enumerate() {
let offset = 32 + i * 4;
out[offset..offset + 4].copy_from_slice(&c.to_le_bytes());
}
for (i, &c) in self.public_b.coeffs.iter().enumerate() {
let offset = 32 + N * 4 + i * 4;
out[offset..offset + 4].copy_from_slice(&c.to_le_bytes());
}
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdentityError> {
if bytes.len() < EPHEMERAL_PROJECTION_BYTE_LEN {
return Err(IdentityError::SerializationError);
}
let mut tau = [0u8; 32];
tau.copy_from_slice(&bytes[..32]);
let mut matrix_a = Poly::zero();
for i in 0..N {
let offset = 32 + i * 4;
let mut b = [0u8; 4];
b.copy_from_slice(&bytes[offset..offset + 4]);
matrix_a.coeffs[i] = u32::from_le_bytes(b);
}
let mut public_b = Poly::zero();
for i in 0..N {
let offset = 32 + N * 4 + i * 4;
let mut b = [0u8; 4];
b.copy_from_slice(&bytes[offset..offset + 4]);
public_b.coeffs[i] = u32::from_le_bytes(b);
}
Ok(Self { tau, matrix_a, public_b })
}
}
#[derive(Clone)]
pub struct ZkIdentityProof {
pub commitment_w: Poly,
pub challenge_c: Poly,
pub response_z: Poly,
}
pub struct Prover;
impl Prover {
pub fn prove_identity(
identity: &MasterIdentity,
proj: &EphemeralProjection,
seed: &[u8; 32],
) -> ZkIdentityProof {
for iter in 0u8..16 {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_MASK_V1");
hasher.update(seed);
hasher.update(&[iter]);
let mut xof = hasher.finalize_xof();
let mut y = sample_mask_from_xof(&mut xof);
let mut w = proj.matrix_a.mul_schoolbook(&y);
let ctx_u64 = u64::from_le_bytes(proj.tau[..8].try_into().unwrap_or([0u8; 8]));
let mut challenge_c = hash_to_challenge(&w, ctx_u64);
let mut cs = challenge_c.mul_schoolbook(&identity.secret_key);
let mut z = y.add(&cs);
y.zeroize();
cs.zeroize();
if z.infinity_norm() < REJECTION_THRESHOLD as i64 {
return ZkIdentityProof {
commitment_w: w,
challenge_c,
response_z: z,
};
}
w.zeroize();
challenge_c.zeroize();
z.zeroize();
}
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_MASK_V1");
hasher.update(seed);
hasher.update(&[0u8]);
let mut xof = hasher.finalize_xof();
let mut y = sample_mask_from_xof(&mut xof);
let w = proj.matrix_a.mul_schoolbook(&y);
let ctx_u64 = u64::from_le_bytes(proj.tau[..8].try_into().unwrap_or([0u8; 8]));
let challenge_c = hash_to_challenge(&w, ctx_u64);
let mut cs = challenge_c.mul_schoolbook(&identity.secret_key);
let z = y.add(&cs);
y.zeroize();
cs.zeroize();
ZkIdentityProof {
commitment_w: w,
challenge_c,
response_z: z,
}
}
}
pub struct Verifier;
impl Verifier {
pub fn verify(proj: &EphemeralProjection, proof: &ZkIdentityProof) -> bool {
if proof.response_z.infinity_norm() >= REJECTION_THRESHOLD as i64 {
return false;
}
let ctx_u64 = u64::from_le_bytes(proj.tau[..8].try_into().unwrap_or([0u8; 8]));
let recomputed_c = hash_to_challenge(&proof.commitment_w, ctx_u64);
let mut mismatch = 0u32;
for i in 0..N {
mismatch |= recomputed_c.coeffs[i] ^ proof.challenge_c.coeffs[i];
}
if mismatch != 0 {
return false;
}
let az = proj.matrix_a.mul_schoolbook(&proof.response_z);
let cb = proof.challenge_c.mul_schoolbook(&proj.public_b);
let w_prime = az.sub(&cb);
let diff = w_prime.sub(&proof.commitment_w);
diff.infinity_norm() < (BETA as i64 * 2)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_seed() -> [u8; 32] {
[0x42u8; 32]
}
#[test]
fn test_poly_add_sub() {
let mut a = Poly::zero();
let mut b = Poly::zero();
a.coeffs[0] = 100;
b.coeffs[0] = 200;
let c = a.add(&b);
assert_eq!(c.coeffs[0], 300);
let d = c.sub(&b);
assert_eq!(d.coeffs[0], 100);
}
#[test]
fn test_derive_context_matrix_deterministic() {
let tau = b"test_context_tau";
let a1 = derive_context_matrix_k1(tau);
let a2 = derive_context_matrix_k1(tau);
assert_eq!(a1.coeffs, a2.coeffs);
for &c in a1.coeffs.iter() {
assert!(c < Q, "coefficient {} >= Q", c);
}
}
#[test]
fn test_hash_to_challenge_sparse() {
let w = Poly::zero();
let c = hash_to_challenge(&w, 1000u64);
let nonzero = c.coeffs.iter().filter(|&&x| x != 0).count();
assert_eq!(nonzero, 60, "challenge should have exactly 60 non-zero coefficients");
}
#[test]
fn test_prove_verify_roundtrip() {
let seed = test_seed();
let identity = MasterIdentity::from_seed(&seed);
let tau = b"block_1000_context";
let proj = identity.project_at_context(tau);
let proof = Prover::prove_identity(&identity, &proj, &seed);
assert!(Verifier::verify(&proj, &proof), "proof should verify");
}
#[test]
fn test_cross_context_unlinkability() {
let seed = test_seed();
let identity = MasterIdentity::from_seed(&seed);
let proj1 = identity.project_at_context(b"context_1");
let proj2 = identity.project_at_context(b"context_2");
assert_ne!(proj1.public_b.coeffs, proj2.public_b.coeffs);
let proof1 = Prover::prove_identity(&identity, &proj1, &seed);
assert!(!Verifier::verify(&proj2, &proof1), "cross-context replay should fail");
}
#[test]
fn test_key_generation() {
let seed = test_seed();
let identity = MasterIdentity::from_seed(&seed);
let all_zero = identity.secret_key.coeffs.iter().all(|&c| c == 0);
assert!(!all_zero, "Secret key should not be all-zero (negligible probability)");
for &coeff in identity.secret_key.coeffs.iter() {
assert!(coeff < Q, "Secret key coefficient {} out of range [0, Q-1]", coeff);
}
}
#[test]
fn test_key_generation_uniqueness() {
let identity_a = MasterIdentity::from_seed(&test_seed());
let identity_b = MasterIdentity::from_seed(&[0xABu8; 32]);
let keys_equal = identity_a
.secret_key
.coeffs
.iter()
.zip(identity_b.secret_key.coeffs.iter())
.all(|(a, b)| a == b);
assert!(!keys_equal, "Two independently generated secret keys should differ");
}
#[test]
fn test_tampered_proof_rejected() {
let seed = test_seed();
let identity = MasterIdentity::from_seed(&seed);
let tau = b"tamper_test_context_777";
let proj = identity.project_at_context(tau);
let mut proof = Prover::prove_identity(&identity, &proj, &seed);
proof.response_z.coeffs[0] = proof.response_z.coeffs[0].wrapping_add(1);
let valid = Verifier::verify(&proj, &proof);
assert!(!valid, "Tampered proof should be rejected by the verifier");
}
fn assert_master_identity_zeroizes() {
assert!(
core::mem::needs_drop::<MasterIdentity>(),
"MasterIdentity must carry Drop glue for ZeroizeOnDrop to do anything on scope exit"
);
let seed = [0x77u8; 32];
let mut identity = MasterIdentity::from_seed(&seed);
assert!(
identity.secret_key.coeffs.iter().any(|&c| c != 0),
"sanity check: this seed should produce a non-zero secret key"
);
identity.zeroize();
assert!(
identity.secret_key.coeffs.iter().all(|&c| c == 0),
"zeroize() must clear every coefficient of the secret key"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn zeroize_wipes_master_identity_secret() {
assert_master_identity_zeroizes();
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test::wasm_bindgen_test]
fn zeroize_wipes_master_identity_secret_wasm() {
assert_master_identity_zeroizes();
}
}