From e381b92de1488925bb6fb40d8c765172848a5fb5 Mon Sep 17 00:00:00 2001 From: Goutam Tamvada Date: Tue, 19 Sep 2023 11:12:26 -0400 Subject: [PATCH] Begin updating spec to FIPS 203 draft. (#78) * Update sampling functions and NTT. * Update PKE and compression functions. --- specs/hacspec-lib/src/field.rs | 11 + specs/kyber/src/compress.rs | 60 ++--- specs/kyber/src/ind_cpa.rs | 444 +++++++++++++++++---------------- specs/kyber/src/lib.rs | 1 + specs/kyber/src/matrix.rs | 46 ++++ specs/kyber/src/ntt.rs | 387 +++++++++++++++------------- specs/kyber/src/parameters.rs | 11 +- specs/kyber/src/sampling.rs | 219 ++++++++-------- 8 files changed, 664 insertions(+), 515 deletions(-) create mode 100644 specs/kyber/src/matrix.rs diff --git a/specs/hacspec-lib/src/field.rs b/specs/hacspec-lib/src/field.rs index c3f0ec499..8ab451b38 100644 --- a/specs/hacspec-lib/src/field.rs +++ b/specs/hacspec-lib/src/field.rs @@ -37,6 +37,17 @@ impl FieldElement for PrimeFieldElement { } } +impl PrimeFieldElement { + pub fn pow(&self, exponent: u8) -> Self { + let mut result = Self::new(1); + for _ in 0..exponent { + result = result * (*self); + } + + result + } +} + impl PrimeFieldElement { pub const MODULUS: u16 = MODULUS; } diff --git a/specs/kyber/src/compress.rs b/specs/kyber/src/compress.rs index a82782f67..84dcde1d4 100644 --- a/specs/kyber/src/compress.rs +++ b/specs/kyber/src/compress.rs @@ -1,57 +1,58 @@ use crate::parameters::{self, KyberFieldElement, KyberPolynomialRingElement}; -/// According to the Kyber Round 3 specification, compressing a polynomial +/// According to the NIST FIPS 203 standard, compressing a polynomial /// ring element is accomplished by `compress()`ing its constituent field /// coefficients. /// -/// The Kyber Round 3 specification can be found at: -/// +/// The NIST FIPS 203 standard can be found at +/// . pub fn compress( re: KyberPolynomialRingElement, bits_per_compressed_coefficient: usize, ) -> KyberPolynomialRingElement { KyberPolynomialRingElement::new( re.coefficients() - .map(|coefficient| compress_q(coefficient, bits_per_compressed_coefficient)), + .map(|coefficient| compress_d(coefficient, bits_per_compressed_coefficient)), ) } -/// According to the Kyber Round 3 specification, decompressing a polynomial +/// According to the NIST FIPS 203 standard, compressing a polynomial /// ring element is accomplished by `decompress()`ing its constituent field /// coefficients. /// -/// The Kyber Round 3 specification can be found at: -/// +/// The NIST FIPS 203 standard can be found at +/// . pub fn decompress( re: KyberPolynomialRingElement, bits_per_compressed_coefficient: usize, ) -> KyberPolynomialRingElement { KyberPolynomialRingElement::new( re.coefficients() - .map(|coefficient| decompress_q(coefficient, bits_per_compressed_coefficient)), + .map(|coefficient| decompress_d(coefficient, bits_per_compressed_coefficient)), ) } -/// This function implements the `Compress` function defined on Page 5 of the -/// Kyber Round 3 specification, which is defined as: +/// This function implements the `Compress` function defined on Page 18 of the +/// NIST FIPS 203 standard, which is defined as: /// /// ```plaintext -/// Compress_q(x, d) = round((2^{d} / q) * x) mod^{+}2^{d} +/// Compress_d: ℤq -> ℤ_{2ᵈ} +/// Compress_d(x) = ⌈(2ᵈ/q)·x⌋ /// ``` /// -/// Since `round(x) = floor(x + 0.5)` we have: +/// Since `⌈x⌋ = ⌊x + 1/2⌋` we have: /// /// ```plaintext -/// Compress_q(x,d) = floor(x*2^d/q + 1/2) mod^{+}2^d -/// = floor((2^{d+1} * x + q) / 2q) mod^{+}2^d +/// Compress_d(x) = ⌊(2ᵈ/q)·x + 1/2⌋ +/// = ⌊(2^{d+1}·x + q) / 2q⌋ /// ``` /// /// this latter expression is what the code computes, since it enables us to -/// avoid the use of floating point arithmetic. +/// avoid the use of floating point computations as required by the standard. /// -/// The Kyber Round 3 specification can be found at: -/// -fn compress_q(fe: KyberFieldElement, to_bit_size: usize) -> KyberFieldElement { +/// The NIST FIPS 203 standard can be found at +/// . +fn compress_d(fe: KyberFieldElement, to_bit_size: usize) -> KyberFieldElement { assert!(to_bit_size <= parameters::BITS_PER_COEFFICIENT); let two_pow_bit_size = 2u32.pow(to_bit_size.try_into().unwrap_or_else(|_| { @@ -68,26 +69,27 @@ fn compress_q(fe: KyberFieldElement, to_bit_size: usize) -> KyberFieldElement { (compressed % two_pow_bit_size).into() } -/// This function implements the `Decompress` function defined on Page 5 of -/// the Kyber Round 3 secification, which is defined as: +/// This function implements the `Decompress` function defined on Page 18 of the +/// NIST FIPS 203 standard, which is defined as: /// /// ```plaintext -/// Decompress_q(x, d) = round((q / 2^{d}) * x) +/// Decompress_d: ℤ_{2ᵈ} -> ℤq +/// Decompress_d(y) = ⌈(q/2ᵈ)·y⌋ /// ``` /// -/// Since `round(x) = floor(x + 0.5)` we have: +/// Since `⌈x⌋ = ⌊x + 1/2⌋` we have: /// /// ```plaintext -/// Decompress_q(x,d) = floor((x * q) / 2^d + 1/2) -/// = floor((2 * x * q + 2^d) / 2^{d+1}) +/// Decompress_d(y) = ⌊(q/2ᵈ)·y + 1/2⌋ +/// = ⌊(2·y·q + 2ᵈ) / 2^{d+1})⌋ /// ``` /// /// this latter expression is what the code computes, since it enables us to -/// avoid the use of floating point arithmetic. +/// avoid the use of floating point computations as required by the standard. /// -/// The Kyber Round 3 specification can be found at: -/// -fn decompress_q(fe: KyberFieldElement, to_bit_size: usize) -> KyberFieldElement { +/// The NIST FIPS 203 standard can be found at +/// . +fn decompress_d(fe: KyberFieldElement, to_bit_size: usize) -> KyberFieldElement { assert!(to_bit_size <= parameters::BITS_PER_COEFFICIENT); let decompressed = (2 * u32::from(fe.value) * u32::from(KyberFieldElement::MODULUS) @@ -122,7 +124,7 @@ pub(crate) mod tests { } // TODO: Check that for a randomly chosen x in Z_q, the expression: - // Decompress_q(Compress_q(x, d), d) - x mod q + // decompress_d(compress_d(x, d), d) - x mod q // is almost uniform over the integers of magnitude at most B_q, where // B_q = round(q / 2^{d + 1}) proptest! { diff --git a/specs/kyber/src/ind_cpa.rs b/specs/kyber/src/ind_cpa.rs index 667cf78a2..a831f172c 100644 --- a/specs/kyber/src/ind_cpa.rs +++ b/specs/kyber/src/ind_cpa.rs @@ -4,19 +4,17 @@ use hacspec_lib::{ use crate::{ compress::{compress, decompress}, - ntt::{ - kyber_polynomial_ring_element_mod::{invert_ntt, ntt_representation}, - *, - }, + matrix::{multiply_matrix_transpose_by_column, multiply_column_by_row, transpose}, + ntt::{ntt, ntt_inverse}, parameters::{ hash_functions::{G, H, PRF, XOF}, - KyberPolynomialRingElement, BITS_PER_RING_ELEMENT, COEFFICIENTS_IN_RING_ELEMENT, + KyberPolynomialRingElement, BYTES_PER_RING_ELEMENT, COEFFICIENTS_IN_RING_ELEMENT, CPA_PKE_CIPHERTEXT_SIZE, CPA_PKE_KEY_GENERATION_SEED_SIZE, CPA_PKE_MESSAGE_SIZE, CPA_PKE_PUBLIC_KEY_SIZE, CPA_PKE_SECRET_KEY_SIZE, CPA_SERIALIZED_KEY_LEN, RANK, REJECTION_SAMPLING_SEED_SIZE, T_AS_NTT_ENCODED_SIZE, VECTOR_U_COMPRESSION_FACTOR, - VECTOR_U_SIZE, VECTOR_V_COMPRESSION_FACTOR, + VECTOR_U_ENCODED_SIZE, VECTOR_V_COMPRESSION_FACTOR, }, - sampling::{sample_from_binomial_distribution, sample_from_uniform_distribution}, + sampling::{sample_ntt, sample_poly_cbd}, serialize::{deserialize_little_endian, serialize_little_endian}, BadRejectionSamplingRandomnessError, }; @@ -61,101 +59,130 @@ fn encode_12(input: [KyberPolynomialRingElement; RANK]) -> Vec { out } -/// This function implements Algorithm 4 of the Kyber Round 3 specification; -/// This is the Kyber Round 3 CPA-PKE key generation algorithm, and is -/// reproduced below: +/// This function implements most of Algorithm 12 of the +/// NIST FIPS 203 specification; this is the Kyber CPA-PKE key generation algorithm. +/// +/// We say "most of" since Algorithm 12 samples the required randomness within +/// the function itself, whereas this implementation expects it to be provided +/// through the `key_generation_seed` parameter. +/// +/// Algorithm 12 is reproduced below: /// /// ```plaintext -/// Output: Secret key sk ∈ B^{12·k·n/8} -/// Output: Public key pk ∈ B^{12·k·n/8+32} -/// d←B^{32} -/// (ρ,σ) := G(d) -/// N := 0 -/// for i from 0 to k−1 do -/// for j from 0 to k − 1 do -/// Aˆ [i][j] := Parse(XOF(ρ, j, i)) +/// Output: encryption key ekₚₖₑ ∈ 𝔹^{384k+32}. +/// Output: decryption key dkₚₖₑ ∈ 𝔹^{384k}. +/// +/// d $← B +/// (ρ,σ) ← G(d) +/// N ← 0 +/// for (i ← 0; i < k; i++) +/// for(j ← 0; j < k; j++) +/// Â[i,j] ← SampleNTT(XOF(ρ, i, j)) /// end for /// end for -/// for i from 0 to k−1 do -/// s[i] := CBD_{η1}(PRF(σ, N)) -/// N := N + 1 +/// for(i ← 0; i < k; i++) +/// s[i] ← SamplePolyCBD_{η₁}(PRF_{η₁}(σ,N)) +/// N ← N + 1 /// end for -/// for i from 0 to k−1 do -/// e[i] := CBD_{η1}(PRF(σ, N)) -/// N := N + 1 +/// for(i ← 0; i < k; i++) +/// e[i] ← SamplePolyCBD_{η₂}(PRF_{η₂}(σ,N)) +/// N ← N + 1 /// end for -/// sˆ := NTT(s) -/// eˆ := NTT(e) -/// tˆ := Aˆ ◦ sˆ + eˆ -/// pk := Encode_12(tˆ mod^{+}q) || ρ -/// sk := Encode_12(sˆ mod^{+}q) -/// return (pk,sk) +/// ŝ ← NTT(s) +/// ê ← NTT(e) +/// t̂ ← Â◦ŝ + ê +/// ekₚₖₑ ← ByteEncode₁₂(t̂) ‖ ρ +/// dkₚₖₑ ← ByteEncode₁₂(ŝ) /// ``` /// -/// The Kyber Round 3 specification can be found at: -/// +/// The NIST FIPS 203 standard can be found at +/// . #[allow(non_snake_case)] pub(crate) fn generate_keypair( key_generation_seed: &[u8; CPA_PKE_KEY_GENERATION_SEED_SIZE], ) -> Result { - let mut prf_input: [u8; 33] = [0; 33]; - - let mut secret_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; - let mut error_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; + // (ρ,σ) ← G(d) + let hashed = G(key_generation_seed); + let (seed_for_A, seed_for_secret_and_error) = hashed.split_at(32); // N := 0 let mut domain_separator: u8 = 0; - // (ρ,σ) := G(d) - let hashed = G(key_generation_seed); - let (seed_for_A, seed_for_secret_and_error) = hashed.split_at(32); + // for (i ← 0; i < k; i++) + // for(j ← 0; j < k; j++) + // Â[i,j] ← SampleNTT(XOF(ρ, i, j)) + // end for + // end for + let mut A_as_ntt = [[KyberPolynomialRingElement::ZERO; RANK]; RANK]; + + let mut xof_input: [u8; 34] = seed_for_A.into_padded_array(); - let A_transpose = parse_a(seed_for_A.into_padded_array(), true)?; + for i in 0..RANK { + for j in 0..RANK { + xof_input[32] = i.as_u8(); + xof_input[33] = j.as_u8(); + let xof_bytes: [u8; REJECTION_SAMPLING_SEED_SIZE] = XOF(&xof_input); - // for i from 0 to k−1 do - // s[i] := CBD_{η1}(PRF(σ, N)) - // N := N + 1 + A_as_ntt[i][j] = sample_ntt(xof_bytes)?; + } + } + + // for(i ← 0; i < k; i++) + // s[i] ← SamplePolyCBD_{η₁}(PRF_{η₁}(σ,N)) + // N ← N + 1 // end for - // sˆ := NTT(s) - prf_input[0..seed_for_secret_and_error.len()].copy_from_slice(seed_for_secret_and_error); + let mut secret = [KyberPolynomialRingElement::ZERO; RANK]; - for i in 0..secret_as_ntt.len() { + let mut prf_input: [u8; 33] = seed_for_secret_and_error.into_padded_array(); + + for i in 0..secret.len() { prf_input[32] = domain_separator; domain_separator += 1; - // 2 sampling coins * 64 + // η₁ * 64 = 2 * 64 sampling coins let prf_output: [u8; 128] = PRF(&prf_input); - let secret = sample_from_binomial_distribution(2, &prf_output[..]); - secret_as_ntt[i] = ntt_representation(secret); + secret[i] = sample_poly_cbd(2, &prf_output[..]); } - // for i from 0 to k−1 do - // e[i] := CBD_{η1}(PRF(σ, N)) - // N := N + 1 + // for(i ← 0; i < k; i++) + // e[i] ← SamplePolyCBD_{η₂}(PRF_{η₂}(σ,N)) + // N ← N + 1 // end for - // eˆ := NTT(e) - for i in 0..error_as_ntt.len() { + let mut error = [KyberPolynomialRingElement::ZERO; RANK]; + + for i in 0..error.len() { prf_input[32] = domain_separator; domain_separator += 1; - // 2 sampling coins * 64 + // η₂ * 64 = 2 * 64 sampling coins let prf_output: [u8; 128] = PRF(&prf_input); - let error = sample_from_binomial_distribution(2, &prf_output[..]); - error_as_ntt[i] = ntt_representation(error); + error[i] = sample_poly_cbd(2, &prf_output[..]); } - // tˆ := Aˆ ◦ sˆ + eˆ - let mut t_as_ntt = multiply_matrix_by_column(&A_transpose, &secret_as_ntt); + // ŝ ← NTT(s) + let mut secret_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; + for i in 0..secret_as_ntt.len() { + secret_as_ntt[i] = ntt(secret[i]); + } + + // ê ← NTT(e) + let mut error_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; + for i in 0..error_as_ntt.len() { + error_as_ntt[i] = ntt(error[i]); + } + + // t̂ ← Â◦ŝ + ê + let mut t_as_ntt = multiply_matrix_transpose_by_column(&A_as_ntt, &secret_as_ntt); for i in 0..t_as_ntt.len() { t_as_ntt[i] = t_as_ntt[i] + error_as_ntt[i]; } - // pk := (Encode_12(tˆ mod^{+}q) || ρ) + // ekₚₖₑ ← ByteEncode₁₂(t̂) ‖ ρ let public_key_serialized = encode_12(t_as_ntt).concat(seed_for_A); - // sk := Encode_12(sˆ mod^{+}q) + // dkₚₖₑ ← ByteEncode₁₂(ŝ) let secret_key_serialized = encode_12(secret_as_ntt); Ok(KeyPair::new( @@ -164,55 +191,6 @@ pub(crate) fn generate_keypair( )) } -/// ```text -/// for i from 0 to k−1 do -/// for j from 0 to k − 1 do -/// Aˆ [i][j] := Parse(XOF(ρ, j, i)) -/// end for -/// end for -/// ``` -#[inline(always)] -fn parse_a( - mut seed: [u8; 34], - transpose: bool, -) -> Result<[[KyberPolynomialRingElement; RANK]; RANK], BadRejectionSamplingRandomnessError> { - let mut a_transpose = [[KyberPolynomialRingElement::ZERO; RANK]; RANK]; - - for i in 0..RANK { - for j in 0..RANK { - seed[32] = i.as_u8(); - seed[33] = j.as_u8(); - - let xof_bytes: [u8; REJECTION_SAMPLING_SEED_SIZE] = XOF(&seed); - - // A[i][j] = A_transpose[j][i] - if transpose { - a_transpose[j][i] = sample_from_uniform_distribution(xof_bytes)?; - } else { - a_transpose[i][j] = sample_from_uniform_distribution(xof_bytes)?; - } - } - } - Ok(a_transpose) -} - -#[inline(always)] -fn cbd(mut prf_input: [u8; 33]) -> ([KyberPolynomialRingElement; RANK], u8) { - let mut domain_separator = 0; - let mut r_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; - for i in 0..r_as_ntt.len() { - prf_input[32] = domain_separator; - domain_separator += 1; - - // 2 sampling coins * 64 - let prf_output: [u8; 128] = PRF(&prf_input); - - let r = sample_from_binomial_distribution(2, &prf_output); - r_as_ntt[i] = ntt_representation(r); - } - (r_as_ntt, domain_separator) -} - fn encode_and_compress_u(input: [KyberPolynomialRingElement; RANK]) -> Vec { let mut out = Vec::new(); for re in input.into_iter() { @@ -225,178 +203,224 @@ fn encode_and_compress_u(input: [KyberPolynomialRingElement; RANK]) -> Vec { out } -/// This function implements Algorithm 5 of the Kyber Round 3 specification; -/// This is the Kyber Round 3 CPA-PKE encryption algorithm, and is reproduced -/// below: +/// This function implements Algorithm 13 of the +/// NIST FIPS 203 specification; this is the Kyber CPA-PKE encryption algorithm. +/// +/// Algorithm 13 is reproduced below: /// /// ```plaintext -/// Input: Public key pk ∈ B^{12·k·n / 8 + 32} -/// Input: Message m ∈ B^{32} -/// Input: Random coins r ∈ B32 -/// Output: Ciphertext c ∈ B^{d_u·k·n/8 + d_v·n/8} -/// N := 0 -/// tˆ := Decode_12(pk) -/// ρ := pk + 12·k·n / 8 -/// for i from 0 to k−1 do -/// for j from 0 to k − 1 do -/// AˆT[i][j] := Parse(XOF(ρ, i, j)) +/// Input: encryption key ekₚₖₑ ∈ 𝔹^{384k+32}. +/// Input: message m ∈ 𝔹^{32}. +/// Input: encryption randomness r ∈ 𝔹^{32}. +/// Output: ciphertext c ∈ 𝔹^{32(dᵤk + dᵥ)}. +/// +/// N ← 0 +/// t̂ ← ByteDecode₁₂(ekₚₖₑ[0:384k]) +/// ρ ← ekₚₖₑ[384k: 384k + 32] +/// for (i ← 0; i < k; i++) +/// for(j ← 0; j < k; j++) +/// Â[i,j] ← SampleNTT(XOF(ρ, i, j)) /// end for /// end for -/// for i from 0 to k−1 do -/// r[i] := CBD{η1}(PRF(r, N)) -/// N := N + 1 +/// for(i ← 0; i < k; i++) +/// r[i] ← SamplePolyCBD_{η₁}(PRF_{η₁}(r,N)) +/// N ← N + 1 /// end for -/// for i from 0 to k−1 do -/// e_1[i] := CBD_{η2}(PRF(r,N)) -/// N := N + 1 +/// for(i ← 0; i < k; i++) +/// e₁[i] ← SamplePolyCBD_{η₂}(PRF_{η₂}(r,N)) +/// N ← N + 1 /// end for -/// e_2 := CBD{η2}(PRF(r, N)) -/// rˆ := NTT(r) -/// u := NTT^{-1}(AˆT ◦ rˆ) + e_1 -/// v := NTT^{−1}(tˆT ◦ rˆ) + e_2 + Decompress_q(Decode_1(m),1) -/// c_1 := Encode_{du}(Compress_q(u,d_u)) -/// c_2 := Encode_{dv}(Compress_q(v,d_v)) -/// return c = c1 || c2 +/// e₂ ← SamplePolyCBD_{η₂}(PRF_{η₂}(r,N)) +/// r̂ ← NTT(r) +/// u ← NTT-¹(Âᵀ ◦ r̂) + e₁ +/// μ ← Decompress₁(ByteDecode₁(m))) +/// v ← NTT-¹(t̂ᵀ ◦ rˆ) + e₂ + μ +/// c₁ ← ByteEncode_{dᵤ}(Compress_{dᵤ}(u)) +/// c₂ ← ByteEncode_{dᵥ}(Compress_{dᵥ}(v)) +/// return c ← (c₁ ‖ c₂) /// ``` /// -/// The Kyber Round 3 specification can be found at: -/// +/// The NIST FIPS 203 standard can be found at +/// . #[allow(non_snake_case)] pub(crate) fn encrypt( public_key: &[u8; CPA_PKE_PUBLIC_KEY_SIZE], message: [u8; CPA_PKE_MESSAGE_SIZE], randomness: &[u8; 32], ) -> Result { - // tˆ := Decode_12(pk) - let mut t_as_ntt_ring_element_bytes = public_key.chunks(BITS_PER_RING_ELEMENT / 8); + // N ← 0 + let mut domain_separator: u8 = 0; + + // t̂ ← ByteDecode₁₂(ekₚₖₑ[0:384k]) let mut t_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; - for i in 0..t_as_ntt.len() { - t_as_ntt[i] = deserialize_little_endian( - 12, - t_as_ntt_ring_element_bytes.next().expect( - "t_as_ntt_ring_element_bytes should have enough bytes to deserialize to t_as_ntt", - ), - ); + for (i, t_as_ntt_bytes) in public_key[..T_AS_NTT_ENCODED_SIZE] + .chunks(BYTES_PER_RING_ELEMENT) + .enumerate() + { + t_as_ntt[i] = deserialize_little_endian(12, t_as_ntt_bytes); } - // ρ := pk + 12·k·n / 8 - // for i from 0 to k−1 do - // for j from 0 to k − 1 do - // AˆT[i][j] := Parse(XOF(ρ, i, j)) + // ρ ← ekₚₖₑ[384k: 384k + 32] + let seed_for_A = &public_key[T_AS_NTT_ENCODED_SIZE..]; + + // for (i ← 0; i < k; i++) + // for(j ← 0; j < k; j++) + // Â[i,j] ← SampleNTT(XOF(ρ, i, j)) // end for // end for - let seed = &public_key[T_AS_NTT_ENCODED_SIZE..]; - let A_transpose = parse_a(seed.into_padded_array(), false)?; + let mut A_as_ntt = [[KyberPolynomialRingElement::ZERO; RANK]; RANK]; + + let mut xof_input: [u8; 34] = seed_for_A.into_padded_array(); + + for i in 0..RANK { + for j in 0..RANK { + xof_input[32] = i.as_u8(); + xof_input[33] = j.as_u8(); + let xof_bytes: [u8; REJECTION_SAMPLING_SEED_SIZE] = XOF(&xof_input); - // for i from 0 to k−1 do - // r[i] := CBD{η1}(PRF(r, N)) - // N := N + 1 + A_as_ntt[i][j] = sample_ntt(xof_bytes)?; + } + } + + // for(i ← 0; i < k; i++) + // r[i] ← SamplePolyCBD_{η₁}(PRF_{η₁}(r,N)) + // N ← N + 1 // end for - // rˆ := NTT(r) + let mut r = [KyberPolynomialRingElement::ZERO; RANK]; + let mut prf_input: [u8; 33] = randomness.into_padded_array(); - let (r_as_ntt, mut domain_separator) = cbd(prf_input); - // for i from 0 to k−1 do - // e1[i] := CBD_{η2}(PRF(r,N)) - // N := N + 1 + for i in 0..r.len() { + prf_input[32] = domain_separator; + domain_separator += 1; + + // η₁ * 64 = 2 * 64 sampling coins + let prf_output: [u8; 128] = PRF(&prf_input); + + r[i] = sample_poly_cbd(2, &prf_output); + } + + // for(i ← 0; i < k; i++) + // e₁[i] ← SamplePolyCBD_{η₂}(PRF_{η₂}(r,N)) + // N ← N + 1 // end for let mut error_1 = [KyberPolynomialRingElement::ZERO; RANK]; for i in 0..error_1.len() { prf_input[32] = domain_separator; domain_separator += 1; - // 2 sampling coins * 64 + // η₂ * 64 = 2 * 64 sampling coins let prf_output: [u8; 128] = PRF(&prf_input); - error_1[i] = sample_from_binomial_distribution(2, &prf_output); + error_1[i] = sample_poly_cbd(2, &prf_output); } - // e_2 := CBD{η2}(PRF(r, N)) + // e_2 := CBD{η₂}(PRF(r, N)) prf_input[32] = domain_separator; - // 2 sampling coins * 64 + // η₂ * 64 = 2 * 64 sampling coins let prf_output: [u8; 128] = PRF(&prf_input); - let error_2 = sample_from_binomial_distribution(2, &prf_output); + let error_2 = sample_poly_cbd(2, &prf_output); - // u := NTT^{-1}(AˆT ◦ rˆ) + e_1 - let mut u = multiply_matrix_by_column(&A_transpose, &r_as_ntt).map(|r| invert_ntt(r)); + // r̂ ← NTT(r) + let mut r_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; + for i in 0..r.len() { + r_as_ntt[i] = ntt(r[i]); + } + + // u ← NTT-¹(Âᵀ ◦ r̂) + e₁ + let A_as_ntt_transpose = transpose(&A_as_ntt); + let mut u = multiply_matrix_transpose_by_column(&A_as_ntt_transpose, &r_as_ntt) + .map(|re| ntt_inverse(re)); for i in 0..u.len() { u[i] = u[i] + error_1[i]; } - // v := NTT^{−1}(tˆT ◦ rˆ) + e_2 + Decompress_q(Decode_1(m),1) - let message_as_ring_element = deserialize_little_endian(1, &message); - let v = invert_ntt(multiply_row_by_column(&t_as_ntt, &r_as_ntt)) + // μ ← Decompress₁(ByteDecode₁(m))) + let message_as_ring_element = decompress(deserialize_little_endian(1, &message), 1); + + // v ← NTT-¹(t̂ᵀ ◦ r̂) + e₂ + μ + let v = ntt_inverse(multiply_column_by_row(&t_as_ntt, &r_as_ntt)) + error_2 - + decompress(message_as_ring_element, 1); + + message_as_ring_element; - // c_1 := Encode_{du}(Compress_q(u,d_u)) + // c₁ ← ByteEncode_{dᵤ}(Compress_{dᵤ}(u)) let c1 = encode_and_compress_u(u); - // c_2 := Encode_{dv}(Compress_q(v,d_v)) + // c₂ ← ByteEncode_{dᵥ}(Compress_{dᵥ}(v)) let c2 = serialize_little_endian( compress(v, VECTOR_V_COMPRESSION_FACTOR), VECTOR_V_COMPRESSION_FACTOR, ); - let ciphertext = c1 - .into_iter() - .chain(c2.into_iter()) - .collect::>() - .as_array(); + // return c ← (c₁ ‖ c₂) + let mut ciphertext: CiphertextCpa = (&c1).into_padded_array(); + ciphertext[VECTOR_U_ENCODED_SIZE..].copy_from_slice(c2.as_slice()); Ok(ciphertext) } -/// This function implements Algorithm 6 of the Kyber Round 3 specification; -/// This is the Kyber Round 3 CPA-PKE decryption algorithm, and is reproduced -/// below: +/// This function implements Algorithm 14 of the +/// NIST FIPS 203 specification; this is the Kyber CPA-PKE decryption algorithm. +/// +/// Algorithm 14 is reproduced below: /// /// ```plaintext -/// Input: Secret key sk ∈ B^{12·k·n} / 8 -/// Input: Ciphertext c ∈ B^{d_u·k·n / 8} + d_v·n / 8 -/// Output: Message m ∈ B^{32} -/// u := Decompress_q(Decode_{d_u}(c), d_u) -/// v := Decompress_q(Decode_{d_v}(c + d_u·k·n / 8), d_v) -/// sˆ := Decode_12(sk) -/// m := Encode_1(Compress_q(v − NTT^{−1}(sˆT ◦ NTT(u)) , 1)) +/// Input: decryption key dkₚₖₑ ∈ 𝔹^{384k}. +/// Input: ciphertext c ∈ 𝔹^{32(dᵤk + dᵥ)}. +/// Output: message m ∈ 𝔹^{32}. +/// +/// c₁ ← c[0 : 32dᵤk] +/// c₂ ← c[32dᵤk : 32(dᵤk + dᵥ)] +/// u ← Decompress_{dᵤ}(ByteDecode_{dᵤ}(c₁)) +/// v ← Decompress_{dᵥ}(ByteDecode_{dᵥ}(c₂)) +/// ŝ ← ByteDecode₁₂(dkₚₖₑ) +/// w ← v - NTT-¹(ŝᵀ ◦ NTT(u)) +/// m ← ByteEncode₁(Compress₁(w)) /// return m /// ``` /// -/// The Kyber Round 3 specification can be found at: -/// +/// The NIST FIPS 203 standard can be found at +/// . #[allow(non_snake_case)] pub(crate) fn decrypt( secret_key: &[u8; CPA_PKE_SECRET_KEY_SIZE], ciphertext: &[u8; CPA_PKE_CIPHERTEXT_SIZE], ) -> [u8; CPA_PKE_MESSAGE_SIZE] { - let mut u_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; - let mut secret_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; - - // u := Decompress_q(Decode_{d_u}(c), d_u) - for (i, u_bytes) in - (0..u_as_ntt.len()).zip(ciphertext.chunks((COEFFICIENTS_IN_RING_ELEMENT * 10) / 8)) + // u ← Decompress_{dᵤ}(ByteDecode_{dᵤ}(c₁)) + let mut u = [KyberPolynomialRingElement::ZERO; RANK]; + for (i, u_bytes) in ciphertext[..VECTOR_U_ENCODED_SIZE] + .chunks((COEFFICIENTS_IN_RING_ELEMENT * VECTOR_U_COMPRESSION_FACTOR) / 8) + .enumerate() { - let u = deserialize_little_endian(10, u_bytes); - u_as_ntt[i] = ntt_representation(decompress(u, 10)); + u[i] = decompress( + deserialize_little_endian(VECTOR_U_COMPRESSION_FACTOR, u_bytes), + VECTOR_U_COMPRESSION_FACTOR, + ); } - // v := Decompress_q(Decode_{d_v}(c + d_u·k·n / 8), d_v) + // v ← Decompress_{dᵥ}(ByteDecode_{dᵥ}(c₂)) let v = decompress( - deserialize_little_endian(VECTOR_V_COMPRESSION_FACTOR, &ciphertext[VECTOR_U_SIZE..]), + deserialize_little_endian( + VECTOR_V_COMPRESSION_FACTOR, + &ciphertext[VECTOR_U_ENCODED_SIZE..], + ), VECTOR_V_COMPRESSION_FACTOR, ); - // sˆ := Decode_12(sk) - let mut secret_as_ntt_ring_element_bytes = secret_key.chunks(BITS_PER_RING_ELEMENT / 8); - for i in 0..secret_as_ntt.len() { - secret_as_ntt[i] = deserialize_little_endian( - 12, - secret_as_ntt_ring_element_bytes.next().expect("secret_as_ntt_ring_element_bytes should have enough bytes to deserialize to secret_as_ntt"), - ); + // ŝ ← ByteDecode₁₂(dkₚₖₑ) + let mut secret_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; + for (i, secret_bytes) in secret_key.chunks_exact(BYTES_PER_RING_ELEMENT).enumerate() { + secret_as_ntt[i] = deserialize_little_endian(12, secret_bytes); } - // m := Encode_1(Compress_q(v − NTT^{−1}(sˆT ◦ NTT(u)) , 1)) - let message = v - invert_ntt(multiply_row_by_column(&secret_as_ntt, &u_as_ntt)); + // w ← v - NTT-¹(ŝᵀ ◦ NTT(u)) + let mut u_as_ntt = [KyberPolynomialRingElement::ZERO; RANK]; + for i in 0..u_as_ntt.len() { + u_as_ntt[i] = ntt(u[i]); + } + let message = v - ntt_inverse(multiply_column_by_row(&secret_as_ntt, &u_as_ntt)); + // m ← ByteEncode₁(Compress₁(w)) + // return m // FIXME: remove conversion serialize_little_endian(compress(message, 1), 1).as_array() } diff --git a/specs/kyber/src/lib.rs b/specs/kyber/src/lib.rs index 5e3a7893f..6b2d69d45 100644 --- a/specs/kyber/src/lib.rs +++ b/specs/kyber/src/lib.rs @@ -7,6 +7,7 @@ use parameters::{ mod compress; mod ind_cpa; +mod matrix; mod ntt; mod parameters; mod sampling; diff --git a/specs/kyber/src/matrix.rs b/specs/kyber/src/matrix.rs new file mode 100644 index 000000000..86ebf8ad4 --- /dev/null +++ b/specs/kyber/src/matrix.rs @@ -0,0 +1,46 @@ +use crate::{ + ntt::multiply_ntts, + parameters::{KyberPolynomialRingElement, RANK}, +}; + +pub(crate) fn transpose( + matrix: &[[KyberPolynomialRingElement; RANK]; RANK], +) -> [[KyberPolynomialRingElement; RANK]; RANK] { + let mut transpose = [[KyberPolynomialRingElement::ZERO; RANK]; RANK]; + for (i, row) in matrix.iter().enumerate() { + for (j, matrix_element) in row.iter().enumerate() { + transpose[j][i] = *matrix_element; + } + } + + transpose +} + +pub(crate) fn multiply_matrix_transpose_by_column( + matrix: &[[KyberPolynomialRingElement; RANK]; RANK], + vector: &[KyberPolynomialRingElement; RANK], +) -> [KyberPolynomialRingElement; RANK] { + let mut result = [KyberPolynomialRingElement::ZERO; RANK]; + + let transposed = transpose(&matrix); + for (i, row) in transposed.iter().enumerate() { + for (j, matrix_element) in row.iter().enumerate() { + let product = multiply_ntts(matrix_element, &vector[j]); + result[i] = result[i] + product; + } + } + result +} + +pub(crate) fn multiply_column_by_row( + column_vector: &[KyberPolynomialRingElement; RANK], + row_vector: &[KyberPolynomialRingElement; RANK], +) -> KyberPolynomialRingElement { + let mut result = KyberPolynomialRingElement::ZERO; + + for (column_element, row_element) in column_vector.iter().zip(row_vector.iter()) { + result = result + multiply_ntts(column_element, row_element); + } + + result +} diff --git a/specs/kyber/src/ntt.rs b/specs/kyber/src/ntt.rs index 5408eb888..d420f67d7 100644 --- a/specs/kyber/src/ntt.rs +++ b/specs/kyber/src/ntt.rs @@ -1,198 +1,249 @@ -use crate::parameters::{KyberPolynomialRingElement, RANK}; - -use self::kyber_polynomial_ring_element_mod::ntt_multiply; - -pub(crate) mod kyber_polynomial_ring_element_mod { - use hacspec_lib::field::FieldElement; - - use crate::parameters::{ - self, KyberFieldElement, KyberPolynomialRingElement, COEFFICIENTS_IN_RING_ELEMENT, - }; - - /// [ pow(17, br(i), p) for 0 <= i < 128 ] - /// br(i) is the bit reversal of i regarded as a 7-bit number. - const ZETAS: [u16; 128] = [ - 1, 1729, 2580, 3289, 2642, 630, 1897, 848, 1062, 1919, 193, 797, 2786, 3260, 569, 1746, - 296, 2447, 1339, 1476, 3046, 56, 2240, 1333, 1426, 2094, 535, 2882, 2393, 2879, 1974, 821, - 289, 331, 3253, 1756, 1197, 2304, 2277, 2055, 650, 1977, 2513, 632, 2865, 33, 1320, 1915, - 2319, 1435, 807, 452, 1438, 2868, 1534, 2402, 2647, 2617, 1481, 648, 2474, 3110, 1227, 910, - 17, 2761, 583, 2649, 1637, 723, 2288, 1100, 1409, 2662, 3281, 233, 756, 2156, 3015, 3050, - 1703, 1651, 2789, 1789, 1847, 952, 1461, 2687, 939, 2308, 2437, 2388, 733, 2337, 268, 641, - 1584, 2298, 2037, 3220, 375, 2549, 2090, 1645, 1063, 319, 2773, 757, 2099, 561, 2466, 2594, - 2804, 1092, 403, 1026, 1143, 2150, 2775, 886, 1722, 1212, 1874, 1029, 2110, 2935, 885, - 2154, - ]; - - /// [ pow(17, 2 * br(i) + 1, p) for 0 <= i < 128 ] - /// br(i) is the bit reversal of i regarded as a 7-bit number. - const MOD_ROOTS: [u16; 128] = [ - 17, 3312, 2761, 568, 583, 2746, 2649, 680, 1637, 1692, 723, 2606, 2288, 1041, 1100, 2229, - 1409, 1920, 2662, 667, 3281, 48, 233, 3096, 756, 2573, 2156, 1173, 3015, 314, 3050, 279, - 1703, 1626, 1651, 1678, 2789, 540, 1789, 1540, 1847, 1482, 952, 2377, 1461, 1868, 2687, - 642, 939, 2390, 2308, 1021, 2437, 892, 2388, 941, 733, 2596, 2337, 992, 268, 3061, 641, - 2688, 1584, 1745, 2298, 1031, 2037, 1292, 3220, 109, 375, 2954, 2549, 780, 2090, 1239, - 1645, 1684, 1063, 2266, 319, 3010, 2773, 556, 757, 2572, 2099, 1230, 561, 2768, 2466, 863, - 2594, 735, 2804, 525, 1092, 2237, 403, 2926, 1026, 2303, 1143, 2186, 2150, 1179, 2775, 554, - 886, 2443, 1722, 1607, 1212, 2117, 1874, 1455, 1029, 2300, 2110, 1219, 2935, 394, 885, - 2444, 2154, 1175, - ]; - - const NTT_LAYERS: [usize; 7] = [2, 4, 8, 16, 32, 64, 128]; - - /// Use the Cooley–Tukey butterfly to compute an in-place NTT representation - /// of a `KyberPolynomialRingElement`. - /// - /// This can be seen (see [CFRG draft]) as 128 applications of the linear map CT where - /// - /// CT_i(a, b) => (a + zeta^i * b, a - zeta^i * b) mod q - /// - /// for the appropriate i. - /// - /// Because the Kyber base field has 256th roots of unity but not 512th roots - /// of unity, the resulting NTT representation is an element in: - /// - /// ```plaintext - /// Product(i = 0 to 255) F_{3329}[x] / (x^2 - zeta^{2i+1}), - /// ``` - /// - /// This is isomorphic to `F_{3329}[x] / (x^{256} + 1)` by the - /// Chinese Remainder Theorem. - /// - /// [CFRG draft]: - pub fn ntt_representation(mut re: KyberPolynomialRingElement) -> KyberPolynomialRingElement { - let mut zeta_i = 0; - for layer in NTT_LAYERS.iter().rev() { - for offset in (0..(COEFFICIENTS_IN_RING_ELEMENT - layer)).step_by(2 * layer) { - zeta_i += 1; - let zeta: KyberFieldElement = ZETAS[zeta_i].into(); - - for j in offset..offset + layer { - let t = zeta * re[j + layer]; - re[j + layer] = re[j] - t; - re[j] = re[j] + t; - } - } - } - re +use crate::parameters::{ + KyberFieldElement, KyberPolynomialRingElement, COEFFICIENTS_IN_RING_ELEMENT, +}; + +use hacspec_lib::{field::FieldElement, PanickingIntegerCasts}; + +const ZETA: KyberFieldElement = KyberFieldElement { value: 17 }; + +const INVERSE_OF_128: KyberFieldElement = KyberFieldElement { value: 3303 }; + +const NTT_LAYERS: [usize; 7] = [2, 4, 8, 16, 32, 64, 128]; + +/// Given a `value`, take its least significant 7 bits and return the number +/// obtained by reversing these bits. +/// +/// Corresponds to the `BitRev₇` function that is referenced in the NIST FIPS +/// 203 standard. +/// +/// The NIST FIPS 203 standard can be found at +/// . +fn bit_rev_7(value: u8) -> u8 { + let mut reversed: u8 = 0; + for bit in 0..u8::BITS - 1 { + reversed <<= 1; + reversed |= (value & (1 << bit)) >> bit; } - /// Use the Gentleman-Sande butterfly to invert, in-place, the NTT representation - /// of a `KyberPolynomialRingElement`. The inverse NTT can be computed (see [CFRG draft]) by - /// replacing CS_i by GS_j and - /// - /// ```plaintext - /// GS_j(a, b) => ( (a + b) / 2, zeta^{2*j + 1} * (a - b) / 2 ) mod q - /// ``` - /// - /// for the appropriate j. - /// - /// [CFRG draft]: https://datatracker.ietf.org/doc/draft-cfrg-schwabe-kyber/ - pub fn invert_ntt(re: KyberPolynomialRingElement) -> KyberPolynomialRingElement { - let inverse_of_2: KyberFieldElement = - KyberFieldElement::new((parameters::FIELD_MODULUS + 1) / 2); - - let mut out = KyberPolynomialRingElement::ZERO; - for i in 0..re.len() { - out[i].value = re[i].value; - } - - let mut zeta_i = COEFFICIENTS_IN_RING_ELEMENT / 2; - - for layer in NTT_LAYERS { - for offset in (0..(COEFFICIENTS_IN_RING_ELEMENT - layer)).step_by(2 * layer) { - zeta_i -= 1; - let zeta: KyberFieldElement = ZETAS[zeta_i].into(); + reversed +} - for j in offset..offset + layer { - let a_minus_b = out[j + layer] - out[j]; - out[j] = inverse_of_2 * (out[j] + out[j + layer]); - out[j + layer] = inverse_of_2 * zeta * a_minus_b; - } +/// Use the Cooley–Tukey butterfly to compute an in-place NTT representation +/// of a `KyberPolynomialRingElement`. +/// +/// Given a `KyberPolynomialRingElement` `f`, the NTT representation `f^` is: +/// +/// ```plaintext +/// f^ := (f mod(X² - ζ^(2*BitRev₇(0) + 1), ..., f mod (X² − ζ^(2·BitRev₇(127) + 1)) +/// ``` +/// +/// This function implements Algorithm 8 of the NIST FIPS 203 standard, which +/// is reproduced below: +/// +/// ```plaintext +/// Input: array f ∈ ℤ₂₅₆. +/// Output: array fˆ ∈ ℤ₂₅₆. +/// +/// fˆ ← f +/// k ← 1 +/// for (len ← 128; len ≥ 2; len ← len/2) +/// for (start ← 0; start < 256; start ← start + 2·len) +/// zeta ← ζ^(BitRev₇(k)) mod q +/// k ← k + 1 +/// for (j ← start; j < start + len; j++) +/// t ← zeta·fˆ[j+len] +/// fˆ[j+len] ← fˆ[j] − t +/// fˆ[j] ← fˆ[j] + t +/// end for +/// end for +/// end for +/// return fˆ +/// ``` +/// +/// The NIST FIPS 203 standard can be found at +/// . +pub(crate) fn ntt(f: KyberPolynomialRingElement) -> KyberPolynomialRingElement { + let mut f_hat = f; + let mut k: u8 = 1; + + // for (len ← 128; len ≥ 2; len ← len/2) + for len in NTT_LAYERS.iter().rev() { + // for (start ← 0; start < 256; start ← start + 2·len) + for start in (0..(COEFFICIENTS_IN_RING_ELEMENT - len)).step_by(2 * len) { + // zeta ← ζ^(BitRev₇(k)) mod q + // k ← k + 1 + let zeta = ZETA.pow(bit_rev_7(k)); + k += 1; + + for j in start..start + len { + let t = zeta * f_hat[j + len]; + f_hat[j + len] = f_hat[j] - t; + f_hat[j] = f_hat[j] + t; } } - - out } - /// Two elements `a, b ∈ F_{3329}[x] / (x^2 - zeta^{2i+1})` in the Kyber NTT - /// domain: - /// - /// ```plaintext - /// a = a_0 + a_1 * x - /// b = b_0 + b_1 * x - /// ``` - /// - /// can be multiplied as follows: - /// - /// ```plaintext - /// (a_2 * x + a_1)(b_2 * x + b_1) = - /// (a_0 * b_0 + a_1 * b_1 * zeta^{2i + 1}) + (a_0 * b_1 + a_1 * b_0) * x - /// ``` - /// - /// for the appropriate i. - pub fn ntt_multiply( - left: &KyberPolynomialRingElement, - other: &KyberPolynomialRingElement, - ) -> KyberPolynomialRingElement { - let mut out = KyberPolynomialRingElement::ZERO; - - for i in (0..COEFFICIENTS_IN_RING_ELEMENT).step_by(2) { - let mod_root: KyberFieldElement = MOD_ROOTS[i / 2].into(); - - let a0_times_b0 = left[i] * other[i]; - let a1_times_b1 = left[i + 1] * other[i + 1]; - - let a0_times_b1 = left[i + 1] * other[i]; - let a1_times_b0 = left[i] * other[i + 1]; - - out[i] = a0_times_b0 + (a1_times_b1 * mod_root); - out[i + 1] = a0_times_b1 + a1_times_b0; - } - out - } + f_hat } -pub(crate) fn multiply_matrix_by_column( - matrix: &[[KyberPolynomialRingElement; RANK]; RANK], - vector: &[KyberPolynomialRingElement; RANK], -) -> [KyberPolynomialRingElement; RANK] { - let mut result = [KyberPolynomialRingElement::ZERO; RANK]; - - for (i, row) in matrix.iter().enumerate() { - for (j, matrix_element) in row.iter().enumerate() { - let product = ntt_multiply(matrix_element, &vector[j]); - result[i] = result[i] + product; +/// Use the Gentleman-Sande butterfly to invert, in-place, the NTT representation +/// of a `KyberPolynomialRingElement`. +/// +/// This function implements Algorithm 9 of the NIST FIPS 203 standard, which +/// is reproduced below: +/// +/// ```plaintext +/// Input: array fˆ ∈ ℤ₂₅₆. +/// Output: array f ∈ ℤ₂₅₆. +/// +/// f ← fˆ +/// k ← 127 +/// for (len ← 2; len ≤ 128; len ← 2·len) +/// for (start ← 0; start < 256; start ← start + 2·len) +/// zeta ← ζ^(BitRev₇(k)) mod q +/// k ← k − 1 +/// for (j ← start; j < start + len; j++) +/// t ← f[j] +/// f[j] ← t + f[j + len] +/// f[j + len] ← zeta·(f[j+len] − t) +/// end for +/// end for +/// end for +/// +/// f ← f·3303 mod q +/// return f +/// ``` +/// +/// The NIST FIPS 203 standard can be found at +/// . +pub(crate) fn ntt_inverse(f_hat: KyberPolynomialRingElement) -> KyberPolynomialRingElement { + let mut f = f_hat; + let mut k: u8 = 127; + + // for (len ← 2; len ≤ 128; len ← 2·len) + for len in NTT_LAYERS { + // for (start ← 0; start < 256; start ← start + 2·len) + for start in (0..(COEFFICIENTS_IN_RING_ELEMENT - len)).step_by(2 * len) { + // zeta ← ζ^(BitRev₇(k)) mod q + // k ← k − 1 + let zeta = ZETA.pow(bit_rev_7(k)); + k -= 1; + + for j in start..start + len { + let t = f[j]; + f[j] = t + f[j + len]; + f[j + len] = zeta * (f[j + len] - t); + } } } - result + + // f ← f·3303 mod q + for i in 0..f.coefficients().len() { + f[i] = f[i] * INVERSE_OF_128; + } + + f } -pub(crate) fn multiply_row_by_column( - row_vector: &[KyberPolynomialRingElement; RANK], - column_vector: &[KyberPolynomialRingElement; RANK], +/// Given two `KyberPolynomialRingElement`s in their NTT representations, +/// compute their product. Given two polynomials in the NTT domain `f^` and `ĵ`, +/// the `iᵗʰ` coefficient of the product `k̂` is determined by the calculation: +/// +/// ```plaintext +/// ĥ[2·i] + ĥ[2·i + 1]X = (f^[2·i] + f^[2·i + 1]X)·(ĝ[2·i] + ĝ[2·i + 1]X) mod (X² - ζ^(2·BitRev₇(i) + 1)) +/// ``` +/// +/// This function implements Algorithm 10 of the NIST FIPS 203 standard, which +/// is reproduced below: +/// +/// ```plaintext +/// Input: Two arrays fˆ ∈ ℤ₂₅₆ and ĝ ∈ ℤ₂₅₆. +/// Output: An array ĥ ∈ ℤq. +/// +/// for(i ← 0; i < 128; i++) +/// (ĥ[2i], ĥ[2i+1]) ← BaseCaseMultiply(fˆ[2i], fˆ[2i+1], ĝ[2i], ĝ[2i+1], ζ^(2·BitRev₇(i) + 1)) +/// end for +/// return ĥ +/// ``` +/// +/// The NIST FIPS 203 standard can be found at +/// . +pub(crate) fn multiply_ntts( + f_hat: &KyberPolynomialRingElement, + g_hat: &KyberPolynomialRingElement, ) -> KyberPolynomialRingElement { - let mut result = KyberPolynomialRingElement::ZERO; + let mut h_hat = KyberPolynomialRingElement::ZERO; + + for i in 0..128 { + let binomial_product = base_case_multiply( + (f_hat[2 * i], f_hat[2 * i + 1]), + (g_hat[2 * i], g_hat[2 * i + 1]), + ZETA.pow(2 * bit_rev_7(i.as_u8()) + 1), + ); - for (row_element, column_element) in row_vector.iter().zip(column_vector.iter()) { - result = result + ntt_multiply(row_element, column_element); + h_hat[2 * i] = binomial_product.0; + h_hat[2 * i + 1] = binomial_product.1; } - result + h_hat +} + +/// Represents a binomial `(a₀ + a₁X)` whose coefficients are +/// `KyberFieldElement`s: +/// - the first element of the tuple is `a₀` +/// - the second element of the tuple is `a₁` +type KyberBinomial = (KyberFieldElement, KyberFieldElement); + +/// Compute the product of two `KyberBinomial`s with respect to the +/// modulus `X² - zeta`. +/// +/// This function implements Algorithm 11 of the NIST FIPS 203 standard, which +/// is reproduced below: +/// +/// ```plaintext +/// Input: a₀, a₁, b₀, b₁ ∈ ℤq. +/// Input: γ ∈ ℤq. +/// Output: c₀, c₁ ∈ ℤq. +/// +/// c₀ ← a₀·b₀ + a₁·b₁·γ +/// c₁ ← a₀·b₁ + a₁·b₀ +/// return c₀, c₁ +/// ``` +/// +/// The NIST FIPS 203 standard can be found at +/// . +fn base_case_multiply( + a: KyberBinomial, + b: KyberBinomial, + zeta: KyberFieldElement, +) -> KyberBinomial { + let mut c = (FieldElement::ZERO, FieldElement::ZERO); + + c.0 = (a.0 * b.0) + (a.1 * b.1 * zeta); + c.1 = (a.0 * b.1) + (a.1 * b.0); + + c } #[cfg(test)] mod tests { + use super::*; + use proptest::prelude::*; - use crate::{ - compress::tests::arb_ring_element, - ntt::kyber_polynomial_ring_element_mod::{invert_ntt, ntt_representation}, - }; + use crate::compress::tests::arb_ring_element; + + #[test] + fn seven_bit_reverse() { + assert_eq!(64, bit_rev_7(1)); + assert_eq!(127, bit_rev_7(255)); + assert_eq!(78, bit_rev_7(185)); + } proptest! { #[test] fn to_ntt_and_back(ring_element in arb_ring_element(12)) { - assert_eq!(ring_element, invert_ntt(ntt_representation(ring_element))); + assert_eq!(ring_element, ntt_inverse(ntt(ring_element))); } } } diff --git a/specs/kyber/src/parameters.rs b/specs/kyber/src/parameters.rs index ee56bca18..ad5545678 100644 --- a/specs/kyber/src/parameters.rs +++ b/specs/kyber/src/parameters.rs @@ -9,9 +9,12 @@ pub(crate) const BITS_PER_COEFFICIENT: usize = 12; /// Coefficients per ring element pub(crate) const COEFFICIENTS_IN_RING_ELEMENT: usize = 256; -/// Bits required per ring element +/// Bits required per (uncompressed) ring element pub(crate) const BITS_PER_RING_ELEMENT: usize = COEFFICIENTS_IN_RING_ELEMENT * 12; +/// Bytes required per (uncompressed) ring element +pub(crate) const BYTES_PER_RING_ELEMENT: usize = BITS_PER_RING_ELEMENT / 8; + /// Seed size for rejection sampling. /// /// See for some background regarding @@ -32,18 +35,18 @@ pub(crate) const VECTOR_U_COMPRESSION_FACTOR: usize = 10; pub(crate) const VECTOR_V_COMPRESSION_FACTOR: usize = 4; /// `U` encoding size in bytes -pub(crate) const VECTOR_U_SIZE: usize = +pub(crate) const VECTOR_U_ENCODED_SIZE: usize = (RANK * COEFFICIENTS_IN_RING_ELEMENT * VECTOR_U_COMPRESSION_FACTOR) / 8; /// `V` encoding size in bytes -pub(crate) const VECTOR_V_SIZE: usize = +pub(crate) const VECTOR_V_ENCODED_SIZE: usize = (COEFFICIENTS_IN_RING_ELEMENT * VECTOR_V_COMPRESSION_FACTOR) / 8; pub(crate) const CPA_PKE_KEY_GENERATION_SEED_SIZE: usize = 32; pub(crate) const CPA_PKE_SECRET_KEY_SIZE: usize = (RANK * COEFFICIENTS_IN_RING_ELEMENT * BITS_PER_COEFFICIENT) / 8; pub(crate) const CPA_PKE_PUBLIC_KEY_SIZE: usize = T_AS_NTT_ENCODED_SIZE + 32; -pub(crate) const CPA_PKE_CIPHERTEXT_SIZE: usize = VECTOR_U_SIZE + VECTOR_V_SIZE; +pub(crate) const CPA_PKE_CIPHERTEXT_SIZE: usize = VECTOR_U_ENCODED_SIZE + VECTOR_V_ENCODED_SIZE; pub(crate) const CPA_PKE_MESSAGE_SIZE: usize = 32; pub(crate) const CPA_SERIALIZED_KEY_LEN: usize = CPA_PKE_SECRET_KEY_SIZE + CPA_PKE_PUBLIC_KEY_SIZE diff --git a/specs/kyber/src/sampling.rs b/specs/kyber/src/sampling.rs index d8ef6d3c2..5d33682e5 100644 --- a/specs/kyber/src/sampling.rs +++ b/specs/kyber/src/sampling.rs @@ -1,88 +1,116 @@ -use hacspec_lib::bit_vector::BitVector; +use hacspec_lib::bit_vector::{BitVector, BitVectorChunks}; use crate::{ parameters::{self, KyberFieldElement, KyberPolynomialRingElement}, BadRejectionSamplingRandomnessError, }; -/// Given a series of uniformly random bytes in `|randomness|`, sample uniformly -/// at random a ring element rˆ, which is treated as being the NTT representation -/// of a corresponding polynomial r. +/// If `bytes` contains a set of uniformly random bytes, this function +/// uniformly samples a ring element `â` that is treated as being the NTT representation +/// of the corresponding polynomial `a`. /// -/// This implementation uses rejection sampling; it is therefore possible the -/// supplied randomness is not enough to sample the element, in which case -/// an Err is returned and the caller must try again with fresh randomness. +/// Since rejection sampling is used, it is possible the supplied bytes are +/// not enough to sample the element, in which case an `Err` is returned and the +/// caller must try again with a fresh set of bytes. /// -/// This function implements Algorithm 1 of the Kyber Round 3 specification, -/// which is reproduced below: +/// This function partially implements Algorithm 6 of the NIST FIPS 203 standard, +/// We say "partially" because this implementation only accepts a finite set of +/// bytes as input and returns an error if the set is not enough; Algorithm 6 of +/// the FIPS 203 standard on the other hand samples from an infinite stream of bytes +/// until the ring element is filled. Algorithm 6 is reproduced below: /// /// ```plaintext -/// Input: Byte stream B = b_0, b_1, b_2 ... ∈ B^{*} -/// Output: NTT-representation aˆ ∈ R_q of a ∈ R_q -/// i := 0 -/// j := 0 -/// while j < n do -/// d_1 := b_i + 256·(b_{i+1} mod^{+}16) -/// d_2 := ⌊b_{i+1}/16⌋ + 16·b_{i+2} -/// if d_1 < q then -/// aˆ_{j} := d_1 -/// j := j + 1 +/// Input: byte stream B ∈ 𝔹*. +/// Output: array â ∈ ℤ₂₅₆. +/// +/// i ← 0 +/// j ← 0 +/// while j < 256 do +/// d₁ ← B[i] + 256·(B[i+1] mod 16) +/// d₂ ← ⌊B[i+1]/16⌋ + 16·B[i+2] +/// if d₁ < q then +/// â[j] ← d₁ +/// j ← j + 1 /// end if -/// if d_2 < q and j < n then -/// aˆ_{j} := d_2 -/// j := j + 1 +/// if d₂ < q and j < 256 then +/// â[j] ← d₂ +/// j ← j + 1 /// end if -/// i := i + 3 +/// i ← i + 3 /// end while -/// return aˆ0 + aˆ1X + · · · + aˆ{n−1}X^{n−1} +/// return â /// ``` /// -/// The Kyber Round 3 specification can be found at: -/// -pub fn sample_from_uniform_distribution( - randomness: [u8; parameters::REJECTION_SAMPLING_SEED_SIZE], +/// The NIST FIPS 203 standard can be found at +/// . +pub fn sample_ntt( + bytes: [u8; parameters::REJECTION_SAMPLING_SEED_SIZE], ) -> Result { let mut sampled_coefficients: usize = 0; - let mut out: KyberPolynomialRingElement = KyberPolynomialRingElement::ZERO; + let mut a_hat: KyberPolynomialRingElement = KyberPolynomialRingElement::ZERO; - for bytes in randomness.chunks(3) { - let b = u16::from(bytes[0]); - let b1 = u16::from(bytes[1]); - let b2 = u16::from(bytes[2]); + for byte_chunk in bytes.chunks(3) { + let b = u16::from(byte_chunk[0]); + let b1 = u16::from(byte_chunk[1]); + let b2 = u16::from(byte_chunk[2]); + // d_1 ← B[i] + 256·(B[i+1] mod 16) + // d_2 ← ⌊B[i+1]/16⌋ + 16·B[i+2] let d1 = b + (256 * (b1 % 16)); - - // Integer division is flooring in Rust. let d2 = (b1 / 16) + (16 * b2); - if d1 < parameters::FIELD_MODULUS && sampled_coefficients < out.len() { - out[sampled_coefficients] = d1.into(); + // if d_1 < q then + // â[j] ← d_1 + // j ← j+1 + // end if + if d1 < parameters::FIELD_MODULUS && sampled_coefficients < a_hat.len() { + a_hat[sampled_coefficients] = d1.into(); sampled_coefficients += 1 } - if d2 < parameters::FIELD_MODULUS && sampled_coefficients < out.len() { - out[sampled_coefficients] = d2.into(); + + // if d_2 < q and j < 256 then + // â[j] ← d_2 + // j ← j+1 + // end if + if d2 < parameters::FIELD_MODULUS && sampled_coefficients < a_hat.len() { + a_hat[sampled_coefficients] = d2.into(); sampled_coefficients += 1; } + } - if sampled_coefficients == out.len() { - return Ok(out); - } + if sampled_coefficients == a_hat.len() { + Ok(a_hat) + } else { + Err(BadRejectionSamplingRandomnessError) + } +} + +// Given an iterator `coins` that returns a vector of bits at a time, advance +// the iterator and return the sum of the bits so returned as a `KyberFieldElement`. +// +// This function calls `unwrap()`, meaning the caller assumes the responsibility +// for ensuring `next()`, when called on the iterator, does not come up empty-handed. +fn sum_coins(coins: &mut BitVectorChunks<'_>) -> KyberFieldElement { + let mut sum: u8 = 0; + for coin in coins.next().unwrap() { + sum += coin; } - Err(BadRejectionSamplingRandomnessError) + sum.into() } -/// Given a series of uniformly random bytes in `|randomness|`, sample +/// Given a series of uniformly random bytes in `randomness`, sample /// a ring element from a binomial distribution centered at 0 that uses two sets -/// of `|sampling_coins|` coin flips. If, for example, -/// `|sampling_coins| = ETA`, each ring coefficient is a value `v` such +/// of `eta` coin flips. If, for example, +/// `eta = ETA`, each ring coefficient is a value `v` such /// such that `v ∈ {-ETA, -ETA + 1, ..., 0, ..., ETA + 1, ETA}` and: /// -/// - If v < 0, Pr\[v\] = Pr[-v] -/// - If v >= 0, Pr\[v\] = BINOMIAL_COEFFICIENT(2 * ETA; ETA - v) / 2 ^ (2 * ETA) +/// ```plaintext +/// - If v < 0, Pr[v] = Pr[-v] +/// - If v >= 0, Pr[v] = BINOMIAL_COEFFICIENT(2 * ETA; ETA - v) / 2 ^ (2 * ETA) +/// ``` /// -/// The values v < 0 are mapped to the appropriate -/// `|parameters::KyberFieldElement|`. +/// The values `v < 0` are mapped to the appropriate `KyberFieldElement`. /// /// The expected value is: /// @@ -100,60 +128,46 @@ pub fn sample_from_uniform_distribution( /// = ETA / 2 /// ``` /// -/// This function implements Algorithm 2 of the Kyber Round 3 specification, -/// which is reproduced below: +/// This function implements Algorithm 7 of the NIST FIPS 203 standard, which is +/// reproduced below: /// /// ```plaintext -/// Input: Byte array B = (b_0, b_1, . . . , b_{64η−1}) ∈ B^{64η} -/// Output: Polynomial f ∈ R_q -/// (β_0, . . . , β_{512η−1}) := BytesToBits(B) -/// for i from 0 to 255 do -/// a := sum(j = 0 to η−1)(β_{2iη+j}) -/// b := sum(j = 0 to η−1)(β_{2iη+η+j}) -/// fi := a − b +/// Input: byte array B ∈ 𝔹^{64η}. +/// Output: array f ∈ ℤ₂₅₆. +/// +/// b ← BytesToBits(B) +/// for (i ← 0; i < 256; i++) +/// x ← ∑(j=0 to η - 1) b[2iη + j] +/// y ← ∑(j=0 to η - 1) b[2iη + η + j] +/// f[i] ← x−y mod q /// end for -/// return f_0 +f_1X +f_2X^2 +···+f_255X^255 +/// return f /// ``` /// -/// The Kyber Round 3 specification can be found at: -/// -/// -/// TODO: This requires a different parameter ETA = 3 only when Kyber-512 is -/// used; generalize it. -pub fn sample_from_binomial_distribution( - sampling_coins: usize, - randomness: &[u8], -) -> KyberPolynomialRingElement { - assert_eq!(randomness.len(), sampling_coins * 64); - - let random_bits: BitVector = randomness.into(); - let mut random_bits = random_bits.chunks(sampling_coins); - - let mut sampled: KyberPolynomialRingElement = KyberPolynomialRingElement::ZERO; - - for i in 0..sampled.len() { - let mut coin_tosses: u8 = 0; - for bit in random_bits - .next() - .expect("the assertion ensures there are enough sampling coins") - { - coin_tosses += bit; - } - let coin_tosses_a: KyberFieldElement = coin_tosses.into(); - - coin_tosses = 0; - for bit in random_bits - .next() - .expect("the assertion ensures there are enough sampling coins") - { - coin_tosses += bit; - } - let coin_tosses_b: KyberFieldElement = coin_tosses.into(); +/// The NIST FIPS 203 standard can be found at +/// . +pub fn sample_poly_cbd(eta: usize, bytes: &[u8]) -> KyberPolynomialRingElement { + assert_eq!(bytes.len(), eta * 64); + + // b ← BytesToBits(B) + let bits: BitVector = bytes.into(); + + let mut bits = bits.chunks(eta); + + let mut f: KyberPolynomialRingElement = KyberPolynomialRingElement::ZERO; + + for i in 0..f.len() { + // x ← ∑(j = 0 to η-1) b[2iη + j] + let x: KyberFieldElement = sum_coins(&mut bits); + + // y ← ∑(j = 0 to η-1) b[2iη + η + j] + let y: KyberFieldElement = sum_coins(&mut bits); - sampled[i] = coin_tosses_a - coin_tosses_b; + // f[i] ← x − y mod q + f[i] = x - y; } - sampled + f } #[cfg(test)] @@ -203,8 +217,7 @@ mod tests { #[test] fn uniform_sample_from_all_zeros() { - let r = sample_from_uniform_distribution([0; parameters::REJECTION_SAMPLING_SEED_SIZE]) - .unwrap(); + let r = sample_ntt([0; parameters::REJECTION_SAMPLING_SEED_SIZE]).unwrap(); for coefficient in r.into_iter() { assert_eq!(coefficient.value, 0); @@ -214,9 +227,7 @@ mod tests { #[test] #[should_panic] fn uniform_sample_from_all_u8_max() { - let _ = - sample_from_uniform_distribution([u8::MAX; parameters::REJECTION_SAMPLING_SEED_SIZE]) - .unwrap(); + let _ = sample_ntt([u8::MAX; parameters::REJECTION_SAMPLING_SEED_SIZE]).unwrap(); } proptest! { @@ -226,7 +237,7 @@ mod tests { let mut sampling_attempts = (0..REJECTION_SAMPLING_ATTEMPTS).peekable(); while let Some(attempt) = sampling_attempts.next() { - let sampled = sample_from_uniform_distribution(randomness[attempt*parameters::REJECTION_SAMPLING_SEED_SIZE..(attempt+1)*parameters::REJECTION_SAMPLING_SEED_SIZE].try_into().unwrap()); + let sampled = sample_ntt(randomness[attempt*parameters::REJECTION_SAMPLING_SEED_SIZE..(attempt+1)*parameters::REJECTION_SAMPLING_SEED_SIZE].try_into().unwrap()); if sampled.is_ok() { sampled_ring_element = sampled.unwrap(); @@ -277,8 +288,8 @@ mod tests { let mut mean : f64 = 0.0; let mut variance : f64 = 0.0; - let ring_element_1 = sample_from_binomial_distribution(sampling_coins, &randomness[0..sampling_coins * 64]); - let ring_element_2 = sample_from_binomial_distribution(sampling_coins, &randomness[sampling_coins * 64..]); + let ring_element_1 = sample_poly_cbd(sampling_coins, &randomness[0..sampling_coins * 64]); + let ring_element_2 = sample_poly_cbd(sampling_coins, &randomness[sampling_coins * 64..]); let all_coefficients = ring_element_1 .into_iter()