#![allow(clippy::needless_range_loop)]
use alloc::vec::Vec;
use sha3::digest::{ExtendableOutput, Update, XofReader};
use sha3::Shake256;
use zeroize::Zeroize;
use crate::identity_error::IdentityError;
use crate::plp;
use crate::saap::{
hash_to_challenge_from_xof, mod_q, poly_add, poly_mul_negacyclic, poly_sub, Polynomial,
PARAM_GAMMA1, PARAM_Q, REJECTION_BOUND, RING_N,
};
pub const CRED_L: usize = 4;
pub const CRED_ATTRIBUTES: usize = 8;
pub const CRED_SLOTS: usize = CRED_ATTRIBUTES + 1;
pub const CRED_T: usize = CRED_L + CRED_SLOTS;
pub const IDENTITY_SLOT: usize = 0;
const MAX_REJECTION_ITERATIONS: usize = 192;
const DOMAIN_B1: &[u8] = b"AETHEL_SAAP_B1_V2";
const DOMAIN_ISSUER_PUBLIC: &[u8] = b"AETHEL_SAAP_ISSUER_PUBLIC_V1";
const DOMAIN_CHALLENGE: &[u8] = b"AETHEL_SAAP_CHALLENGE_V3";
const DOMAIN_ISSUE_RANDOMNESS: &[u8] = b"AETHEL_SAAP_ISSUE_R_V1";
const DOMAIN_BLIND: &[u8] = b"AETHEL_SAAP_BLIND_V1";
const DOMAIN_MASK: &[u8] = b"AETHEL_SAAP_PROOF_MASK_V2";
fn from_plp_vec(v: &plp::PolyVec) -> [Polynomial; plp::MODULE_K] {
let mut out = [Polynomial::zero(); plp::MODULE_K];
for (slot, vi) in out.iter_mut().zip(v.iter()) {
*slot = from_plp(vi);
}
out
}
fn from_plp_mat(m: &plp::PolyMat) -> [[Polynomial; plp::MODULE_K]; plp::MODULE_K] {
let mut out = [[Polynomial::zero(); plp::MODULE_K]; plp::MODULE_K];
for (orow, mrow) in out.iter_mut().zip(m.iter()) {
for (cell, src) in orow.iter_mut().zip(mrow.iter()) {
*cell = from_plp(src);
}
}
out
}
fn mat_vec(
a: &[[Polynomial; plp::MODULE_K]; plp::MODULE_K],
v: &[Polynomial; plp::MODULE_K],
) -> [Polynomial; plp::MODULE_K] {
let mut out = [Polynomial::zero(); plp::MODULE_K];
for (slot, row) in out.iter_mut().zip(a.iter()) {
let mut acc = Polynomial::zero();
for (cell, vj) in row.iter().zip(v.iter()) {
acc = poly_add(&acc, &poly_mul_negacyclic(cell, vj));
}
*slot = acc;
}
out
}
fn from_plp(p: &plp::Poly) -> Polynomial {
let mut out = Polynomial::zero();
for i in 0..RING_N {
out.coeffs[i] = mod_q(p.coeffs[i] as i64);
}
out
}
fn sample_cbd(xof: &mut impl XofReader) -> Polynomial {
let mut p = Polynomial::zero();
let mut buf = [0u8; 1];
for i in 0..RING_N {
xof.read(&mut buf);
let a = (buf[0] & 0x03).count_ones() as i32;
let b = ((buf[0] >> 2) & 0x03).count_ones() as i32;
p.coeffs[i] = a - b;
}
p
}
fn sample_short_mask(xof: &mut impl XofReader) -> Polynomial {
let mut p = Polynomial::zero();
let range = 2 * PARAM_GAMMA1 as u32 + 1;
let mut i = 0usize;
while i < 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 {
p.coeffs[i] = val as i32 - PARAM_GAMMA1;
i += 1;
}
}
p
}
fn sample_uniform(xof: &mut impl XofReader) -> Polynomial {
let mut p = Polynomial::zero();
let q = PARAM_Q as u32;
let mut i = 0usize;
while i < 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 < q {
p.coeffs[i] = mod_q(val as i64);
i += 1;
}
}
p
}
fn xof_for(domain: &[u8], parts: &[&[u8]]) -> sha3::Shake256Reader {
let mut h = Shake256::default();
h.update(domain);
for part in parts {
h.update(&(part.len() as u32).to_le_bytes());
h.update(part);
}
h.finalize_xof()
}
pub struct IssuerParams {
public_seed: [u8; PUBLIC_SEED_BYTES],
b1: [[Polynomial; CRED_L]; CRED_T],
}
pub const PUBLIC_SEED_BYTES: usize = 32;
impl IssuerParams {
pub fn from_seed(issuer_seed: &[u8]) -> Result<Self, IdentityError> {
if issuer_seed.len() < PUBLIC_SEED_BYTES {
return Err(IdentityError::InvalidInputLength);
}
let mut public_seed = [0u8; PUBLIC_SEED_BYTES];
xof_for(DOMAIN_ISSUER_PUBLIC, &[issuer_seed]).read(&mut public_seed);
Ok(Self::from_public_seed(&public_seed))
}
pub fn from_public_seed(public_seed: &[u8; PUBLIC_SEED_BYTES]) -> Self {
let mut b1 = [[Polynomial::zero(); CRED_L]; CRED_T];
for row in 0..CRED_T {
for col in 0..CRED_L {
let mut xof = xof_for(DOMAIN_B1, &[public_seed, &[row as u8, col as u8]]);
b1[row][col] = sample_uniform(&mut xof);
}
}
Self { public_seed: *public_seed, b1 }
}
pub fn public_seed(&self) -> &[u8; PUBLIC_SEED_BYTES] {
&self.public_seed
}
fn mul(&self, v: &[Polynomial; CRED_L]) -> [Polynomial; CRED_T] {
let mut out = [Polynomial::zero(); CRED_T];
for row in 0..CRED_T {
let mut acc = Polynomial::zero();
for col in 0..CRED_L {
acc = poly_add(&acc, &poly_mul_negacyclic(&self.b1[row][col], &v[col]));
}
out[row] = acc;
}
out
}
}
const ATTRIBUTE_LIMB_BITS: u32 = 16;
const ATTRIBUTE_LIMBS: usize = 4;
fn encode_attribute(value: u64) -> Result<Polynomial, IdentityError> {
let mut p = Polynomial::zero();
let mask = (1u64 << ATTRIBUTE_LIMB_BITS) - 1;
for limb in 0..ATTRIBUTE_LIMBS {
let chunk = (value >> (ATTRIBUTE_LIMB_BITS * limb as u32)) & mask;
p.coeffs[limb] = mod_q(chunk as i64);
}
Ok(p)
}
fn decode_attribute(p: &Polynomial) -> u64 {
let mut value = 0u64;
for limb in 0..ATTRIBUTE_LIMBS {
let coeff = p.coeffs[limb];
let chunk = if coeff < 0 {
(coeff as i64 + PARAM_Q as i64) as u64
} else {
coeff as u64
};
value |= chunk << (ATTRIBUTE_LIMB_BITS * limb as u32);
}
value
}
pub struct Credential {
t_cred: [Polynomial; CRED_T],
r: [Polynomial; CRED_L],
m: [Polynomial; CRED_SLOTS],
}
impl Drop for Credential {
fn drop(&mut self) {
for p in self.r.iter_mut() {
p.zeroize();
}
for p in self.m.iter_mut() {
p.zeroize();
}
}
}
impl Credential {
pub fn issue(
params: &IssuerParams,
identity: &plp::MasterIdentity,
attributes: &[u64; CRED_ATTRIBUTES],
issuance_randomness: &[u8],
) -> Result<Self, IdentityError> {
if issuance_randomness.len() < 32 {
return Err(IdentityError::InvalidInputLength);
}
let mut m = [Polynomial::zero(); CRED_SLOTS];
m[IDENTITY_SLOT] = from_plp(&identity.secret()[0]);
for (i, value) in attributes.iter().enumerate() {
m[i + 1] = encode_attribute(*value)?;
}
let mut r = [Polynomial::zero(); CRED_L];
for (i, slot) in r.iter_mut().enumerate() {
let mut xof = xof_for(DOMAIN_ISSUE_RANDOMNESS, &[issuance_randomness, &[i as u8]]);
*slot = sample_cbd(&mut xof);
}
let mut t_cred = params.mul(&r);
for slot in 0..CRED_SLOTS {
t_cred[CRED_L + slot] = poly_add(&t_cred[CRED_L + slot], &m[slot]);
}
Ok(Self { t_cred, r, m })
}
}
pub struct BlindedCredential {
t_blind: [Polynomial; CRED_T],
r_star: [Polynomial; CRED_L],
m: [Polynomial; CRED_SLOTS],
}
impl Drop for BlindedCredential {
fn drop(&mut self) {
for p in self.r_star.iter_mut() {
p.zeroize();
}
for p in self.m.iter_mut() {
p.zeroize();
}
}
}
impl BlindedCredential {
pub fn new(
params: &IssuerParams,
credential: &Credential,
blinding_randomness: &[u8],
) -> Result<Self, IdentityError> {
if blinding_randomness.len() < 32 {
return Err(IdentityError::InvalidInputLength);
}
let mut r_blind = [Polynomial::zero(); CRED_L];
for (i, slot) in r_blind.iter_mut().enumerate() {
let mut xof = xof_for(DOMAIN_BLIND, &[blinding_randomness, &[i as u8]]);
*slot = sample_cbd(&mut xof);
}
let b_r_blind = params.mul(&r_blind);
let mut t_blind = [Polynomial::zero(); CRED_T];
for i in 0..CRED_T {
t_blind[i] = poly_add(&credential.t_cred[i], &b_r_blind[i]);
}
let mut r_star = [Polynomial::zero(); CRED_L];
for i in 0..CRED_L {
r_star[i] = poly_add(&credential.r[i], &r_blind[i]);
}
for p in r_blind.iter_mut() {
p.zeroize();
}
Ok(Self { t_blind, r_star, m: credential.m })
}
pub fn commitment(&self) -> &[Polynomial; CRED_T] {
&self.t_blind
}
}
pub struct SaapPresentation {
pub tau: [u8; 32],
pub disclosed: u8,
pub disclosed_values: [u64; CRED_ATTRIBUTES],
pub challenge: Polynomial,
pub z_r: [Polynomial; CRED_L],
pub z_m: [Polynomial; CRED_SLOTS],
pub z_s: [Polynomial; plp::MODULE_K],
pub z_e: [Polynomial; plp::MODULE_K],
}
fn absorb_poly(h: &mut Shake256, p: &Polynomial) {
for c in p.coeffs.iter() {
h.update(&c.to_le_bytes());
}
}
#[allow(clippy::too_many_arguments)]
fn derive_challenge(
w1: &[Polynomial; CRED_T],
w2: &[Polynomial; plp::MODULE_K],
b_tau: &[Polynomial; plp::MODULE_K],
t_blind: &[Polynomial; CRED_T],
disclosed: u8,
disclosed_values: &[u64; CRED_ATTRIBUTES],
tau: &[u8],
salt: &[u8; 32],
public_seed: &[u8; PUBLIC_SEED_BYTES],
) -> Polynomial {
let mut h = Shake256::default();
h.update(DOMAIN_CHALLENGE);
for p in w1.iter() {
absorb_poly(&mut h, p);
}
for p in w2.iter() {
absorb_poly(&mut h, p);
}
for p in b_tau.iter() {
absorb_poly(&mut h, p);
}
for p in t_blind.iter() {
absorb_poly(&mut h, p);
}
h.update(&[disclosed]);
for v in disclosed_values.iter() {
h.update(&v.to_le_bytes());
}
h.update(&(tau.len() as u32).to_le_bytes());
h.update(tau);
h.update(salt);
h.update(public_seed);
let mut xof = h.finalize_xof();
hash_to_challenge_from_xof(&mut xof)
}
fn split_messages(
m: &[Polynomial; CRED_SLOTS],
disclosed: u8,
) -> ([Polynomial; CRED_SLOTS], [Polynomial; CRED_SLOTS]) {
let mut m_pub = [Polynomial::zero(); CRED_SLOTS];
let mut m_hidden = [Polynomial::zero(); CRED_SLOTS];
m_hidden[IDENTITY_SLOT] = m[IDENTITY_SLOT];
for i in 0..CRED_ATTRIBUTES {
let slot = i + 1;
if disclosed & (1 << i) != 0 {
m_pub[slot] = m[slot];
} else {
m_hidden[slot] = m[slot];
}
}
(m_pub, m_hidden)
}
fn infinity_norm(p: &Polynomial) -> i32 {
p.coeffs.iter().map(|c| c.abs()).max().unwrap_or(0)
}
#[allow(clippy::too_many_arguments)] pub fn prove(
params: &IssuerParams,
blinded: &BlindedCredential,
identity: &plp::MasterIdentity,
projection: &plp::EphemeralProjection,
tau: &[u8],
projection_randomness: &[u8],
disclosed: u8,
presentation_randomness: &[u8],
) -> Result<SaapPresentation, IdentityError> {
if presentation_randomness.len() < 32 {
return Err(IdentityError::InvalidInputLength);
}
let a_tau = from_plp_mat(&projection.matrix_a);
let b_tau = from_plp_vec(&projection.public_b);
let s = from_plp_vec(identity.secret());
let mut e_tau_plp = plp::derive_error_tau(projection_randomness, tau);
let e_tau = from_plp_vec(&e_tau_plp);
e_tau_plp.iter_mut().for_each(|p| p.zeroize());
let (m_pub, m_hidden) = split_messages(&blinded.m, disclosed);
let mut disclosed_values = [0u64; CRED_ATTRIBUTES];
for i in 0..CRED_ATTRIBUTES {
if disclosed & (1 << i) != 0 {
disclosed_values[i] = decode_attribute(&m_pub[i + 1]);
}
}
let statement = {
let mut h = Shake256::default();
h.update(&(tau.len() as u32).to_le_bytes());
h.update(tau);
h.update(&projection.salt);
h.update(params.public_seed());
for p in blinded.t_blind.iter() {
absorb_poly(&mut h, p);
}
for p in b_tau.iter() {
absorb_poly(&mut h, p);
}
h.update(&[disclosed]);
for v in disclosed_values.iter() {
h.update(&v.to_le_bytes());
}
let mut out = [0u8; 32];
h.finalize_xof().read(&mut out);
out
};
for iteration in 0..MAX_REJECTION_ITERATIONS {
let nonce = [iteration as u8];
let mut y_r = [Polynomial::zero(); CRED_L];
for (i, slot) in y_r.iter_mut().enumerate() {
let mut xof =
xof_for(DOMAIN_MASK, &[presentation_randomness, &statement, b"r", &nonce, &[i as u8]]);
*slot = sample_short_mask(&mut xof);
}
let mut y_s = [Polynomial::zero(); plp::MODULE_K];
for (i, slot) in y_s.iter_mut().enumerate() {
let mut xof =
xof_for(DOMAIN_MASK, &[presentation_randomness, &statement, b"s", &nonce, &[i as u8]]);
*slot = sample_short_mask(&mut xof);
}
let mut y_e = [Polynomial::zero(); plp::MODULE_K];
for (i, slot) in y_e.iter_mut().enumerate() {
let mut xof =
xof_for(DOMAIN_MASK, &[presentation_randomness, &statement, b"e", &nonce, &[i as u8]]);
*slot = sample_short_mask(&mut xof);
}
let mut y_m = [Polynomial::zero(); CRED_SLOTS];
y_m[IDENTITY_SLOT] = y_s[0];
for slot in 1..CRED_SLOTS {
let mut xof =
xof_for(DOMAIN_MASK, &[presentation_randomness, &statement, b"m", &nonce, &[slot as u8]]);
y_m[slot] = sample_uniform(&mut xof);
}
let mut w1 = params.mul(&y_r);
for slot in 0..CRED_SLOTS {
w1[CRED_L + slot] = poly_add(&w1[CRED_L + slot], &y_m[slot]);
}
let mut w2 = mat_vec(&a_tau, &y_s);
for i in 0..plp::MODULE_K {
w2[i] = poly_add(&w2[i], &y_e[i]);
}
let c = derive_challenge(
&w1,
&w2,
&b_tau,
&blinded.t_blind,
disclosed,
&disclosed_values,
tau,
&projection.salt,
params.public_seed(),
);
let mut z_r = [Polynomial::zero(); CRED_L];
for i in 0..CRED_L {
z_r[i] = poly_add(&y_r[i], &poly_mul_negacyclic(&c, &blinded.r_star[i]));
}
let mut z_m = [Polynomial::zero(); CRED_SLOTS];
for slot in 0..CRED_SLOTS {
z_m[slot] = poly_add(&y_m[slot], &poly_mul_negacyclic(&c, &m_hidden[slot]));
}
let mut z_s = [Polynomial::zero(); plp::MODULE_K];
let mut z_e = [Polynomial::zero(); plp::MODULE_K];
for i in 0..plp::MODULE_K {
z_s[i] = poly_add(&y_s[i], &poly_mul_negacyclic(&c, &s[i]));
z_e[i] = poly_add(&y_e[i], &poly_mul_negacyclic(&c, &e_tau[i]));
}
let mut reject = z_s.iter().any(|z| infinity_norm(z) >= REJECTION_BOUND)
|| z_e.iter().any(|z| infinity_norm(z) >= REJECTION_BOUND);
for z in z_r.iter() {
reject |= infinity_norm(z) >= REJECTION_BOUND;
}
if !reject {
y_r.iter_mut().for_each(|p| p.zeroize());
y_s.iter_mut().for_each(|p| p.zeroize());
y_e.iter_mut().for_each(|p| p.zeroize());
return Ok(SaapPresentation {
tau: projection.tau,
disclosed,
disclosed_values,
challenge: c,
z_r,
z_m,
z_s,
z_e,
});
}
y_r.iter_mut().for_each(|p| p.zeroize());
y_s.iter_mut().for_each(|p| p.zeroize());
y_e.iter_mut().for_each(|p| p.zeroize());
z_r.iter_mut().for_each(|p| p.zeroize());
z_m.iter_mut().for_each(|p| p.zeroize());
}
Err(IdentityError::RejectionSamplingFailed)
}
pub fn verify(
params: &IssuerParams,
presentation: &SaapPresentation,
t_blind: &[Polynomial; CRED_T],
projection: &plp::EphemeralProjection,
tau: &[u8],
) -> Result<bool, IdentityError> {
let a_tau = from_plp_mat(&projection.matrix_a);
let b_tau = from_plp_vec(&projection.public_b);
let c = &presentation.challenge;
if presentation.z_s.iter().any(|z| infinity_norm(z) >= REJECTION_BOUND)
|| presentation.z_e.iter().any(|z| infinity_norm(z) >= REJECTION_BOUND)
{
return Ok(false);
}
for z in presentation.z_r.iter() {
if infinity_norm(z) >= REJECTION_BOUND {
return Ok(false);
}
}
let mut linkage_mismatch = 0i32;
for i in 0..RING_N {
linkage_mismatch |=
presentation.z_m[IDENTITY_SLOT].coeffs[i] ^ presentation.z_s[0].coeffs[i];
}
if linkage_mismatch != 0 {
return Ok(false);
}
let mut m_pub = [Polynomial::zero(); CRED_SLOTS];
for i in 0..CRED_ATTRIBUTES {
if presentation.disclosed & (1 << i) != 0 {
m_pub[i + 1] = encode_attribute(presentation.disclosed_values[i])?;
}
}
let mut w1 = params.mul(&presentation.z_r);
for slot in 0..CRED_SLOTS {
w1[CRED_L + slot] = poly_add(&w1[CRED_L + slot], &presentation.z_m[slot]);
}
for row in 0..CRED_T {
let mut target = t_blind[row];
if row >= CRED_L {
target = poly_sub(&target, &m_pub[row - CRED_L]);
}
w1[row] = poly_sub(&w1[row], &poly_mul_negacyclic(c, &target));
}
let az = mat_vec(&a_tau, &presentation.z_s);
let mut w2 = [Polynomial::zero(); plp::MODULE_K];
for i in 0..plp::MODULE_K {
w2[i] = poly_sub(
&poly_add(&az[i], &presentation.z_e[i]),
&poly_mul_negacyclic(c, &b_tau[i]),
);
}
let c_prime = derive_challenge(
&w1,
&w2,
&b_tau,
t_blind,
presentation.disclosed,
&presentation.disclosed_values,
tau,
&projection.salt,
params.public_seed(),
);
let mut mismatch = 0i32;
for i in 0..RING_N {
mismatch |= c_prime.coeffs[i] ^ c.coeffs[i];
}
Ok(mismatch == 0)
}
pub fn flatten(polys: &[Polynomial]) -> Vec<i32> {
let mut out = Vec::with_capacity(polys.len() * RING_N);
for p in polys {
out.extend_from_slice(&p.coeffs);
}
out
}
pub fn unflatten<const K: usize>(flat: &[i32]) -> Result<[Polynomial; K], IdentityError> {
if flat.len() != K * RING_N {
return Err(IdentityError::InvalidInputLength);
}
let mut out = [Polynomial::zero(); K];
for (k, poly) in out.iter_mut().enumerate() {
poly.coeffs.copy_from_slice(&flat[k * RING_N..(k + 1) * RING_N]);
}
Ok(out)
}
pub fn commitment_flat(blinded: &BlindedCredential) -> Vec<i32> {
flatten(blinded.commitment())
}
pub fn presentation_bytes(p: &SaapPresentation) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&p.tau);
out.push(p.disclosed);
for v in p.disclosed_values.iter() {
out.extend_from_slice(&v.to_le_bytes());
}
let mut push = |poly: &Polynomial| {
for c in poly.coeffs.iter() {
out.extend_from_slice(&c.to_le_bytes());
}
};
push(&p.challenge);
for z in p.z_r.iter() {
push(z);
}
for z in p.z_m.iter() {
push(z);
}
for z in p.z_s.iter() {
push(z);
}
for z in p.z_e.iter() {
push(z);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const ISSUER_SEED: &[u8] = b"issuer seed for the test suite!!";
const ISSUE_R: &[u8] = b"issuance randomness for tests!!!";
const BLIND_R: &[u8] = b"blinding randomness for tests!!!";
const PRES_R: &[u8] = b"presentation randomness tests!!!";
const RHO: &[u8] = b"projection randomness for tests!";
fn identity(seed: u8) -> plp::MasterIdentity {
plp::MasterIdentity::from_seed(&[seed; 32])
}
fn attrs() -> [u64; CRED_ATTRIBUTES] {
[31, 1990, 7, 42, 100, 5, 9, 12345]
}
#[test]
fn the_two_polynomial_representations_agree() {
let id = identity(0x42);
let proj = id.project_at_context(b"arithmetic-check", RHO);
let a_tau = from_plp_mat(&proj.matrix_a);
let s = from_plp_vec(id.secret());
let e_tau = from_plp_vec(&plp::derive_error_tau(RHO, b"arithmetic-check"));
let az = mat_vec(&a_tau, &s);
let expected = from_plp_vec(&proj.public_b);
for i in 0..plp::MODULE_K {
let recomputed = poly_add(&az[i], &e_tau[i]);
assert_eq!(
recomputed.coeffs, expected[i].coeffs,
"SAAP arithmetic over the converted PLP values does not reproduce b_tau in component {i}"
);
}
}
#[test]
fn the_arithmetic_check_can_fail() {
let id = identity(0x42);
let other = identity(0x99);
let proj = id.project_at_context(b"arithmetic-check", RHO);
let a_tau = from_plp_mat(&proj.matrix_a);
let wrong_s = from_plp_vec(other.secret());
let e_tau = from_plp_vec(&plp::derive_error_tau(RHO, b"arithmetic-check"));
let az = mat_vec(&a_tau, &wrong_s);
let expected = from_plp_vec(&proj.public_b);
let agrees = (0..plp::MODULE_K)
.all(|i| poly_add(&az[i], &e_tau[i]).coeffs == expected[i].coeffs);
assert!(
!agrees,
"a different secret reproduced b_tau, so the check proves nothing"
);
}
#[test]
fn the_error_term_is_reproducible_from_rho_and_tau() {
let flat = |v: &plp::PolyVec| {
v.iter().flat_map(|p| p.coeffs().to_vec()).collect::<Vec<u32>>()
};
let a = plp::derive_error_tau(RHO, b"ctx");
let b = plp::derive_error_tau(RHO, b"ctx");
assert_eq!(flat(&a), flat(&b));
let different = plp::derive_error_tau(RHO, b"other-ctx");
assert_ne!(flat(&a), flat(&different), "e_tau does not depend on tau");
}
struct Fixture {
params: IssuerParams,
blinded: BlindedCredential,
id: plp::MasterIdentity,
proj: plp::EphemeralProjection,
tau: &'static [u8],
}
fn fixture(seed: u8, tau: &'static [u8]) -> Fixture {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let id = identity(seed);
let cred = Credential::issue(¶ms, &id, &attrs(), ISSUE_R).expect("issue");
let blinded = BlindedCredential::new(¶ms, &cred, BLIND_R).expect("blind");
let proj = id.project_at_context(tau, RHO);
Fixture { params, blinded, id, proj, tau }
}
#[test]
fn an_honest_presentation_verifies_against_the_plp_projection() {
let f = fixture(0x42, b"context-alpha");
let p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
assert!(
verify(&f.params, &p, f.blinded.commitment(), &f.proj, f.tau).expect("verify"),
"an honestly generated presentation failed to verify"
);
}
#[test]
fn a_presentation_does_not_verify_against_another_identity() {
let f = fixture(0x42, b"context-alpha");
let other = identity(0x99);
let other_proj = other.project_at_context(b"context-alpha", RHO);
let p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
assert!(
!verify(&f.params, &p, f.blinded.commitment(), &other_proj, f.tau).expect("verify"),
"a presentation verified against a different identity's projection"
);
}
#[test]
fn rewriting_a_disclosed_attribute_invalidates_the_presentation() {
let f = fixture(0x42, b"context-alpha");
let mut p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0011, PRES_R)
.expect("prove");
assert!(verify(&f.params, &p, f.blinded.commitment(), &f.proj, f.tau).expect("verify"));
p.disclosed_values[0] += 1;
assert!(
!verify(&f.params, &p, f.blinded.commitment(), &f.proj, f.tau).expect("verify"),
"a rewritten disclosed attribute still verified"
);
}
#[test]
fn public_parameters_are_not_the_issuer_seed() {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
assert_ne!(
params.public_seed().as_slice(),
ISSUER_SEED,
"the published parameters were the issuer seed verbatim"
);
}
#[test]
fn deriving_public_parameters_is_deterministic_and_issuer_specific() {
let a = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let b = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let other = IssuerParams::from_seed(b"a different issuer seed entirely").unwrap();
assert_eq!(a.public_seed(), b.public_seed(), "derivation was not deterministic");
assert_ne!(a.public_seed(), other.public_seed(), "two issuers collided");
}
#[test]
fn parameters_rebuilt_from_the_published_seed_verify_the_same_presentation() {
let f = fixture(0x42, b"context-alpha");
let p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
let published = *f.params.public_seed();
let verifier_side = IssuerParams::from_public_seed(&published);
let ok = verify(&verifier_side, &p, f.blinded.commitment(), &f.proj, f.tau)
.expect("verify");
assert!(ok, "a presentation did not verify under rebuilt public parameters");
}
#[test]
fn a_short_issuer_seed_is_refused() {
assert!(matches!(
IssuerParams::from_seed(b"too short"),
Err(IdentityError::InvalidInputLength)
));
}
#[test]
fn a_credential_from_another_issuer_fails() {
let f = fixture(0x42, b"context-alpha");
let rogue = IssuerParams::from_seed(b"a different issuer seed entirely").unwrap();
let p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
assert!(
!verify(&rogue, &p, f.blinded.commitment(), &f.proj, f.tau).expect("verify"),
"a presentation verified under an issuer that never issued it"
);
}
#[test]
fn a_fabricated_commitment_fails() {
let f = fixture(0x42, b"context-alpha");
let p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
let mut fake = *f.blinded.commitment();
fake[CRED_L].coeffs[0] = mod_q(fake[CRED_L].coeffs[0] as i64 + 1);
assert!(
!verify(&f.params, &p, &fake, &f.proj, f.tau).expect("verify"),
"a presentation verified against a commitment it was not made over"
);
}
#[test]
fn breaking_the_identity_linkage_is_detected() {
let f = fixture(0x42, b"context-alpha");
let mut p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
p.z_m[IDENTITY_SLOT].coeffs[0] = mod_q(p.z_m[IDENTITY_SLOT].coeffs[0] as i64 + 1);
assert!(
!verify(&f.params, &p, f.blinded.commitment(), &f.proj, f.tau).expect("verify"),
"z_m[0] and z_s were allowed to disagree"
);
}
#[test]
fn a_tampered_response_is_rejected() {
let f = fixture(0x42, b"context-alpha");
let base = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
for which in 0..3 {
let mut p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 0b0000_0001, PRES_R)
.expect("prove");
match which {
0 => p.z_r[0].coeffs[5] = mod_q(p.z_r[0].coeffs[5] as i64 + 1),
1 => p.z_e[0].coeffs[5] = mod_q(p.z_e[0].coeffs[5] as i64 + 1),
_ => p.z_m[3].coeffs[5] = mod_q(p.z_m[3].coeffs[5] as i64 + 1),
}
assert!(
!verify(&f.params, &p, f.blinded.commitment(), &f.proj, f.tau).expect("verify"),
"tampering with response {which} was not detected"
);
}
assert!(verify(&f.params, &base, f.blinded.commitment(), &f.proj, f.tau).expect("verify"));
}
#[test]
fn two_presentations_under_different_contexts_are_unlinkable() {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let id = identity(0x42);
let cred = Credential::issue(¶ms, &id, &attrs(), ISSUE_R).expect("issue");
let b1 = BlindedCredential::new(¶ms, &cred, b"blinding for presentation one!!!")
.expect("blind");
let b2 = BlindedCredential::new(¶ms, &cred, b"blinding for presentation two!!!")
.expect("blind");
let p1_proj = id.project_at_context(b"context-one", RHO);
let p2_proj = id.project_at_context(b"context-two", RHO);
let p1 = prove(¶ms, &b1, &id, &p1_proj, b"context-one", RHO, 0b0000_0001, PRES_R)
.expect("prove");
let p2 = prove(¶ms, &b2, &id, &p2_proj, b"context-two", RHO, 0b0000_0001, PRES_R)
.expect("prove");
assert!(verify(¶ms, &p1, b1.commitment(), &p1_proj, b"context-one").expect("verify"));
assert!(verify(¶ms, &p2, b2.commitment(), &p2_proj, b"context-two").expect("verify"));
assert_ne!(
b1.commitment()[0].coeffs,
b2.commitment()[0].coeffs,
"two presentations reused the same blinded commitment"
);
assert_ne!(p1.z_s[0].coeffs, p2.z_s[0].coeffs, "z_s was reused across contexts");
assert_ne!(p1.z_r[0].coeffs, p2.z_r[0].coeffs, "z_r was reused across contexts");
assert_ne!(
p1.challenge.coeffs, p2.challenge.coeffs,
"the challenge was reused across contexts"
);
}
#[test]
fn masks_differ_when_only_the_disclosure_set_differs() {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let id = identity(0x31);
let cred = Credential::issue(¶ms, &id, &[11, 22, 33, 0, 0, 0, 0, 0], ISSUE_R)
.expect("issue");
let blinded = BlindedCredential::new(¶ms, &cred, BLIND_R).expect("blind");
let proj = id.project_at_context(b"one-context", RHO);
let a = prove(¶ms, &blinded, &id, &proj, b"one-context", RHO, 0b0000_0001, PRES_R)
.expect("prove");
let b = prove(¶ms, &blinded, &id, &proj, b"one-context", RHO, 0b0000_0010, PRES_R)
.expect("prove");
assert_ne!(
a.z_s[0].coeffs, b.z_s[0].coeffs,
"two presentations differing only in the disclosure set produced the same identity response, so the mask is not bound to the statement and the difference of the two responses leaks the master secret"
);
assert!(verify(¶ms, &a, blinded.commitment(), &proj, b"one-context").expect("v"));
assert!(verify(¶ms, &b, blinded.commitment(), &proj, b"one-context").expect("v"));
}
#[test]
fn the_challenge_binds_the_issuer_seed_and_the_projection_salt() {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let w1 = [Polynomial::zero(); CRED_T];
let w2 = [Polynomial::zero(); plp::MODULE_K];
let b_tau = [Polynomial::zero(); plp::MODULE_K];
let t_blind = [Polynomial::zero(); CRED_T];
let values = [0u64; CRED_ATTRIBUTES];
let base = derive_challenge(
&w1, &w2, &b_tau, &t_blind, 0, &values, b"ctx", &[0x11u8; 32], params.public_seed(),
);
let other_salt = derive_challenge(
&w1, &w2, &b_tau, &t_blind, 0, &values, b"ctx", &[0x22u8; 32], params.public_seed(),
);
let other_seed = derive_challenge(
&w1, &w2, &b_tau, &t_blind, 0, &values, b"ctx", &[0x11u8; 32], &[0x99u8; 32],
);
assert_ne!(
base.coeffs, other_salt.coeffs,
"the challenge does not depend on the projection salt, so A_tau is bound only through the verification equation"
);
assert_ne!(
base.coeffs, other_seed.coeffs,
"the challenge does not depend on the issuer public seed, so B_1 is bound only through the verification equation"
);
}
#[test]
fn an_undisclosed_attribute_does_not_appear_in_the_transcript() {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let id = identity(0x42);
let secret_attr = 0x0011_2233u64;
let mut values = attrs();
values[3] = secret_attr;
let cred = Credential::issue(¶ms, &id, &values, ISSUE_R).expect("issue");
let blinded = BlindedCredential::new(¶ms, &cred, BLIND_R).expect("blind");
let proj = id.project_at_context(b"context-alpha", RHO);
let p = prove(¶ms, &blinded, &id, &proj, b"context-alpha", RHO, 0b0000_0001, PRES_R)
.expect("prove");
let bytes = presentation_bytes(&p);
assert!(
!bytes.windows(8).any(|w| w == secret_attr.to_le_bytes()),
"the hidden attribute value appears verbatim in the transcript"
);
assert_eq!(
p.disclosed_values[3], 0,
"a hidden attribute was published in disclosed_values"
);
assert_ne!(
p.z_m[4].coeffs[0], secret_attr as i32,
"the hidden attribute's response is the value itself"
);
}
#[test]
fn the_transcript_leak_check_can_detect_a_leak() {
let secret_attr = 0x0011_2233u64;
let mut leaky = Vec::new();
leaky.extend_from_slice(&[0u8; 16]);
leaky.extend_from_slice(&secret_attr.to_le_bytes());
assert!(
leaky.windows(8).any(|w| w == secret_attr.to_le_bytes()),
"the leak check cannot see the value even when it is present"
);
}
#[test]
fn presentations_depend_on_their_randomness() {
let f = fixture(0x42, b"context-alpha");
let a = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 1, PRES_R).expect("prove");
let b = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, 1, PRES_R).expect("prove");
let c = prove(
&f.params,
&f.blinded,
&f.id,
&f.proj,
f.tau,
RHO,
1,
b"different presentation rand!!!!!",
)
.expect("prove");
assert_eq!(a.z_s[0].coeffs, b.z_s[0].coeffs);
assert_ne!(a.z_s[0].coeffs, c.z_s[0].coeffs, "the presentation ignores its randomness");
}
#[test]
fn short_randomness_is_refused() {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let id = identity(0x42);
assert!(matches!(
Credential::issue(¶ms, &id, &attrs(), b"short"),
Err(IdentityError::InvalidInputLength)
));
}
#[test]
fn attributes_larger_than_q_round_trip() {
let params = IssuerParams::from_seed(ISSUER_SEED).unwrap();
let id = identity(0x42);
let big = [
PARAM_Q as u64, 19_900_101, 1_767_225_600, u32::MAX as u64,
u64::MAX,
0,
1,
123_456_789_012,
];
let cred = Credential::issue(¶ms, &id, &big, ISSUE_R).expect("issue");
let blinded = BlindedCredential::new(¶ms, &cred, BLIND_R).expect("blind");
let proj = id.project_at_context(b"big-attributes", RHO);
let p = prove(¶ms, &blinded, &id, &proj, b"big-attributes", RHO, 0xFF, PRES_R)
.expect("prove");
assert!(
verify(¶ms, &p, blinded.commitment(), &proj, b"big-attributes").expect("verify"),
"a credential with large attributes failed to verify"
);
assert_eq!(p.disclosed_values, big, "large attribute values did not survive");
}
#[test]
fn values_differing_above_the_old_limit_do_not_collide() {
let a = encode_attribute(19_900_101).expect("encode");
let b = encode_attribute(19_900_101 + PARAM_Q as u64).expect("encode");
assert_ne!(
a.coeffs, b.coeffs,
"two values a multiple of q apart encoded identically"
);
}
#[test]
fn every_disclosure_pattern_verifies() {
let f = fixture(0x42, b"context-alpha");
for mask in [0u8, 1, 0b1010_1010, 0b0111_1111, 0xFF] {
let p = prove(&f.params, &f.blinded, &f.id, &f.proj, f.tau, RHO, mask, PRES_R)
.expect("prove");
assert!(
verify(&f.params, &p, f.blinded.commitment(), &f.proj, f.tau).expect("verify"),
"disclosure mask {mask:#010b} failed to verify"
);
}
}
}