Skip to main content

kopis/
pke.rs

1//! This file implements the IND-CPA-secure Kopis PKE scheme
2
3use crate::{
4    arithmetic::{Matrix, NttMatrix, RingElem},
5    consts::{
6        DOMSEP_KGEXPAND, DOMSEP_PKHASH, MAX_L, MAX_T, MODULUS_P_BITS, MODULUS_Q_BITS, RING_DEG,
7    },
8    sample::{gen_matrix_from_seed, gen_secret_from_seed},
9    ser::deserialize_generic,
10    turboshake256_hash,
11};
12
13use turboshake::CTurboShake256;
14use turboshake::digest::{ExtendableOutput, Update, XofReader};
15use zeroize::{Zeroize, ZeroizeOnDrop};
16
17const H1_VAL: u16 = 1 << (MODULUS_Q_BITS - MODULUS_P_BITS - 1);
18
19/// The serialized length of one public-vector ring element: an element of `R_p`, packed at
20/// `MODULUS_P_BITS` (10) bits per coefficient, i.e. 320 bytes.
21const PK_VEC_ELEM_BYTES: usize = MODULUS_P_BITS * RING_DEG / 8;
22
23/// A secret key for the IND-CPA-secure Kopis PKE scheme (expanded form, NTT domain).
24///
25/// This wraps the secret vector `s`, so it zeroes itself from memory when dropped. Note that
26/// `s` is stored in the NTT domain, which is an invertible linear image of the coefficient
27/// form, so it is exactly as sensitive and must be zeroed just the same.
28#[derive(Zeroize, ZeroizeOnDrop)]
29pub(crate) struct PkeSecretKey<const L: usize>(NttMatrix<L, 1>);
30
31/// A public key for the IND-CPA-secure Kopis PKE scheme.
32///
33/// The key stores its own serialized form (`vec_bytes` followed by `matrix_seed` is exactly
34/// what `serialize` emits and `from_bytes` reads) alongside the NTT-domain data the encryptor
35/// needs. It deliberately does *not* keep the structured `Matrix<L, 1>` public vector: that was
36/// held only to be serializable, and the packed bytes serve that purpose with no reconstruction.
37#[derive(Clone)]
38pub struct PkePublicKey<const L: usize> {
39    /// The seed used to generate `mat_a_ntt`; also the 32-byte tail of the serialization.
40    matrix_seed: [u8; 32],
41    /// The expanded public matrix, in NTT form. Precomputed here so that repeated encryptions
42    /// (e.g. every encapsulation and every FO re-encryption during decapsulation) don't have to
43    /// re-run the XOF that derives it from `matrix_seed`, nor re-transform it. This mirrors the
44    /// "unpacked" public-key form used by other KEM implementations. It is never serialized:
45    /// `serialize` writes only `vec_bytes || matrix_seed`, and `from_bytes` re-derives it.
46    mat_a_ntt: NttMatrix<L, L>,
47    /// The serialized public vector `b`: `L` ring elements of `R_p`, each packed to 10 bits
48    /// (`PK_VEC_ELEM_BYTES` bytes). This is the head of the wire serialization, held directly so
49    /// serialization is a copy rather than a re-encode of a structured vector.
50    vec_bytes: [[u8; PK_VEC_ELEM_BYTES]; L],
51    /// The public vector in NTT form, precomputed for the inner product in every encryption.
52    vec_ntt: NttMatrix<L, 1>,
53}
54
55impl<const L: usize> PkePublicKey<L> {
56    pub const SERIALIZED_LEN: usize = 32 + L * MODULUS_P_BITS * RING_DEG / 8;
57
58    /// Serializes this public key to a byte string. `out_buf` MUST have length SERIALIZED_LEN
59    // Explicit index loop (not `.iter().enumerate()`) to stay friendly to the aeneas extractor.
60    #[allow(clippy::needless_range_loop)]
61    pub(crate) fn serialize(&self, out_buf: &mut [u8]) {
62        assert_eq!(out_buf.len(), Self::SERIALIZED_LEN);
63
64        // The stored bytes are already the serialization: the L packed vector chunks, then the
65        // seed. Copy each chunk into place, then the seed as the 32-byte tail.
66        for i in 0..L {
67            let start = i * PK_VEC_ELEM_BYTES;
68            out_buf[start..start + PK_VEC_ELEM_BYTES].copy_from_slice(&self.vec_bytes[i]);
69        }
70        out_buf[L * PK_VEC_ELEM_BYTES..].copy_from_slice(&self.matrix_seed);
71    }
72
73    // `needless_range_loop`: explicit index loop kept for aeneas-extraction friendliness.
74    #[allow(clippy::unwrap_used, clippy::needless_range_loop)]
75    pub(crate) fn from_bytes(bytes: &[u8]) -> Self {
76        assert_eq!(bytes.len(), Self::SERIALIZED_LEN);
77
78        let (vec_slice, seed) = bytes.split_at(Self::SERIALIZED_LEN - 32);
79        let matrix_seed: [u8; 32] = seed.try_into().unwrap(); // checked above
80
81        // Deserialize the vector transiently, only to build its NTT form; the structured vector
82        // itself is not kept.
83        let vec = Matrix::deserialize_10(vec_slice);
84        let vec_ntt = NttMatrix::from_uniform_matrix(&vec);
85
86        // Store the vector's packed bytes verbatim (10-bit packing is canonical, so this is
87        // exactly what `serialize` would re-emit).
88        let mut vec_bytes = [[0u8; PK_VEC_ELEM_BYTES]; L];
89        for i in 0..L {
90            let start = i * PK_VEC_ELEM_BYTES;
91            vec_bytes[i].copy_from_slice(&vec_slice[start..start + PK_VEC_ELEM_BYTES]);
92        }
93
94        let mat_a = gen_matrix_from_seed::<L>(&matrix_seed);
95        let mat_a_ntt = NttMatrix::from_uniform_matrix(&mat_a);
96        Self {
97            matrix_seed,
98            mat_a_ntt,
99            vec_bytes,
100            vec_ntt,
101        }
102    }
103
104    /// Returns the public key hash
105    pub(crate) fn hash(&self) -> [u8; 32] {
106        // pkh = TurboSHAKE256(pk, 32, DOMSEP_PKHASH)
107        let mut buf = [0u8; max_pke_pubkey_serialized_len()];
108        let pk_slice = &mut buf[..PkePublicKey::<L>::SERIALIZED_LEN];
109        self.serialize(pk_slice);
110        turboshake256_hash::<DOMSEP_PKHASH>(pk_slice, &[])
111    }
112}
113
114/// The maximum length of a serialized public key, for all parameter choices
115pub(crate) const fn max_pke_pubkey_serialized_len() -> usize {
116    32 + MAX_L * MODULUS_P_BITS * RING_DEG / 8
117}
118
119/// The maximum length of a ciphertext (PKE or KEM, since they're the same), for all parameter
120/// choices, for a message that is 32-bytes.
121pub const fn max_ciphertext_len() -> usize {
122    // b' is in R^l_P and c is in R_T
123    MAX_T * RING_DEG / 8 + MAX_L * MODULUS_P_BITS * RING_DEG / 8
124}
125
126/// The length of a ciphertext (PKE or KEM, since they're the same) for a given parameter choice,
127/// for a message that is 32-bytes.
128pub const fn ciphertext_len<const L: usize, const T: usize>() -> usize {
129    // b' is in R^l_P and c is in R_T
130    L * MODULUS_P_BITS * RING_DEG / 8 + T * RING_DEG / 8
131}
132
133/// Expands a 32-byte secret key into the full decapsulation key components.
134///
135/// Returns (vec_s, z, pk, pkh) where:
136/// - vec_s is the secret vector (as PkeSecretKey)
137/// - z is 32 bytes used for rejection in decapsulation
138/// - pk is the public key
139/// - pkh is the hash of the public key
140// `needless_range_loop`: explicit index loop kept for aeneas-extraction friendliness.
141#[allow(clippy::needless_range_loop)]
142pub(crate) fn expand_decap_key<const L: usize, const MU: usize>(
143    sk: &[u8; 32],
144) -> (PkeSecretKey<L>, [u8; 32], PkePublicKey<L>, [u8; 32]) {
145    // mat_seed || secret_seed || r = TurboSHAKE256(sk || L, 96, DOMSEP_KGEXPAND)
146    let mut mat_seed = [0u8; 32];
147    let mut secret_seed = [0u8; 32];
148    let mut z = [0u8; 32];
149
150    let mut xof = {
151        let mut hasher = CTurboShake256::<DOMSEP_KGEXPAND>::default();
152        hasher.update(sk);
153        hasher.update(&[L as u8]);
154        hasher.finalize_xof()
155    };
156    xof.read(&mut mat_seed);
157    xof.read(&mut secret_seed);
158    xof.read(&mut z);
159
160    let mat_a = gen_matrix_from_seed::<L>(&mat_seed);
161    let vec_s = gen_secret_from_seed::<L, MU>(&secret_seed);
162    let mat_a_ntt = NttMatrix::from_uniform_matrix(&mat_a);
163    let vec_s_ntt = NttMatrix::from_secret_matrix(&vec_s);
164
165    // vec_b = RoundToR10(transpose(mat_A) * vec_s)
166    let b = {
167        let mut prod = mat_a_ntt.mul_transpose(&vec_s_ntt);
168        prod.wrapping_add_to_all(H1_VAL);
169        prod.shift_right(MODULUS_Q_BITS - MODULUS_P_BITS);
170        prod
171    };
172
173    // b's coefficients are 10-bit after the rounding shift, so it transforms as uniform
174    let vec_ntt = NttMatrix::from_uniform_matrix(&b);
175
176    // Pack b into its serialized bytes; we keep those, not the structured vector.
177    let mut vec_bytes = [[0u8; PK_VEC_ELEM_BYTES]; L];
178    for i in 0..L {
179        b.0[i][0].serialize(&mut vec_bytes[i], MODULUS_P_BITS);
180    }
181
182    let pk = PkePublicKey {
183        matrix_seed: mat_seed,
184        mat_a_ntt,
185        vec_bytes,
186        vec_ntt,
187    };
188    let pkh = pk.hash();
189
190    (PkeSecretKey(vec_s_ntt), z, pk, pkh)
191}
192
193/// Decrypts a ciphertext using the given secret key. `ciphertext` MUST have length
194/// `ciphertext_len::<L, T>()`.
195pub(crate) fn decrypt<const L: usize, const T: usize>(
196    sk: &PkeSecretKey<L>,
197    ciphertext: &[u8],
198) -> [u8; 32] {
199    assert_eq!(ciphertext.len(), ciphertext_len::<L, T>());
200    // b' is in R^l_P and c is in R_T
201    let (bprime_bytes, c_bytes) = ciphertext.split_at(L * MODULUS_P_BITS * RING_DEG / 8);
202
203    let bprime: Matrix<L, 1> = Matrix::deserialize_10(bprime_bytes);
204    let bprime_ntt = NttMatrix::from_uniform_matrix(&bprime);
205
206    let mut c = RingElem::deserialize(c_bytes, T);
207    c.shift_left(MODULUS_P_BITS - T);
208
209    let v = bprime_ntt.mul_transpose(&sk.0);
210    let v = v.0[0][0];
211
212    // Compute v - c + h₂
213    let mut mprime = &v - &c;
214    let h2_val = (1 << (MODULUS_P_BITS - 2)) - (1 << (MODULUS_P_BITS - T - 1))
215        + (1 << (MODULUS_Q_BITS - MODULUS_P_BITS - 1));
216    mprime.wrapping_add_to_all(h2_val);
217    mprime.shift_right(MODULUS_P_BITS - 1);
218
219    let mut m = [0u8; 32];
220    mprime.serialize(&mut m, 1);
221    m
222}
223
224/// Encrypts a message with a given public key and randomness (`coins`).
225/// `out_buf` MUST have length `ciphertext_len::<L, T>()`.
226pub(crate) fn encrypt_deterministic<const L: usize, const MU: usize, const T: usize>(
227    pk: &PkePublicKey<L>,
228    msg: &[u8; 32],
229    randomness: &[u8; 32],
230    out_buf: &mut [u8],
231) {
232    assert_eq!(out_buf.len(), ciphertext_len::<L, T>());
233
234    let vec_sprime = gen_secret_from_seed::<L, MU>(randomness);
235    let sprime_ntt = NttMatrix::from_secret_matrix(&vec_sprime);
236
237    let bprime = {
238        let mut prod = pk.mat_a_ntt.mul(&sprime_ntt);
239        prod.wrapping_add_to_all(H1_VAL);
240        prod.shift_right(MODULUS_Q_BITS - MODULUS_P_BITS);
241        prod
242    };
243
244    let vprime: Matrix<1, 1> = pk.vec_ntt.mul_transpose(&sprime_ntt);
245    let vprime = vprime.0[0][0];
246
247    let mut msg_polyn = RingElem(deserialize_generic(msg, 1));
248    msg_polyn.shift_left(MODULUS_P_BITS - 1);
249
250    // Compute v' - mp + h₁
251    let mut c = &vprime - &msg_polyn;
252    c.wrapping_add_to_all(H1_VAL);
253    c.shift_right(MODULUS_P_BITS - T);
254
255    // b' is in R^l_P and c is in R_T
256    let (bprime_buf, c_buf) = out_buf.split_at_mut(L * MODULUS_P_BITS * RING_DEG / 8);
257    bprime.serialize(bprime_buf, MODULUS_P_BITS);
258    c.serialize(c_buf, T);
259}
260
261#[cfg(test)]
262mod test {
263    use super::*;
264    use crate::consts::*;
265
266    use rand::RngCore;
267
268    // Tests that Decrypt(Encrypt(m)) == m
269    #[test]
270    fn encryption_correctness() {
271        test_enc_dec::<KOPIS512_L, KOPIS512_T, KOPIS512_MU>();
272        test_enc_dec::<KOPIS768_L, KOPIS768_T, KOPIS768_MU>();
273        test_enc_dec::<KOPIS1024_L, KOPIS1024_T, KOPIS1024_MU>();
274    }
275
276    // Helper function that encrypts and decrypts a random 32-byte message
277    fn test_enc_dec<const L: usize, const T: usize, const MU: usize>() {
278        let mut rng = rand::rng();
279        let mut backing_buf = [0u8; MAX_T * RING_DEG / 8 + MAX_L * MODULUS_P_BITS * RING_DEG / 8];
280
281        for _ in 0..100 {
282            // Generate a random secret key seed and expand it
283            let mut sk_seed = [0u8; 32];
284            rng.fill_bytes(&mut sk_seed);
285            let (sk, _, pk, _) = expand_decap_key::<L, MU>(&sk_seed);
286
287            let mut enc_seed = [0u8; 32];
288            let mut msg = [0u8; 32];
289            rng.fill_bytes(&mut enc_seed);
290            rng.fill_bytes(&mut msg);
291            let ct_buf = &mut backing_buf[..T * RING_DEG / 8 + L * MODULUS_P_BITS * RING_DEG / 8];
292
293            encrypt_deterministic::<L, MU, T>(&pk, &msg, &enc_seed, ct_buf);
294            let recovered_msg = decrypt::<L, T>(&sk, ct_buf);
295            assert_eq!(msg, recovered_msg);
296        }
297    }
298}