灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-crypto/src/lw/lrs.rs
//! LRS-87: experimental lattice linkable ring signature (DESIGN §5).
//!
//! **Experimental.** See DESIGN §5.5 for exactly what is and isn't known.
//!
//! Implementation notes:
//! - Secrets (`s_spend`, `s_o`, the mask `y`, their NTT images and `c·s`) are
//!   zeroized after use / on drop. Arithmetic on them is branch-free.
//! - Every ring member costs the same work in each signing attempt: the
//!   signer's commitment goes through the same routine as a simulated member,
//!   with the challenge polynomial set to zero.
//! - Signing randomness is hedged: the stream seed is
//!   `H("LW1/lrs/sign-seed", s_o, μ, R, I, 32 B OS randomness)`.

use sha3::digest::{ExtendableOutput, Update, XofReader};
use sha3::Shake256;
use zeroize::{Zeroize, Zeroizing};

use crate::lw::hash::{h256, xof, XofStream};
use crate::lw::lattice::{self, center, matrix_a, matrix_b, pack23, to_mod_q, unpack23, Poly, PolyVec, N, Q};
use crate::lw::{CryptoError, Result};

pub const K: usize = 4;
pub const M: usize = 4;
pub const L: usize = 15;
pub const ETA_SPEND: i32 = 2;
pub const ETA_OFFSET: i32 = 2;
pub const ETA: i32 = 4;
pub const TAU: usize = 60;
pub const BETA: i32 = 240;
pub const GAMMA: i32 = 1 << 19;
pub const RING_SIZE: usize = 11;
/// Largest ring `sign`/`verify`/`from_bytes` accept (DESIGN §5.1).
pub const MAX_RING_LEN: usize = 64;

pub const PUBLIC_KEY_LEN: usize = 2944; // k polys × 256 × 23 bits
pub const KEY_IMAGE_LEN: usize = 2944; // m polys × 256 × 23 bits
pub const Z_LEN: usize = 9600; // l polys × 256 × 20 bits

const _: () = {
    assert!(K == lattice::K && M == lattice::M && L == lattice::L);
    assert!(PUBLIC_KEY_LEN == lattice::packed23_len(K));
    assert!(KEY_IMAGE_LEN == lattice::packed23_len(M));
    assert!(Z_LEN == L * N * 20 / 8);
    assert!(BETA == TAU as i32 * ETA && ETA == ETA_SPEND + ETA_OFFSET);
};

/// Responses are accepted iff every coefficient satisfies `|z| < Z_BOUND`.
const Z_BOUND: i32 = GAMMA - BETA;

/// One-time (or spend) public key `t = A·s`.
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub struct PublicKey(pub Vec<u8>); // PUBLIC_KEY_LEN packed bytes

/// Key image `I = B·s_o`.
#[derive(Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct KeyImage(pub Vec<u8>); // KEY_IMAGE_LEN packed bytes

/// Long-term spend secret `s_spend` (coefficients in [−2, 2]). Zeroized on drop.
pub struct SpendSecret {
    /// L polys, coefficients mod q in [0, q).
    s: PolyVec,
}

/// One-time secret `s_o = s_spend + δ` (coefficients in [−4, 4]). Zeroized on drop.
pub struct OneTimeSecret {
    /// L polys, coefficients mod q in [0, q).
    s: PolyVec,
}

impl Drop for SpendSecret {
    fn drop(&mut self) {
        self.s.zeroize();
    }
}

impl Drop for OneTimeSecret {
    fn drop(&mut self) {
        self.s.zeroize();
    }
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RingSignature {
    pub ch0: [u8; 32],
    pub z: Vec<Vec<u8>>, // RING_SIZE (or bootstrap-ring) entries of Z_LEN bytes
}

// ====================================================================== helpers

/// L polynomials of centered integer coefficients (a response or a mask).
type ZVec = Vec<[i32; N]>;

/// `M·s` for a normal-form secret/public vector `s` (length L), normal form out.
fn mat_apply(m: &lattice::Matrix, s: &PolyVec) -> Vec<Poly> {
    let mut s_hat = s.clone();
    s_hat.ntt();
    let mut r = PolyVec(m.mul_ntt(&s_hat.0));
    s_hat.zeroize();
    r.inv_ntt();
    r.0
}

/// SampleSmall(seed, η = 2): for each poly r, RejBoundedPoly over
/// `XOF("LW1/lrs/small", seed, u32_le(r))`: each byte gives nibbles b0 (low)
/// then b1 (high); a nibble b < 15 yields coefficient `2 − (b mod 5)`.
fn sample_small(seed: &[u8; 32]) -> PolyVec {
    let mut out = PolyVec::zero(L);
    let mut buf = Zeroizing::new([0u8; 136]);
    for (r, poly) in out.0.iter_mut().enumerate() {
        let mut xs = XofStream::new("LW1/lrs/small", &[seed, &(r as u32).to_le_bytes()]);
        let mut i = 0usize;
        'fill: loop {
            xs.read(buf.as_mut());
            for &byte in buf.iter() {
                for nib in [byte & 0x0f, byte >> 4] {
                    if nib < 15 {
                        poly.0[i] = to_mod_q(2 - (nib as i32 % 5));
                        i += 1;
                        if i == N {
                            break 'fill;
                        }
                    }
                }
            }
        }
    }
    out
}

/// One-time offset `δ = SampleSmall(XOF("LW1/onetime", ss, u32_le(j), 32))`.
fn onetime_offset(shared: &[u8; 32], index: u32) -> PolyVec {
    let mut seed = Zeroizing::new([0u8; 32]);
    xof("LW1/onetime", &[shared, &index.to_le_bytes()], seed.as_mut());
    sample_small(&seed)
}

fn decode_pk_polys(pk: &PublicKey) -> Option<Vec<Poly>> {
    unpack23(&pk.0, K)
}

fn ntt_all(mut v: Vec<Poly>) -> Vec<Poly> {
    v.iter_mut().for_each(Poly::ntt);
    v
}

/// ML-DSA SampleInBall (FIPS 204 Alg. 29) over SHAKE256(ch), τ = 60.
/// Inputs are public, so the data-dependent loop is fine.
fn sample_in_ball(ch: &[u8; 32]) -> Poly {
    let mut h = Shake256::default();
    h.update(ch);
    let mut rd = h.finalize_xof();
    let mut sb = [0u8; 8];
    rd.read(&mut sb);
    let signs = u64::from_le_bytes(sb);
    let mut c = Poly::zero();
    for (k, i) in (N - TAU..N).enumerate() {
        let j = loop {
            let mut b = [0u8; 1];
            rd.read(&mut b);
            if (b[0] as usize) <= i {
                break b[0] as usize;
            }
        };
        c.0[i] = c.0[j];
        c.0[j] = if (signs >> k) & 1 == 1 { Q - 1 } else { 1 };
    }
    c
}

/// Response encoding (DESIGN §5.2): `γ − 1 − z` in 20 bits, l·256 coefficients.
fn encode_z(z: &[[i32; N]]) -> Vec<u8> {
    debug_assert_eq!(z.len(), L);
    let mut out = vec![0u8; Z_LEN];
    lattice::pack_bits(
        z.iter().flat_map(|p| p.iter()).map(|&c| {
            let v = GAMMA - 1 - c;
            assert!((0..1 << 20).contains(&v), "response coefficient not encodable");
            v as u32
        }),
        20,
        &mut out,
    );
    out
}

/// Decode a response. Wrong length ⇒ None. With `enforce_bound`, any
/// coefficient with `|z| ≥ γ − β` ⇒ None (the normal, strict decoder).
fn decode_z_inner(b: &[u8], enforce_bound: bool) -> Option<ZVec> {
    if b.len() != Z_LEN {
        return None;
    }
    let mut vals = vec![0u32; L * N];
    lattice::unpack_bits(b, 20, &mut vals);
    let mut z = vec![[0i32; N]; L];
    for (p, chunk) in z.iter_mut().zip(vals.chunks(N)) {
        for (d, &v) in p.iter_mut().zip(chunk) {
            let c = GAMMA - 1 - v as i32;
            if enforce_bound && c.abs() >= Z_BOUND {
                return None;
            }
            *d = c;
        }
    }
    Some(z)
}

fn decode_z(b: &[u8]) -> Option<ZVec> {
    decode_z_inner(b, true)
}

/// `R = H("LW1/lrs/ring", t_0 ‖ … ‖ t_{N−1})` — one part, the concatenation.
fn ring_hash(ring: &[PublicKey]) -> [u8; 32] {
    let mut cat = Vec::with_capacity(ring.len() * PUBLIC_KEY_LEN);
    for t in ring {
        cat.extend_from_slice(&t.0);
    }
    h256("LW1/lrs/ring", &[&cat])
}

/// Per-signature public context of the challenge chain.
struct ChainCtx<'a> {
    msg: &'a [u8; 32],
    ring_hash: [u8; 32],
    ki_bytes: &'a [u8],
    ki_hat: Vec<Poly>,
    t_hat: Vec<Vec<Poly>>,
}

impl ChainCtx<'_> {
    /// `H("LW1/lrs/ch", μ, R, I, u32_le(i), pack23(A·v − c·t_i), pack23(B·v − c·I))`
    /// with `c_hat` = NTT(c). Identical work whatever `v` and `c` are.
    fn step(&self, i: usize, v: &[[i32; N]], c_hat: &Poly) -> [u8; 32] {
        let mut v_hat: Vec<Poly> = Vec::with_capacity(L);
        for coeffs in v {
            let mut p = Poly::zero();
            for (d, &s) in p.0.iter_mut().zip(coeffs.iter()) {
                *d = to_mod_q(s);
            }
            p.ntt();
            v_hat.push(p);
        }
        let mut w1 = matrix_a().mul_ntt(&v_hat);
        let mut w2 = matrix_b().mul_ntt(&v_hat);
        for (w, t) in w1.iter_mut().zip(&self.t_hat[i]) {
            *w = w.sub(&c_hat.pointwise(t));
            w.inv_ntt();
        }
        for (w, ki) in w2.iter_mut().zip(&self.ki_hat) {
            *w = w.sub(&c_hat.pointwise(ki));
            w.inv_ntt();
        }
        let mut p1 = Zeroizing::new(pack23(&w1));
        let mut p2 = Zeroizing::new(pack23(&w2));
        let out = h256(
            "LW1/lrs/ch",
            &[self.msg, &self.ring_hash, self.ki_bytes, &(i as u32).to_le_bytes(), &p1, &p2],
        );
        v_hat.iter_mut().for_each(Zeroize::zeroize);
        w1.iter_mut().for_each(Zeroize::zeroize);
        w2.iter_mut().for_each(Zeroize::zeroize);
        p1.zeroize();
        p2.zeroize();
        out
    }
}

/// Signing randomness: an XOF stream, read in 640-byte blocks
/// (256 candidates of 20 bits each).
struct SignRng {
    xs: XofStream,
    buf: Zeroizing<[u8; 640]>,
    pos: usize, // in 20-bit candidates
}

impl SignRng {
    fn new(seed: &[u8; 32]) -> Self {
        SignRng { xs: XofStream::new("LW1/lrs/sign-rng", &[seed]), buf: Zeroizing::new([0u8; 640]), pos: 256 }
    }

    fn next20(&mut self) -> u32 {
        if self.pos == 256 {
            self.xs.read(self.buf.as_mut());
            self.pos = 0;
        }
        let bit = self.pos * 20;
        let byte = bit / 8;
        let mut w = [0u8; 4];
        let take = (self.buf.len() - byte).min(4);
        w[..take].copy_from_slice(&self.buf[byte..byte + take]);
        let v = (u32::from_le_bytes(w) >> (bit % 8)) & 0xF_FFFF;
        w.zeroize();
        self.pos += 1;
        v
    }

    /// Uniform in `[−half, half]` for every one of the L·256 coefficients.
    /// Rejection depends only on the random stream, never on secrets.
    fn uniform_vec(&mut self, half: i32) -> ZVec {
        let range = (2 * half + 1) as u32;
        debug_assert!(range <= 1 << 20);
        let mut z = vec![[0i32; N]; L];
        for p in z.iter_mut() {
            for c in p.iter_mut() {
                let v = loop {
                    let v = self.next20();
                    if v < range {
                        break v;
                    }
                };
                *c = v as i32 - half;
            }
        }
        z
    }
}

/// Branch-free: 0 iff every coefficient has `|x| < bound`, else nonzero.
fn exceeds(v: &[[i32; N]], bound: i32) -> i32 {
    let mut flag = 0i32;
    for p in v {
        for &x in p.iter() {
            let m = x >> 31;
            let a = (x ^ m) - m;
            flag |= (bound - 1 - a) >> 31;
        }
    }
    flag
}

// ====================================================================== API

impl PublicKey {
    /// Validates length and that every coefficient is < q.
    pub fn from_bytes(b: &[u8]) -> Result<Self> {
        unpack23(b, K).ok_or(CryptoError::Malformed)?;
        Ok(PublicKey(b.to_vec()))
    }
}

impl KeyImage {
    pub fn from_bytes(b: &[u8]) -> Result<Self> {
        unpack23(b, M).ok_or(CryptoError::Malformed)?;
        Ok(KeyImage(b.to_vec()))
    }
    /// Compact 32-byte id for indexing: `H("LW1/lrs/ki-id", bytes)`.
    pub fn id(&self) -> [u8; 32] {
        h256("LW1/lrs/ki-id", &[&self.0])
    }
}

impl SpendSecret {
    /// `SampleSmall(spend_seed, η_spend)` (DESIGN §3.1).
    pub fn from_seed(seed: &[u8; 32]) -> Self {
        SpendSecret { s: sample_small(seed) }
    }
    pub fn public(&self) -> PublicKey {
        PublicKey(pack23(&mat_apply(matrix_a(), &self.s)))
    }
}

impl OneTimeSecret {
    pub fn public(&self) -> PublicKey {
        PublicKey(pack23(&mat_apply(matrix_a(), &self.s)))
    }
    pub fn key_image(&self) -> KeyImage {
        KeyImage(pack23(&mat_apply(matrix_b(), &self.s)))
    }
}

/// Sender side: `t_o = t_spend + A·δ(shared, index)`.
///
/// Panics if `spend_pub` is not a valid encoding (construct it with
/// [`PublicKey::from_bytes`]).
pub fn derive_onetime_public(spend_pub: &PublicKey, shared: &[u8; 32], index: u32) -> PublicKey {
    let t = decode_pk_polys(spend_pub).expect("derive_onetime_public: malformed spend public key");
    let mut delta = onetime_offset(shared, index);
    let ad = mat_apply(matrix_a(), &delta);
    delta.zeroize();
    let to: Vec<Poly> = t.iter().zip(&ad).map(|(a, b)| a.add(b)).collect();
    PublicKey(pack23(&to))
}

/// Recipient side: `s_o = s_spend + δ(shared, index)`.
pub fn derive_onetime_secret(spend: &SpendSecret, shared: &[u8; 32], index: u32) -> OneTimeSecret {
    let mut delta = onetime_offset(shared, index);
    let s = spend.s.add(&delta);
    delta.zeroize();
    OneTimeSecret { s }
}

/// Sign `msg` with `secret`, which must correspond to `ring[signer]`.
/// Panics if it doesn't (caller bug). Ring order is the caller's (sorted by
/// global index in practice).
pub fn sign(msg: &[u8; 32], ring: &[PublicKey], signer: usize, secret: &OneTimeSecret) -> RingSignature {
    let mut rnd = Zeroizing::new([0u8; 32]);
    crate::lw::kdf::random_bytes(rnd.as_mut());
    sign_inner(msg, ring, signer, secret, &rnd, |_, _| {})
}

/// [`sign`] that returns `Malformed` instead of panicking when the ring size
/// is outside `1..=MAX_RING_LEN`, `signer` is out of range, a ring member
/// does not decode canonically, or `secret` does not match `ring[signer]`.
pub fn try_sign(msg: &[u8; 32], ring: &[PublicKey], signer: usize, secret: &OneTimeSecret) -> Result<RingSignature> {
    let n = ring.len();
    if !(1..=MAX_RING_LEN).contains(&n) || signer >= n {
        return Err(CryptoError::Malformed);
    }
    if ring.iter().any(|pk| decode_pk_polys(pk).is_none()) {
        return Err(CryptoError::Malformed);
    }
    if secret.public() != ring[signer] {
        return Err(CryptoError::Malformed);
    }
    Ok(sign(msg, ring, signer, secret))
}

/// [`derive_onetime_public`] that returns `Malformed` instead of panicking on
/// a non-canonical spend public key.
pub fn try_derive_onetime_public(spend_pub: &PublicKey, shared: &[u8; 32], index: u32) -> Result<PublicKey> {
    decode_pk_polys(spend_pub).ok_or(CryptoError::Malformed)?;
    Ok(derive_onetime_public(spend_pub, shared, index))
}

/// The signing algorithm (DESIGN §5.4). `tamper` is a test hook applied to
/// each simulated response before it is hashed; production passes a no-op.
fn sign_inner(
    msg: &[u8; 32],
    ring: &[PublicKey],
    signer: usize,
    secret: &OneTimeSecret,
    os_rnd: &[u8; 32],
    tamper: impl Fn(usize, &mut ZVec),
) -> RingSignature {
    let n = ring.len();
    assert!((1..=MAX_RING_LEN).contains(&n), "lrs::sign: ring size must be 1..={MAX_RING_LEN}");
    assert!(signer < n, "lrs::sign: signer index out of range");
    let t_hat: Vec<Vec<Poly>> = ring
        .iter()
        .map(|pk| ntt_all(decode_pk_polys(pk).expect("lrs::sign: malformed ring member")))
        .collect();
    assert!(secret.public() == ring[signer], "lrs::sign: secret does not match ring[signer]");

    let ki = mat_apply(matrix_b(), &secret.s);
    let ki_bytes = pack23(&ki);
    let ctx = ChainCtx { msg, ring_hash: ring_hash(ring), ki_bytes: &ki_bytes, ki_hat: ntt_all(ki), t_hat };

    // Hedged seed: secret ‖ μ ‖ R ‖ I ‖ OS randomness.
    let mut s_bytes = Zeroizing::new(Vec::with_capacity(L * N * 4));
    for p in &secret.s.0 {
        for &c in p.0.iter() {
            s_bytes.extend_from_slice(&(c as u32).to_le_bytes());
        }
    }
    let seed = Zeroizing::new(h256(
        "LW1/lrs/sign-seed",
        &[&s_bytes, msg, &ctx.ring_hash, &ki_bytes, os_rnd],
    ));
    drop(s_bytes);
    let mut rng = SignRng::new(&seed);
    drop(seed);

    let mut s_hat = secret.s.clone();
    s_hat.ntt();
    let zero_c = Poly::zero();
    let mut ch = vec![[0u8; 32]; n];
    let mut z: Vec<ZVec> = vec![Vec::new(); n];

    loop {
        // Signer commitment: same routine as everyone else, with c = 0.
        let mut y = Zeroizing::new(rng.uniform_vec(GAMMA - 1));
        ch[(signer + 1) % n] = ctx.step(signer, &y, &zero_c);
        for k in 1..n {
            let i = (signer + k) % n;
            let mut zi = rng.uniform_vec(Z_BOUND - 1);
            tamper(i, &mut zi);
            let mut c_hat = sample_in_ball(&ch[i]);
            c_hat.ntt();
            ch[(i + 1) % n] = ctx.step(i, &zi, &c_hat);
            z[i] = zi;
        }

        // z_π = y + c_π·s_o, exact over the integers since |c·s|∞ ≤ τ·η = β.
        let mut c_hat = sample_in_ball(&ch[signer]);
        c_hat.ntt();
        let mut zp: Zeroizing<ZVec> = Zeroizing::new(vec![[0i32; N]; L]);
        let mut cs_flag = 0i32;
        for (r, s) in s_hat.0.iter().enumerate() {
            let mut cs = c_hat.pointwise(s);
            cs.inv_ntt();
            for j in 0..N {
                let v = center(cs.0[j]);
                let m = v >> 31;
                cs_flag |= (BETA - ((v ^ m) - m)) >> 31; // |v| > β ⇒ flag
                zp[r][j] = y[r][j] + v;
            }
            cs.zeroize();
        }
        y.zeroize();
        assert!(cs_flag == 0, "lrs::sign: one-time secret out of range");

        if exceeds(&zp, Z_BOUND) == 0 {
            z[signer] = zp.to_vec();
            zp.zeroize();
            break;
        }
        // Rejected: the would-be response (y + c·s) is not uniform, discard it.
        zp.zeroize();
    }
    s_hat.zeroize();

    let out = RingSignature { ch0: ch[0], z: z.iter().map(|zi| encode_z(zi)).collect() };
    if let Some(zs) = z.get_mut(signer) {
        zs.zeroize();
    }
    out
}

pub fn verify(msg: &[u8; 32], ring: &[PublicKey], key_image: &KeyImage, sig: &RingSignature) -> bool {
    verify_inner(msg, ring, key_image, sig, true).unwrap_or(false)
}

fn verify_inner(
    msg: &[u8; 32],
    ring: &[PublicKey],
    key_image: &KeyImage,
    sig: &RingSignature,
    enforce_bound: bool,
) -> Option<bool> {
    let n = ring.len();
    if !(1..=MAX_RING_LEN).contains(&n) || sig.z.len() != n {
        return None;
    }
    let ki = unpack23(&key_image.0, M)?;
    let mut t_hat = Vec::with_capacity(n);
    for pk in ring {
        t_hat.push(ntt_all(decode_pk_polys(pk)?));
    }
    let mut zs = Vec::with_capacity(n);
    for zb in &sig.z {
        zs.push(decode_z_inner(zb, enforce_bound)?);
    }
    let ctx = ChainCtx { msg, ring_hash: ring_hash(ring), ki_bytes: &key_image.0, ki_hat: ntt_all(ki), t_hat };
    let mut ch = sig.ch0;
    for (i, zi) in zs.iter().enumerate() {
        let mut c_hat = sample_in_ball(&ch);
        c_hat.ntt();
        ch = ctx.step(i, zi, &c_hat);
    }
    Some(ch == sig.ch0)
}

impl RingSignature {
    /// `ch0 ‖ z_0 ‖ … ‖ z_{n-1}` (n known from the input's ring length).
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(32 + self.z.len() * Z_LEN);
        out.extend_from_slice(&self.ch0);
        for z in &self.z {
            out.extend_from_slice(z);
        }
        out
    }
    pub fn from_bytes(b: &[u8], ring_len: usize) -> Result<Self> {
        if !(1..=MAX_RING_LEN).contains(&ring_len) || b.len() != 32 + ring_len * Z_LEN {
            return Err(CryptoError::Malformed);
        }
        let mut ch0 = [0u8; 32];
        ch0.copy_from_slice(&b[..32]);
        let mut z = Vec::with_capacity(ring_len);
        for chunk in b[32..].chunks_exact(Z_LEN) {
            decode_z(chunk).ok_or(CryptoError::Malformed)?;
            z.push(chunk.to_vec());
        }
        Ok(RingSignature { ch0, z })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Instant;

    fn seed(tag: u32) -> [u8; 32] {
        h256("LW1/test/seed", &[&tag.to_le_bytes()])
    }

    /// `n` one-time secrets (each from its own spend key) and their public keys.
    fn make_ring(n: usize, tag: u32) -> (Vec<OneTimeSecret>, Vec<PublicKey>) {
        let mut secrets = Vec::new();
        let mut ring = Vec::new();
        for i in 0..n {
            let sp = SpendSecret::from_seed(&seed(tag * 1000 + i as u32));
            let ss = seed(tag * 1000 + 500 + i as u32);
            let ot = derive_onetime_secret(&sp, &ss, i as u32);
            ring.push(ot.public());
            secrets.push(ot);
        }
        (secrets, ring)
    }

    fn coeffs_mod_q_small(v: &PolyVec, eta: i32) -> bool {
        v.0.iter().flat_map(|p| p.0.iter()).all(|&c| center(c).abs() <= eta)
    }

    #[test]
    fn sample_small_range_and_distribution() {
        let s = sample_small(&seed(1));
        assert_eq!(s.0.len(), L);
        assert!(coeffs_mod_q_small(&s, 2));
        let mut hist = [0usize; 5];
        for c in s.0.iter().flat_map(|p| p.0.iter()) {
            hist[(center(*c) + 2) as usize] += 1;
        }
        for h in hist {
            // 3840/5 = 768 expected each.
            assert!((600..940).contains(&h), "{hist:?}");
        }
        assert_ne!(sample_small(&seed(1)), sample_small(&seed(2)));
    }

    #[test]
    fn sample_in_ball_weight() {
        for t in 0..50u32 {
            let c = sample_in_ball(&seed(t));
            let nz = c.0.iter().filter(|&&x| x != 0).count();
            assert_eq!(nz, TAU);
            assert!(c.0.iter().all(|&x| x == 0 || x == 1 || x == Q - 1));
        }
    }

    #[test]
    fn keys_deterministic_and_onetime_consistent() {
        let sp = SpendSecret::from_seed(&seed(7));
        let sp2 = SpendSecret::from_seed(&seed(7));
        assert_eq!(sp.public(), sp2.public());
        assert_ne!(sp.public(), SpendSecret::from_seed(&seed(8)).public());
        let pk = sp.public();
        assert_eq!(pk.0.len(), PUBLIC_KEY_LEN);
        assert!(PublicKey::from_bytes(&pk.0).is_ok());
        for j in 0..5u32 {
            let ss = seed(100 + j);
            let ot = derive_onetime_secret(&sp, &ss, j);
            assert!(coeffs_mod_q_small(&ot.s, ETA));
            assert_eq!(derive_onetime_public(&pk, &ss, j), ot.public());
            assert_ne!(ot.public(), pk);
            // Different index ⇒ different key and image.
            let ot2 = derive_onetime_secret(&sp, &ss, j + 1);
            assert_ne!(ot.public(), ot2.public());
            assert_ne!(ot.key_image(), ot2.key_image());
            assert_eq!(ot.key_image().0.len(), KEY_IMAGE_LEN);
            assert_eq!(ot.key_image(), derive_onetime_secret(&sp2, &ss, j).key_image());
        }
    }

    #[test]
    fn decoders_reject_bad_input() {
        let pk = SpendSecret::from_seed(&seed(9)).public();
        assert!(PublicKey::from_bytes(&pk.0[..PUBLIC_KEY_LEN - 1]).is_err());
        assert!(PublicKey::from_bytes(&[pk.0.clone(), vec![0]].concat()).is_err());
        assert!(PublicKey::from_bytes(&[0xff; PUBLIC_KEY_LEN]).is_err());
        assert!(KeyImage::from_bytes(&[0xff; KEY_IMAGE_LEN]).is_err());
        assert!(KeyImage::from_bytes(&[]).is_err());
        assert!(KeyImage::from_bytes(&[0; KEY_IMAGE_LEN]).is_ok());

        // z: roundtrip, extremes, out-of-range.
        let mut z = vec![[0i32; N]; L];
        z[0][0] = Z_BOUND - 1;
        z[14][255] = -(Z_BOUND - 1);
        z[3][7] = -12345;
        let b = encode_z(&z);
        assert_eq!(b.len(), Z_LEN);
        assert_eq!(decode_z(&b).unwrap(), z);
        for bad in [Z_BOUND, -Z_BOUND, GAMMA - 1, -GAMMA] {
            let mut zb = z.clone();
            zb[5][5] = bad;
            let enc = encode_z(&zb);
            assert!(decode_z(&enc).is_none(), "{bad}");
            assert_eq!(decode_z_inner(&enc, false).unwrap(), zb);
        }
        assert!(decode_z(&b[1..]).is_none());
        // All-ones 20-bit fields ⇒ z = γ − 1 − (2^20 − 1) = −γ: rejected.
        assert!(decode_z(&[0xff; Z_LEN]).is_none());
        assert!(decode_z(&[0x00; Z_LEN]).is_none()); // z = γ − 1

        assert!(RingSignature::from_bytes(&[0; 32], 0).is_err());
        assert!(RingSignature::from_bytes(&vec![0; 32 + 65 * Z_LEN], 65).is_err());
    }

    #[test]
    fn sign_verify_ring_sizes_and_positions() {
        let msg = seed(42);
        for &n in &[1usize, 2, 3, 11] {
            let (secrets, ring) = make_ring(n, n as u32);
            let mut positions = vec![0, n / 2, n - 1];
            positions.dedup();
            for &pi in &positions {
                let sig = sign(&msg, &ring, pi, &secrets[pi]);
                let ki = secrets[pi].key_image();
                assert!(verify(&msg, &ring, &ki, &sig), "n={n} pi={pi}");
                let bytes = sig.to_bytes();
                assert_eq!(bytes.len(), 32 + n * Z_LEN);
                let sig2 = RingSignature::from_bytes(&bytes, n).unwrap();
                assert_eq!(sig2, sig);
                assert!(verify(&msg, &ring, &ki, &sig2));
                assert!(RingSignature::from_bytes(&bytes, n + 1).is_err());
                assert!(RingSignature::from_bytes(&bytes[..bytes.len() - 1], n).is_err());
            }
        }
    }

    #[test]
    fn signing_is_randomized_and_hedged() {
        let (secrets, ring) = make_ring(3, 77);
        let msg = seed(1);
        let a = sign(&msg, &ring, 1, &secrets[1]);
        let b = sign(&msg, &ring, 1, &secrets[1]);
        assert_ne!(a, b);
        // With a fixed (e.g. broken) OS RNG the output is still a valid
        // signature, deterministic in (secret, μ, ring), and differs per message.
        let c1 = sign_inner(&msg, &ring, 1, &secrets[1], &[0; 32], |_, _| {});
        let c2 = sign_inner(&msg, &ring, 1, &secrets[1], &[0; 32], |_, _| {});
        let c3 = sign_inner(&seed(2), &ring, 1, &secrets[1], &[0; 32], |_, _| {});
        assert_eq!(c1, c2);
        assert_ne!(c1.ch0, c3.ch0);
        assert!(verify(&msg, &ring, &secrets[1].key_image(), &c1));
    }

    #[test]
    #[should_panic(expected = "does not match")]
    fn sign_panics_on_wrong_secret() {
        let (secrets, ring) = make_ring(3, 78);
        sign(&seed(0), &ring, 0, &secrets[1]);
    }

    #[test]
    fn verify_rejects_tampering() {
        let n = 11;
        let (secrets, ring) = make_ring(n, 21);
        let pi = 4;
        let msg = seed(5);
        let ki = secrets[pi].key_image();
        let sig = sign(&msg, &ring, pi, &secrets[pi]);
        assert!(verify(&msg, &ring, &ki, &sig));

        // Different message.
        assert!(!verify(&seed(6), &ring, &ki, &sig));
        // A ring member swapped for an outsider (signer and non-signer slots).
        let (others, _) = make_ring(1, 99);
        for slot in [pi, 0, n - 1] {
            let mut r2 = ring.clone();
            r2[slot] = others[0].public();
            assert!(!verify(&msg, &r2, &ki, &sig), "slot {slot}");
        }
        // Ring reordered.
        let mut r3 = ring.clone();
        r3.swap(1, 2);
        assert!(!verify(&msg, &r3, &ki, &sig));
        let mut r4 = ring.clone();
        r4.rotate_left(1);
        assert!(!verify(&msg, &r4, &ki, &sig));
        // Ring with a member dropped / added.
        assert!(!verify(&msg, &ring[..n - 1], &ki, &sig));
        // Wrong key images.
        assert!(!verify(&msg, &ring, &secrets[0].key_image(), &sig));
        assert!(!verify(&msg, &ring, &others[0].key_image(), &sig));
        let mut ki_bad = ki.clone();
        ki_bad.0[100] ^= 1;
        assert!(!verify(&msg, &ring, &ki_bad, &sig));
        assert!(!verify(&msg, &ring, &KeyImage(vec![0xff; KEY_IMAGE_LEN]), &sig));
        // ch0 flipped.
        for bit in [0usize, 77, 255] {
            let mut s2 = sig.clone();
            s2.ch0[bit / 8] ^= 1 << (bit % 8);
            assert!(!verify(&msg, &ring, &ki, &s2));
        }
        // Any z bit flipped (every entry, several positions incl. signer's).
        for i in 0..n {
            for pos in [0usize, 1234, Z_LEN - 1] {
                let mut s2 = sig.clone();
                s2.z[i][pos] ^= 0x01;
                assert!(!verify(&msg, &ring, &ki, &s2), "z[{i}][{pos}]");
            }
        }
        // z entries swapped, truncated, missing.
        let mut s3 = sig.clone();
        s3.z.swap(0, 1);
        assert!(!verify(&msg, &ring, &ki, &s3));
        let mut s4 = sig.clone();
        s4.z[3].pop();
        assert!(!verify(&msg, &ring, &ki, &s4));
        let mut s5 = sig.clone();
        s5.z.pop();
        assert!(!verify(&msg, &ring, &ki, &s5));
        // Empty ring.
        assert!(!verify(&msg, &[], &ki, &RingSignature { ch0: sig.ch0, z: vec![] }));
    }

    #[test]
    fn verify_rejects_out_of_bound_z_even_if_chain_closes() {
        let (secrets, ring) = make_ring(3, 31);
        let msg = seed(3);
        let ki = secrets[0].key_image();
        for big in [Z_BOUND, -Z_BOUND, GAMMA - 1] {
            // Simulated member 2 gets one coefficient at |z| ≥ γ − β; the
            // chain is computed over the tampered value so it still closes.
            let sig = sign_inner(&msg, &ring, 0, &secrets[0], &[1; 32], |i, z| {
                if i == 2 {
                    z[7][9] = big;
                }
            });
            assert_eq!(verify_inner(&msg, &ring, &ki, &sig, false), Some(true), "chain must close");
            assert!(!verify(&msg, &ring, &ki, &sig));
            assert!(RingSignature::from_bytes(&sig.to_bytes(), 3).is_err());
        }
    }

    #[test]
    fn linkability() {
        let (secrets, ring_a) = make_ring(4, 41);
        let (_, mut ring_b) = make_ring(5, 42);
        ring_b[3] = ring_a[2].clone();
        let s = &secrets[2];
        let sig_a = sign(&seed(10), &ring_a, 2, s);
        let sig_b = sign(&seed(11), &ring_b, 3, s);
        let ki = s.key_image();
        assert!(verify(&seed(10), &ring_a, &ki, &sig_a));
        assert!(verify(&seed(11), &ring_b, &ki, &sig_b));
        // The only key image either verifies under is the same one.
        assert!(!verify(&seed(10), &ring_a, &secrets[1].key_image(), &sig_a));
        // Different secrets ⇒ different images.
        let images: Vec<_> = secrets.iter().map(|s| s.key_image().id()).collect();
        for i in 0..images.len() {
            for j in i + 1..images.len() {
                assert_ne!(images[i], images[j]);
            }
        }
    }

    /// Documents DESIGN §5.5 "sender spend-tracing": a sender who knows two
    /// offsets δ1, δ2 for the same recipient can link the two key images.
    #[test]
    fn known_issue_sender_can_link_key_images() {
        let sp = SpendSecret::from_seed(&seed(300));
        let ss = seed(301);
        let i1 = unpack23(&derive_onetime_secret(&sp, &ss, 0).key_image().0, M).unwrap();
        let i2 = unpack23(&derive_onetime_secret(&sp, &ss, 1).key_image().0, M).unwrap();
        let bd1 = mat_apply(matrix_b(), &onetime_offset(&ss, 0));
        let bd2 = mat_apply(matrix_b(), &onetime_offset(&ss, 1));
        let x1: Vec<Poly> = i1.iter().zip(&bd1).map(|(a, b)| a.sub(b)).collect();
        let x2: Vec<Poly> = i2.iter().zip(&bd2).map(|(a, b)| a.sub(b)).collect();
        assert_eq!(x1, x2, "if this ever fails, the key image construction changed: update DESIGN §5.5");
    }

    #[test]
    fn statistical_signer_indistinguishable_by_norm() {
        let (secrets, ring) = make_ring(2, 51);
        let sigs = 300;
        let (mut sum_sig, mut sum_other) = (0f64, 0f64);
        let mut max_sig = 0i32;
        for k in 0..sigs {
            let pi = k % 2;
            let sig = sign(&seed(1000 + k as u32), &ring, pi, &secrets[pi]);
            let zs = decode_z(&sig.z[pi]).expect("valid");
            let zo = decode_z(&sig.z[1 - pi]).expect("valid");
            for (a, b) in zs.iter().flatten().zip(zo.iter().flatten()) {
                sum_sig += a.abs() as f64;
                sum_other += b.abs() as f64;
                max_sig = max_sig.max(a.abs());
            }
        }
        assert!(max_sig < Z_BOUND);
        let cnt = (sigs * L * N) as f64;
        let (m_sig, m_other) = (sum_sig / cnt, sum_other / cnt);
        let expect = Z_BOUND as f64 / 2.0;
        assert!((m_sig / m_other - 1.0).abs() < 0.01, "{m_sig} vs {m_other}");
        assert!((m_sig / expect - 1.0).abs() < 0.01, "{m_sig} vs {expect}");
    }

    #[test]
    fn timing_ring_11() {
        let (secrets, ring) = make_ring(RING_SIZE, 61);
        let msg = seed(9);
        let ki = secrets[5].key_image();
        let _ = sign(&msg, &ring, 5, &secrets[5]); // warm caches (matrix expansion)
        let iters = 10u32;
        let t0 = Instant::now();
        let mut sigs = Vec::new();
        for _ in 0..iters {
            sigs.push(sign(&msg, &ring, 5, &secrets[5]));
        }
        let ts = t0.elapsed() / iters;
        let t1 = Instant::now();
        for s in &sigs {
            assert!(verify(&msg, &ring, &ki, s));
        }
        let tv = t1.elapsed() / iters;
        println!(
            "LRS ring-11: sign {:?}/sig, verify {:?}/sig, signature {} B",
            ts,
            tv,
            sigs[0].to_bytes().len()
        );
    }
}

← Back to index