Skip to main content

kopis/
sample.rs

1//! This file implements methods for generating uniform matrices and binomially distributed vectors
2
3use crate::{
4    arithmetic::{Matrix, RingElem},
5    consts::{DOMSEP_GENMAT, DOMSEP_GENSEC, MAX_MU, MODULUS_Q_BITS, RING_DEG},
6};
7
8use turboshake::digest::{ExtendableOutput, Update, XofReader};
9use turboshake::{CTurboShake128, CTurboShake256};
10
11/// One CBD coefficient: popcount of the low `half` bits of `raw`, minus popcount of the
12/// next `half` bits. `mask` must be `(1 << half) - 1`.
13///
14/// This is a free function rather than a closure inside `cbd` on purpose: aeneas extracts a
15/// closure declared inside a const-generic function as a type parameterized by that const
16/// (`gen.cbd.closure_1 (MU : Usize) := Slice U8`), which does not mention it, so `MU` becomes
17/// un-inferable at every call site and the generated Lean does not typecheck.
18fn cbd_diff(raw: u32, half: u32, mask: u32) -> u16 {
19    let a = (raw & mask).count_ones() as u16;
20    let b = ((raw >> half) & mask).count_ones() as u16;
21    a.wrapping_sub(b)
22}
23
24/// Computes the Centered Binomial Distribution using the given bytes as randomness
25fn cbd<const MU: usize>(buf: &[u8], out: &mut RingElem) {
26    assert_eq!(buf.len(), RING_DEG * MU / 8);
27
28    // The three parameter sets use MU = 8, 10, and 6, and each gets a specialized branchless
29    // path. Since MU is a const generic, this dispatch is resolved at compile time. The final
30    // branch is a generic fallback for any other MU.
31    if MU == 8 {
32        // Kopis-768: Each coefficient uses exactly 1 byte, with the low nibble as the positive
33        // half and the high nibble as the negative half. So we don't need a buffer to read bits
34        for (i, &byte) in buf.iter().enumerate() {
35            let a = (byte & 0x0F).count_ones() as u16;
36            let b = (byte >> 4).count_ones() as u16;
37            out.0[i] = a.wrapping_sub(b);
38        }
39    } else if MU == 10 {
40        // Kopis-512: 4 coefficients per 5-byte group. Coefficient k of a group occupies bits
41        // [10k, 10k+10), which always fit in the two bytes starting at byte 10k/8. The low 5
42        // bits are the positive half and the next 5 bits are the negative half.
43        for g in 0..RING_DEG / 4 {
44            let i = 5 * g;
45            let b0 = buf[i] as u32;
46            let b1 = buf[i + 1] as u32;
47            let b2 = buf[i + 2] as u32;
48            let b3 = buf[i + 3] as u32;
49            let b4 = buf[i + 4] as u32;
50            let o = 4 * g;
51            out.0[o] = cbd_diff(b0 | (b1 << 8), 5, 0x1f);
52            out.0[o + 1] = cbd_diff((b1 | (b2 << 8)) >> 2, 5, 0x1f);
53            out.0[o + 2] = cbd_diff((b2 | (b3 << 8)) >> 4, 5, 0x1f);
54            out.0[o + 3] = cbd_diff((b3 | (b4 << 8)) >> 6, 5, 0x1f);
55        }
56    } else if MU == 6 {
57        // Kopis-1024: 4 coefficients per 3-byte group. Coefficient k of a group occupies bits
58        // [6k, 6k+6). The low 3 bits are the positive half, the next 3 the negative half.
59        for g in 0..RING_DEG / 4 {
60            let i = 3 * g;
61            let b0 = buf[i] as u32;
62            let b1 = buf[i + 1] as u32;
63            let b2 = buf[i + 2] as u32;
64            let o = 4 * g;
65            out.0[o] = cbd_diff(b0, 3, 0x07);
66            out.0[o + 1] = cbd_diff((b0 | (b1 << 8)) >> 6, 3, 0x07);
67            out.0[o + 2] = cbd_diff((b1 | (b2 << 8)) >> 4, 3, 0x07);
68            out.0[o + 3] = cbd_diff(b2 >> 2, 3, 0x07);
69        }
70    } else {
71        // Generic fallback: read MU bits at a time, spanning up to 3 bytes when not
72        // byte-aligned. Worst case: MU=10 starting at bit 7 needs bits 7..16, spanning 3 bytes.
73        let half = MU / 2;
74        let mask: u32 = (1 << half) - 1;
75
76        let mut bit_pos = 0;
77        for coeff in out.0.iter_mut() {
78            let byte_idx = bit_pos / 8;
79            let bit_in_byte = bit_pos % 8;
80
81            let mut raw: u32 = buf[byte_idx] as u32;
82            if byte_idx + 1 < buf.len() {
83                raw |= (buf[byte_idx + 1] as u32) << 8;
84            }
85            if byte_idx + 2 < buf.len() {
86                raw |= (buf[byte_idx + 2] as u32) << 16;
87            }
88            raw >>= bit_in_byte;
89
90            let a = (raw & mask).count_ones() as u16;
91            let b = ((raw >> half) & mask).count_ones() as u16;
92            *coeff = a.wrapping_sub(b);
93
94            bit_pos += MU;
95        }
96    }
97}
98
99/// Uses a random seed to generate an MLWR secret, i.e., an element in R^ℓ whose entries are
100/// sampled according to a binomial distribution.
101pub(crate) fn gen_secret_from_seed<const L: usize, const MU: usize>(
102    seed: &[u8; 32],
103) -> Matrix<L, 1> {
104    let mut secret = Matrix::default();
105    // Buffer to hold XOF bytes. Can't do const math here, so we make it the max size
106    // and cut it down
107    let mut backing_buf = [0u8; RING_DEG * MAX_MU / 8];
108    let buf = &mut backing_buf[..RING_DEG * MU / 8];
109
110    // Sample the secret using the Centered Binomial Distribution
111    for i in 0..L {
112        let mut hasher = CTurboShake256::<DOMSEP_GENSEC>::default();
113        hasher.update(seed);
114        hasher.update(&[i as u8]);
115        let mut reader = hasher.finalize_xof();
116        reader.read(buf);
117        cbd::<MU>(buf, &mut secret.0[i][0]);
118    }
119
120    secret
121}
122
123/// Uses a random seed to generate a uniform matrix in R^{ℓ×ℓ}.
124///
125/// For each element (i,j), we compute TurboSHAKE128(seed || i || j, 256*13/8, DOMSEP_GENMAT).
126pub(crate) fn gen_matrix_from_seed<const L: usize>(seed: &[u8; 32]) -> Matrix<L, L> {
127    // Our output is a matrix of ring elements
128    let mut mat = Matrix::default();
129    // For each ring element we need to sample the same number of bytes
130    let mut buf = [0u8; RING_DEG * MODULUS_Q_BITS / 8];
131
132    // Construct the matrix entries
133    for i in 0..L {
134        for j in 0..L {
135            let mut hasher = CTurboShake128::<DOMSEP_GENMAT>::default();
136            hasher.update(seed);
137            hasher.update(&[i as u8]);
138            hasher.update(&[j as u8]);
139            let mut reader = hasher.finalize_xof();
140            reader.read(&mut buf);
141            mat.0[i][j] = RingElem::deserialize(&buf, MODULUS_Q_BITS);
142        }
143    }
144
145    mat
146}
147
148#[cfg(test)]
149mod test {
150    use super::*;
151
152    use rand::RngCore;
153
154    /// Naive bit-by-bit CBD for testing: coefficient c is
155    /// popcount(bits [c*MU, c*MU + MU/2)) - popcount(bits [c*MU + MU/2, c*MU + MU))
156    fn cbd_reference<const MU: usize>(buf: &[u8], out: &mut RingElem) {
157        let bit = |i: usize| ((buf[i / 8] >> (i % 8)) & 1) as u16;
158        for (c, coeff) in out.0.iter_mut().enumerate() {
159            let mut a = 0u16;
160            let mut b = 0u16;
161            for k in 0..MU / 2 {
162                a += bit(c * MU + k);
163                b += bit(c * MU + MU / 2 + k);
164            }
165            *coeff = a.wrapping_sub(b);
166        }
167    }
168
169    // The specialized MU=6, 8, and 10 paths must agree with the naive bit-by-bit CBD
170    #[test]
171    fn specialized_cbd_matches_reference() {
172        fn check<const MU: usize>(rng: &mut impl RngCore) {
173            let mut backing_buf = [0u8; RING_DEG * MAX_MU / 8];
174            let buf = &mut backing_buf[..RING_DEG * MU / 8];
175
176            for _ in 0..100 {
177                rng.fill_bytes(buf);
178                let mut fast = RingElem::default();
179                let mut reference = RingElem::default();
180                cbd::<MU>(buf, &mut fast);
181                cbd_reference::<MU>(buf, &mut reference);
182                assert_eq!(fast.0, reference.0);
183            }
184        }
185
186        let mut rng = rand::rng();
187        check::<6>(&mut rng);
188        check::<8>(&mut rng);
189        check::<10>(&mut rng);
190    }
191}