diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 080f0b8..8a0c4be 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -10,6 +10,7 @@ use criterion::criterion_main; pub mod pfdh; +pub mod psf; pub mod regev; -criterion_main! {regev::benches, pfdh::benches} +criterion_main! {regev::benches, pfdh::benches, psf::benches} diff --git a/benches/psf.rs b/benches/psf.rs new file mode 100644 index 0000000..bfe09cd --- /dev/null +++ b/benches/psf.rs @@ -0,0 +1,100 @@ +// Copyright © 2025 Niklas Siemer +// +// This file is part of qFALL-crypto. +// +// qFALL-crypto is free software: you can redistribute it and/or modify it under +// the terms of the Mozilla Public License Version 2.0 as published by the +// Mozilla Foundation. See . + +use criterion::{criterion_group, Criterion}; +use qfall_crypto::{ + primitive::psf::{PSFPerturbation, PSF, PSFGPV}, + sample::g_trapdoor::gadget_parameters::GadgetParameters, +}; +use qfall_math::{integer_mod_q::MatZq, rational::Q}; + +/// Benchmark [bench_psf] with `n = 8`. +/// +/// This benchmark can be run with for example: +/// - `cargo criterion PSF\ GPV\ n=8` +/// - `cargo bench --bench benchmarks PSF\ GPV\ n=8` +/// - `cargo flamegraph --bench benchmarks -- --bench PSF\ GPV\ n=8` +/// +/// Shorter variants or regex expressions can also be used to specify the +/// benchmark name. The `\ ` is used to escape the space, alternatively, +/// quotation marks can be used. +fn bench_psf(c: &mut Criterion) { + let (n, q) = (8, 128); + + let psf = PSFGPV { + gp: GadgetParameters::init_default(n, q), + // multiply with the rounding parameter from next test to have the same samplign parameter + s: Q::from(30) * Q::from(n).log(2).unwrap(), + }; + + let target = MatZq::sample_uniform(n, 1, q); + let (a, r) = psf.trap_gen(); + + c.bench_function("PSF GPV n=8", |b| b.iter(|| psf.samp_p(&a, &r, &target))); +} + +/// Benchmark [bench_psf_perturbation] with `n = 8`. +/// +/// This benchmark can be run with for example: +/// - `cargo criterion PSF\ Perturbation\ n=8` +/// - `cargo bench --bench benchmarks PSF\ Perturbation\ n=8` +/// - `cargo flamegraph --bench benchmarks -- --bench PSF\ Perturbation\ n=8` +/// +/// Shorter variants or regex expressions can also be used to specify the +/// benchmark name. The `\ ` is used to escape the space, alternatively, +/// quotation marks can be used. +fn bench_psf_perturbation(c: &mut Criterion) { + let (n, q) = (8, 128); + + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(n, q), + s: Q::from(30), + r: Q::from(n).log(2).unwrap(), + }; + + let target = MatZq::sample_uniform(n, 1, q); + let (a, r) = psf.trap_gen(); + + c.bench_function("PSF Perturbation n=8", |b| { + b.iter(|| psf.samp_p(&a, &r, &target)) + }); +} + +/// Benchmark [bench_psf_perturbation] with `n = 64`. +/// +/// This benchmark can be run with for example: +/// - `cargo criterion PSF\ Perturbation\ n=64` +/// - `cargo bench --bench benchmarks PSF\ Perturbation\ n=64` +/// - `cargo flamegraph --bench benchmarks -- --bench PSF\ Perturbation\ n=64` +/// +/// Shorter variants or regex expressions can also be used to specify the +/// benchmark name. The `\ ` is used to escape the space, alternatively, +/// quotation marks can be used. +fn bench_psf_perturbation_larger(c: &mut Criterion) { + let (n, q) = (64, 128); + + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(n, q), + s: Q::from(100), + r: Q::from(n).log(2).unwrap(), + }; + + let target = MatZq::sample_uniform(n, 1, q); + let (a, r) = psf.trap_gen(); + + c.bench_function("PSF Perturbation n=64", |b| { + b.iter(|| psf.samp_p(&a, &r, &target)) + }); +} + +criterion_group!( + benches, + bench_psf, + bench_psf_perturbation, + bench_psf_perturbation_larger, +); diff --git a/src/primitive/psf.rs b/src/primitive/psf.rs index 3502a18..33cdce4 100644 --- a/src/primitive/psf.rs +++ b/src/primitive/psf.rs @@ -20,12 +20,18 @@ //! January. Implementation and evaluation of improved Gaussian sampling for lattice //! trapdoors. In Proceedings of the 6th Workshop on Encrypted Computing & Applied //! Homomorphic Cryptography (pp. 61-71). +//! - \[3\] Peikert, Chris. +//! An efficient and parallel Gaussian sampler for lattices. +//! In: Annual Cryptology Conference - CRYPTO 2010. +//! Springer, Berlin, Heidelberg. mod gpv; mod gpv_ring; +mod mp_perturbation; pub use gpv::PSFGPV; pub use gpv_ring::PSFGPVRing; +pub use mp_perturbation::PSFPerturbation; /// This trait should be implemented by all constructions that are /// actual implementations of a preimage sampleable function. diff --git a/src/primitive/psf/mp_perturbation.rs b/src/primitive/psf/mp_perturbation.rs new file mode 100644 index 0000000..ac99c94 --- /dev/null +++ b/src/primitive/psf/mp_perturbation.rs @@ -0,0 +1,562 @@ +// Copyright © 2025 Niklas Siemer +// +// This file is part of qFALL-crypto. +// +// qFALL-crypto is free software: you can redistribute it and/or modify it under +// the terms of the Mozilla Public License Version 2.0 as published by the +// Mozilla Foundation. See . + +//! Implements a Perturbation MP12 PSF according to [\[1\]](<../index.html#:~:text=[1]>) +//! using G-Trapdoors and corresponding trapdoor. + +use super::PSF; +use crate::sample::g_trapdoor::{ + gadget_classical::short_basis_gadget, + gadget_classical::{find_solution_gadget_mat, gen_trapdoor}, + gadget_parameters::GadgetParameters, +}; +use qfall_math::{ + integer::{MatZ, Z}, + integer_mod_q::MatZq, + rational::{MatQ, Q}, + traits::{Concatenate, MatrixDimensions, Pow}, +}; +use serde::{Deserialize, Serialize}; + +/// A lattice-based implementation of a [`PSF`] according to +/// [\[1\]]() using +/// G-Trapdoors where D_n = {e ∈ Z^m | |e| <= s sqrt(m)} +/// and R_n = Z_q^n. +/// +/// Attributes +/// - `gp`: Describes the gadget parameters with which the G-Trapdoor is generated +/// - `r`: The rounding parameter +/// - `s`: The Gaussian parameter with which is sampled +/// +/// # Examples +/// ``` +/// use qfall_crypto::primitive::psf::PSFPerturbation; +/// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; +/// use qfall_math::rational::Q; +/// use qfall_crypto::primitive::psf::PSF; +/// +/// let psf = PSFPerturbation { +/// gp: GadgetParameters::init_default(8, 64), +/// r: Q::from(3), +/// s: Q::from(25), +/// }; +/// +/// let (a, td) = psf.trap_gen(); +/// let domain_sample = psf.samp_d(); +/// let range_fa = psf.f_a(&a, &domain_sample); +/// let preimage = psf.samp_p(&a, &td, &range_fa); +/// +/// assert!(psf.check_domain(&preimage)); +/// assert_eq!(a * preimage, range_fa); +/// ``` +#[derive(Serialize, Deserialize)] +pub struct PSFPerturbation { + pub gp: GadgetParameters, + pub r: Q, + pub s: Q, +} + +impl PSFPerturbation { + /// Computes √Σ_2 = √(r^2 * (b^2 + 1) * [R^t | I]^t * [R^t | I] - r^2 * I) + /// to perform non-spherical Gaussian sampling according to Algorithm 1 in + /// [\[3\]](). + /// This matrix is the second part of the secret key and needs to be precomputed + /// to execute [PSFPerturbation::samp_p]. + /// [PSFPerturbation::trap_gen] outputs this matrix for `s^2 * I`, i.e. for discrete + /// Gaussian preimages centered around `0`. This function enables changing the + /// covariance matrix to any covariance matrix s.t. Σ_2 is positive definite. + /// + /// Parameters: + /// - `mat_r`: The trapdoor matrix `R` + /// - `mat_sigma`: The covariance matrix `Σ` to sample [`Self::samp_p`] with + /// + /// Returns a [`MatQ`] containing √Σ_2 = √(r^2 * (b^2 + 1) * [R^t | I]^t * [R^t | I] - r^2 * I) + /// if Σ_2 was positive definite. + /// + /// # Examples + /// ``` + /// use qfall_crypto::primitive::psf::PSFPerturbation; + /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; + /// use qfall_math::rational::{Q, MatQ}; + /// use qfall_crypto::primitive::psf::PSF; + /// use qfall_math::traits::*; + /// + /// let psf = PSFPerturbation { + /// gp: GadgetParameters::init_default(8, 64), + /// r: Q::from(3), + /// s: Q::from(25), + /// }; + /// let different_s: f64 = 35.0; + /// + /// let (a, td) = psf.trap_gen(); + /// + /// let cov_mat = different_s.powi(2) * MatQ::identity(a.get_num_columns(), a.get_num_columns()); + /// let mat_sqrt_sigma_2 = psf.compute_sqrt_sigma_2(&td.0, &cov_mat); + /// let new_td = (td.0, mat_sqrt_sigma_2, td.2); + /// + /// let domain_sample = psf.samp_d(); + /// let range_fa = psf.f_a(&a, &domain_sample); + /// let preimage = psf.samp_p(&a, &new_td, &range_fa); + /// + /// assert!(psf.check_domain(&preimage)); + /// ``` + /// + /// # Panics ... + /// - if Σ_2 is not positive definite. + pub fn compute_sqrt_sigma_2(&self, mat_r: &MatZ, mat_sigma: &MatQ) -> MatQ { + // Normalization factor according to MP12, Section 2.3 + let normalization_factor = 1.0 / (2.0 * Q::PI); + + // full_td = [R^t | I]^t + let full_td = mat_r + .concat_vertical(&MatZ::identity( + mat_sigma.get_num_rows() - mat_r.get_num_rows(), + mat_r.get_num_columns(), + )) + .unwrap(); + + // Assemble Σ_p = Σ - [R^t|I]^t * Σ_G * [R^t|I] with √Σ_G ≥ η (Λ^⊥(G)) + // and assumption Σ_G = (base^2 + 1) * I as ||S|| = √(b^2 + 1), Theorem 4.1, MP12 + let mat_sigma_p: MatQ = + mat_sigma - (self.gp.base.pow(2).unwrap() + 1) * &full_td * full_td.transpose(); + + // r^2 * Σ_p <=> r * √Σ_p + // Compute Σ_2 according to the requirements of Algorithm 1 in [3] + // Assume Σ_1 = r^2 * B_1 * B_1^t with B_1 = I as basis for ZZ^n + // Then, Σ_2 = Σ - Σ_1, where Σ = r^2 * Σ_p + let sigma_2: MatQ = normalization_factor + * self.r.pow(2).unwrap() + * (&mat_sigma_p + - MatQ::identity(mat_sigma_p.get_num_rows(), mat_sigma_p.get_num_columns())); + + // Compute √Σ_2 + sigma_2.cholesky_decomposition_flint() + } +} + +/// Samples a preimage with respect to the gadget matrix `G` of `vec_u`. +/// +/// Parameters: +/// - `psf`: The [`PSFPerturbation`] that sets the parameters for this function +/// - `vec_u`: The target vector +/// - `short_basis_gadget`: The short basis of the corresponding gadget-matrix - just required to speed up the algorithm +/// - `short_basis_gadget_gso`: The GSO of the short basis of the gadget-matrix - just required to speed up the algorithm +/// +/// Returns a discrete Gaussian sampled preimage of `u` under `G` with Gaussian parameter `r * √(b^2 + 1)`, +/// where `b` is the base used for the G-trapdoor. +/// +/// # Examples +/// ```compile_fail +/// use qfall_crypto::primitive::psf::PSFPerturbation; +/// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; +/// use qfall_crypto::sample::g_trapdoor::gadget_classical::short_basis_gadget; +/// use qfall_math::rational::Q; +/// use qfall_crypto::primitive::psf::PSF; +/// +/// let psf = PSFPerturbation { +/// gp: GadgetParameters::init_default(8, 64), +/// r: Q::from(3), +/// s: Q::from(25), +/// }; +/// +/// let short_basis_g = short_basis_gadget(&psf.gp); +/// let short_basis_g_gso = MatQ::from(&short_basis_g).gso(); +/// +/// let target = MatZq::sample_uniform(8, 1, 64); +/// let preimage = randomized_nearest_plane_gadget(&osf, &target, &short_basis_g, &short_basis_g_gso); +/// ``` +pub(crate) fn randomized_nearest_plane_gadget( + psf: &PSFPerturbation, + vec_u: &MatZq, + short_basis_gadget: &MatZ, + short_basis_gadget_gso: &MatQ, +) -> MatZ { + // s = r * √(b^2 + 1) according to Algorithm 3 in [1] + let s = &psf.r * (psf.gp.base.pow(2).unwrap() + Z::ONE).sqrt(); + + // find solution s.t. G * long_solution = vec_u + let long_solution = find_solution_gadget_mat(vec_u, &psf.gp.k, &psf.gp.base); + + let center = MatQ::from(&(-1 * &long_solution)); + + // just as PSFGPV::samp_p + long_solution + + MatZ::sample_d_precomputed_gso( + short_basis_gadget, + short_basis_gadget_gso, + &psf.gp.n, + ¢er, + &s, + ) + .unwrap() +} + +impl PSF for PSFPerturbation { + type A = MatZq; + type Trapdoor = (MatZ, MatQ, (MatZ, MatQ)); + type Domain = MatZ; + type Range = MatZq; + + /// Computes a G-Trapdoor according to the [`GadgetParameters`] defined in the struct. + /// It returns a matrix `A` together with the short trapdoor matrix `R` and a precomputed √Σ_2 + /// for covariance matrix `s^2 * I`, i.e. for preimage sampling centered around `0`. + /// The last part of the trapdoor tuple contains a short basis for the gadget matrix `G` and its + /// GSO, which removes the need to recompute the GSO for each iteration and speeds up preimage sampling + /// drastically. + /// + /// # Examples + /// ``` + /// use qfall_crypto::primitive::psf::PSFPerturbation; + /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; + /// use qfall_math::rational::Q; + /// use qfall_crypto::primitive::psf::PSF; + /// + /// let psf = PSFPerturbation { + /// gp: GadgetParameters::init_default(8, 64), + /// r: Q::from(3), + /// s: Q::from(25), + /// }; + /// + /// let (mat_a, (mat_r, mat_sqrt_sigma_2, (sh_b_g, sh_b_g_gso))) = psf.trap_gen(); + /// ``` + fn trap_gen(&self) -> (MatZq, (MatZ, MatQ, (MatZ, MatQ))) { + let mat_a_bar = MatZq::sample_uniform(&self.gp.n, &self.gp.m_bar, &self.gp.q); + let tag = MatZq::identity(&self.gp.n, &self.gp.n, &self.gp.q); + + let (mat_a, mat_r) = gen_trapdoor(&self.gp, &mat_a_bar, &tag).unwrap(); + + let mat_sqrt_sigma_2 = self.compute_sqrt_sigma_2( + &mat_r, + &(&self.s.pow(2).unwrap() + * MatQ::identity(mat_a.get_num_columns(), mat_a.get_num_columns())), + ); + + let short_basis_gadget = short_basis_gadget(&self.gp); + let short_basis_gadget_gso = MatQ::from(&short_basis_gadget).gso(); + + ( + mat_a, + ( + mat_r, + mat_sqrt_sigma_2, + (short_basis_gadget, short_basis_gadget_gso), + ), + ) + } + + /// Samples in the domain using SampleD with the standard basis and center `0`. + /// + /// # Examples + /// ``` + /// use qfall_crypto::primitive::psf::PSFPerturbation; + /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; + /// use qfall_math::rational::Q; + /// use qfall_crypto::primitive::psf::PSF; + /// + /// let psf = PSFPerturbation { + /// gp: GadgetParameters::init_default(8, 64), + /// r: Q::from(3), + /// s: Q::from(25), + /// }; + /// let (mat_a, td) = psf.trap_gen(); + /// + /// let domain_sample = psf.samp_d(); + /// ``` + fn samp_d(&self) -> MatZ { + let m = &self.gp.n * &self.gp.k + &self.gp.m_bar; + MatZ::sample_d_common(&m, &self.gp.n, &self.s * &self.r).unwrap() + } + + /// Samples an `e` in the domain using SampleD that is generated + /// from the G-Trapdoor from the conditioned + /// discrete Gaussian with `f_a(a,e) = u` for a provided syndrome `u`. + /// + /// *Note*: the provided parameters `mat_a, mat_r, vec_u` must fit together, + /// otherwise unexpected behavior such as panics may occur. + /// + /// Parameters: + /// - `mat_a`: The parity-check matrix + /// - `mat_r`: The short trapdoor matrix `R` + /// - `mat_sqrt_sigma_2`: The precomputed √Σ_2 + /// - `vec_u`: The syndrome from the range + /// + /// Returns a sample `e` from the domain on the conditioned discrete + /// Gaussian distribution `f_a(a,e) = u` with covariance matrix Σ depending on `mat_sqrt_sigma_2`. + /// + /// # Examples + /// ``` + /// use qfall_crypto::primitive::psf::PSFPerturbation; + /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; + /// use qfall_math::rational::Q; + /// use qfall_crypto::primitive::psf::PSF; + /// + /// let psf = PSFPerturbation { + /// gp: GadgetParameters::init_default(8, 64), + /// r: Q::from(3), + /// s: Q::from(25), + /// }; + /// let (mat_a, td) = psf.trap_gen(); + /// let domain_sample = psf.samp_d(); + /// let range_fa = psf.f_a(&mat_a, &domain_sample); + /// + /// let preimage = psf.samp_p(&mat_a, &td, &range_fa); + /// assert_eq!(range_fa, psf.f_a(&mat_a, &preimage)) + /// ``` + fn samp_p( + &self, + mat_a: &MatZq, + (mat_r, mat_sqrt_sigma_2, (short_basis_gadget, short_basis_gadget_gso)): &( + MatZ, + MatQ, + (MatZ, MatQ), + ), + vec_u: &MatZq, + ) -> MatZ { + // Sample perturbation p <- D_{ZZ^m, r * √Σ_2} + let vec_p = + MatZ::sample_d_common_non_spherical(&self.gp.n, mat_sqrt_sigma_2, &self.r).unwrap(); + + // v = u - A * p + let vec_v = vec_u - mat_a * &vec_p; + + // z <- D_{Λ_v^⊥(G), r * √Σ_G} + let vec_z = randomized_nearest_plane_gadget( + self, + &vec_v, + short_basis_gadget, + short_basis_gadget_gso, + ); + + let full_td = mat_r + .concat_vertical(&MatZ::identity( + mat_r.get_num_columns(), + mat_r.get_num_columns(), + )) + .unwrap(); + + vec_p + full_td * vec_z + } + + /// Implements the efficiently computable function `f_a` which here corresponds to + /// `mat_a * sigma`. The sigma must be from the domain, i.e. D_n. + /// + /// Parameters: + /// - `mat_a`: The parity-check matrix of dimensions `n x m` + /// - `sigma`: A column vector of length `m` + /// + /// Returns `mat_a * sigma`. + /// + /// # Examples + /// ``` + /// use qfall_crypto::primitive::psf::PSFPerturbation; + /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; + /// use qfall_math::rational::Q; + /// use qfall_crypto::primitive::psf::PSF; + /// + /// let psf = PSFPerturbation { + /// gp: GadgetParameters::init_default(8, 64), + /// r: Q::from(3), + /// s: Q::from(25), + /// }; + /// let (mat_a, td) = psf.trap_gen(); + /// let domain_sample = psf.samp_d(); + /// let range_fa = psf.f_a(&mat_a, &domain_sample); + /// ``` + /// + /// # Panics ... + /// - if `sigma` is not in the domain. + fn f_a(&self, mat_a: &MatZq, sigma: &MatZ) -> MatZq { + assert!(self.check_domain(sigma)); + mat_a * sigma + } + + /// Checks whether a value `sigma` is in D_n = {e ∈ Z^m | |e| <= s * r * sqrt(m)}. + /// + /// Parameters: + /// - `sigma`: The value for which is checked, if it is in the domain + /// + /// Returns true, if `sigma` is in D_n. + /// + /// # Examples + /// ``` + /// use qfall_crypto::primitive::psf::PSF; + /// use qfall_crypto::primitive::psf::PSFPerturbation; + /// use qfall_crypto::sample::g_trapdoor::gadget_parameters::GadgetParameters; + /// use qfall_math::rational::Q; + /// + /// let psf = PSFPerturbation { + /// gp: GadgetParameters::init_default(8, 64), + /// r: Q::from(3), + /// s: Q::from(25), + /// }; + /// let (mat_a, td) = psf.trap_gen(); + /// + /// let vector = psf.samp_d(); + /// + /// assert!(psf.check_domain(&vector)); + /// ``` + fn check_domain(&self, sigma: &MatZ) -> bool { + let m = &self.gp.n * &self.gp.k + &self.gp.m_bar; + sigma.is_column_vector() + && m == sigma.get_num_rows() + && sigma.norm_eucl_sqrd().unwrap() + <= self.s.pow(2).unwrap() * &m * &self.r.pow(2).unwrap() + } +} + +#[cfg(test)] +mod test_psf_perturbation { + use super::PSFPerturbation; + use super::PSF; + use crate::sample::g_trapdoor::gadget_parameters::GadgetParameters; + use qfall_math::integer::MatZ; + use qfall_math::rational::Q; + use qfall_math::traits::*; + + /// Ensures that `samp_d` actually computes values that are in D_n. + #[test] + fn samp_d_samples_from_dn() { + for (n, q) in [(5, 256), (10, 128), (15, 157)] { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(n, q), + r: Q::from(n).log(2).unwrap(), + s: Q::from(25), + }; + + for _ in 0..5 { + assert!(psf.check_domain(&psf.samp_d())); + } + } + } + + /// Ensures that `samp_p` actually computes preimages that are also in the correct + /// domain. + #[test] + fn samp_p_preimage_and_domain() { + for (n, q) in [(5, 256), (6, 128)] { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(n, q), + r: Q::from(n).log(2).unwrap(), + s: Q::from(25), + }; + let (a, r) = psf.trap_gen(); + let domain_sample = psf.samp_d(); + let range_fa = psf.f_a(&a, &domain_sample); + + let preimage = psf.samp_p(&a, &r, &range_fa); + assert_eq!(range_fa, psf.f_a(&a, &preimage)); + assert!(psf.check_domain(&preimage)); + } + } + + /// Ensures that `f_a` returns `a * sigma`. + #[test] + fn f_a_works_as_expected() { + for (n, q) in [(5, 256), (6, 128)] { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(n, q), + r: Q::from(n).log(2).unwrap(), + s: Q::from(25), + }; + let (a, _) = psf.trap_gen(); + let domain_sample = psf.samp_d(); + + assert_eq!(&a * &domain_sample, psf.f_a(&a, &domain_sample)); + } + } + + /// Ensures that `f_a` panics if a value is provided, that is not within the domain. + /// Sigma is not a vector. + #[test] + #[should_panic] + fn f_a_sigma_not_in_domain_matrix() { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(8, 128), + r: Q::from(8).log(2).unwrap(), + s: Q::from(25), + }; + let (a, _) = psf.trap_gen(); + let not_in_domain = MatZ::new(a.get_num_columns(), 2); + + let _ = psf.f_a(&a, ¬_in_domain); + } + + /// Ensures that `f_a` panics if a value is provided, that is not within the domain. + /// Sigma is not of the correct length. + #[test] + #[should_panic] + fn f_a_sigma_not_in_domain_incorrect_length() { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(8, 128), + r: Q::from(8).log(2).unwrap(), + s: Q::from(25), + }; + let (a, _) = psf.trap_gen(); + let not_in_domain = MatZ::new(a.get_num_columns() - 1, 1); + + let _ = psf.f_a(&a, ¬_in_domain); + } + + /// Ensures that `f_a` panics if a value is provided, that is not within the domain. + /// Sigma is too long. + #[test] + #[should_panic] + fn f_a_sigma_not_in_domain_too_long() { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(8, 128), + r: Q::from(8).log(2).unwrap(), + s: Q::from(25), + }; + let (a, _) = psf.trap_gen(); + let not_in_domain = + psf.s.round() * a.get_num_columns() * MatZ::identity(a.get_num_columns(), 1); + + let _ = psf.f_a(&a, ¬_in_domain); + } + + /// Ensures that `check_domain` works for vectors with the correct length. + #[test] + fn check_domain_as_expected() { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(8, 128), + r: Q::from(8).log(2).unwrap(), + s: Q::from(25), + }; + let (a, _) = psf.trap_gen(); + let value = psf.s.round(); + let mut in_domain = MatZ::new(a.get_num_columns(), 1); + for i in 0..in_domain.get_num_rows() { + in_domain.set_entry(i, 0, &value).unwrap(); + } + + assert!(psf.check_domain(&MatZ::new(a.get_num_columns(), 1))); + assert!(psf.check_domain(&in_domain)); + } + + /// Ensures that `check_domain` returns false for values that are not in the domain. + #[test] + fn check_domain_not_in_dn() { + let psf = PSFPerturbation { + gp: GadgetParameters::init_default(8, 128), + r: Q::from(8).log(2).unwrap(), + s: Q::from(25), + }; + let (a, _) = psf.trap_gen(); + + let matrix = MatZ::new(a.get_num_columns(), 2); + let too_short = MatZ::new(a.get_num_columns() - 1, 1); + let too_long = MatZ::new(a.get_num_columns() + 1, 1); + let entry_too_large = + psf.s.round() * a.get_num_columns() * MatZ::identity(a.get_num_columns(), 1); + + assert!(!psf.check_domain(&matrix)); + assert!(!psf.check_domain(&too_long)); + assert!(!psf.check_domain(&too_short)); + assert!(!psf.check_domain(&entry_too_large)); + } +} diff --git a/src/sample/g_trapdoor/gadget_classical.rs b/src/sample/g_trapdoor/gadget_classical.rs index a56b790..94dcc6a 100644 --- a/src/sample/g_trapdoor/gadget_classical.rs +++ b/src/sample/g_trapdoor/gadget_classical.rs @@ -228,6 +228,64 @@ pub fn find_solution_gadget_mat(value: &MatZq, k: &Z, base: &Z) -> MatZ { out } +/// Outputs the short basis `S` of any gadget matrix `G`. +/// +/// Parameters: +/// - `params`: The [`GadgetParameters`] that define the gadget matrix `G` +/// +/// Returns a [`MatZ`] a short basis matrix `S` for `G`. +/// +/// # Examples +/// ``` +/// use qfall_crypto::sample::g_trapdoor::{gadget_parameters::GadgetParameters, +/// gadget_default::gen_trapdoor_default}; +/// use qfall_crypto::sample::g_trapdoor::gadget_classical::short_basis_gadget; +/// +/// let params = GadgetParameters::init_default(10, 127); +/// +/// let short_basis_gadget = short_basis_gadget(¶ms); +/// ``` +pub fn short_basis_gadget(params: &GadgetParameters) -> MatZ { + let mut sk = MatZ::new(¶ms.k, ¶ms.k); + let n: i64 = (¶ms.n).try_into().unwrap(); + let k: i64 = (¶ms.k).try_into().unwrap(); + for j in 0..k { + sk.set_entry(j, j, ¶ms.base).unwrap(); + } + for i in 0..k - 1 { + sk.set_entry(i + 1, i, Z::MINUS_ONE).unwrap(); + } + sk = if params.base.pow(k).unwrap() == params.q { + // compute s in the special case where the modulus is a power of base + // i.e. the last column can remain as it is + sk + } else { + // compute s for any arbitrary modulus + // represent modulus in `base` and set last row accordingly + let mut q = Z::from(¶ms.q); + for i in 0..k { + let q_i = &q % ¶ms.base; + sk.set_entry(i, k - 1, &q_i).unwrap(); + q = (q - q_i).div_exact(¶ms.base).unwrap(); + } + sk + }; + let mut out = MatZ::new(n * k, n * k); + for j in 0..n { + out.set_submatrix( + j * sk.get_num_rows(), + j * sk.get_num_columns(), + &sk, + 0, + 0, + -1, + -1, + ) + .unwrap(); + } + out +} + #[cfg(test)] mod test_gen_gadget_vec { use crate::sample::g_trapdoor::gadget_classical::gen_gadget_vec; @@ -420,3 +478,96 @@ mod test_find_solution_gadget { ) } } + +#[cfg(test)] +mod test_short_basis_gadget { + use crate::sample::g_trapdoor::{ + gadget_classical::short_basis_gadget, gadget_parameters::GadgetParameters, + }; + use qfall_math::integer::{MatZ, Z}; + use std::str::FromStr; + + /// Ensure that the matrix s is computed correctly for a power-of-two modulus. + #[test] + fn base_2_power_two() { + let params = GadgetParameters::init_default(2, 16); + + let s = short_basis_gadget(¶ms); + + let s_cmp = MatZ::from_str( + "[[2, 0, 0, 0, 0, 0, 0, 0],\ + [-1, 2, 0, 0, 0, 0, 0, 0],\ + [0, -1, 2, 0, 0, 0, 0, 0],\ + [0, 0, -1, 2, 0, 0, 0, 0],\ + [0, 0, 0, 0, 2, 0, 0, 0],\ + [0, 0, 0, 0, -1, 2, 0, 0],\ + [0, 0, 0, 0, 0, -1, 2, 0],\ + [0, 0, 0, 0, 0, 0, -1, 2]]", + ) + .unwrap(); + assert_eq!(s_cmp, s) + } + + /// Ensure that the matrix s is computed correctly for a an arbitrary modulus. + #[test] + fn base_2_arbitrary() { + let q = Z::from(0b1100110); + let params = GadgetParameters::init_default(1, q); + + let s = short_basis_gadget(¶ms); + + let s_cmp = MatZ::from_str( + "[[2, 0, 0, 0, 0, 0, 0],\ + [-1, 2, 0, 0, 0, 0, 1],\ + [0, -1, 2, 0, 0, 0, 1],\ + [0, 0, -1, 2, 0, 0, 0],\ + [0, 0, 0, -1, 2, 0, 0],\ + [0, 0, 0, 0, -1, 2, 1],\ + [0, 0, 0, 0, 0, -1, 1]]", + ) + .unwrap(); + + assert_eq!(s_cmp, s) + } + + /// Ensure that the matrix s is computed correctly for a power-of-5 modulus. + #[test] + fn base_5_power_5() { + let mut params = GadgetParameters::init_default(1, 625); + params.k = Z::from(4); + params.base = Z::from(5); + + let s = short_basis_gadget(¶ms); + + let s_cmp = MatZ::from_str( + "[[5, 0, 0, 0],\ + [-1, 5, 0, 0],\ + [0, -1, 5, 0],\ + [0, 0, -1, 5]]", + ) + .unwrap(); + assert_eq!(s_cmp, s) + } + + /// Ensure that the matrix s is computed correctly for an arbitrary modulus with + /// base 5. + #[test] + fn base_5_arbitrary() { + let q = Z::from_str_b("4123", 5).unwrap(); + let mut params = GadgetParameters::init_default(1, q); + params.k = Z::from(4); + params.base = Z::from(5); + + let s = short_basis_gadget(¶ms); + + let s_cmp = MatZ::from_str( + "[[5, 0, 0, 3],\ + [-1, 5, 0, 2],\ + [0, -1, 5, 1],\ + [0, 0, -1, 4]]", + ) + .unwrap(); + + assert_eq!(s_cmp, s) + } +} diff --git a/src/sample/g_trapdoor/short_basis_classical.rs b/src/sample/g_trapdoor/short_basis_classical.rs index ad229be..ff66b3d 100644 --- a/src/sample/g_trapdoor/short_basis_classical.rs +++ b/src/sample/g_trapdoor/short_basis_classical.rs @@ -9,7 +9,10 @@ //! This module contains an implementation to generate a short basis from a G-Trapdoor //! and its parity check matrix. -use super::{gadget_classical::find_solution_gadget_mat, gadget_parameters::GadgetParameters}; +use super::{ + gadget_classical::{find_solution_gadget_mat, short_basis_gadget}, + gadget_parameters::GadgetParameters, +}; use qfall_math::{ integer::{MatZ, Z}, integer_mod_q::MatZq, @@ -37,7 +40,7 @@ use qfall_math::{ /// # Examples /// ``` /// use qfall_crypto::sample::g_trapdoor::{gadget_parameters::GadgetParameters, -/// gadget_default::gen_trapdoor_default}; +/// gadget_default::gen_trapdoor_default}; /// use qfall_crypto::sample::g_trapdoor::short_basis_classical::gen_short_basis_for_trapdoor; /// use qfall_math::integer_mod_q::MatZq; /// @@ -72,7 +75,7 @@ fn gen_sa_l(r: &MatZ) -> MatZ { /// Computes `[ 0 | I, S' | W ]` fn gen_sa_r(params: &GadgetParameters, tag: &MatZq, a: &MatZq) -> MatZ { - let mut s = compute_s(params); + let mut s = short_basis_gadget(params); // if `base^k = q`, then the reverse of `S` has a shorter diagonalization if params.base.pow(¶ms.k).unwrap() == params.q { s.reverse_columns(); @@ -98,48 +101,6 @@ fn gen_sa_r(params: &GadgetParameters, tag: &MatZq, a: &MatZq) -> MatZ { sa_r } -/// Compute S for `[ 0 | I, S' | W ]` -fn compute_s(params: &GadgetParameters) -> MatZ { - let mut sk = MatZ::new(¶ms.k, ¶ms.k); - let n: i64 = (¶ms.n).try_into().unwrap(); - let k: i64 = (¶ms.k).try_into().unwrap(); - for j in 0..k { - sk.set_entry(j, j, ¶ms.base).unwrap(); - } - for i in 0..k - 1 { - sk.set_entry(i + 1, i, Z::MINUS_ONE).unwrap(); - } - sk = if params.base.pow(k).unwrap() == params.q { - // compute s in the special case where the modulus is a power of base - // i.e. the last column can remain as it is - sk - } else { - // compute s for any arbitrary modulus - // represent modulus in `base` and set last row accordingly - let mut q = Z::from(¶ms.q); - for i in 0..k { - let q_i = &q % ¶ms.base; - sk.set_entry(i, k - 1, &q_i).unwrap(); - q = (q - q_i).div_exact(¶ms.base).unwrap(); - } - sk - }; - let mut out = MatZ::new(n * k, n * k); - for j in 0..n { - out.set_submatrix( - j * sk.get_num_rows(), - j * sk.get_num_columns(), - &sk, - 0, - 0, - -1, - -1, - ) - .unwrap(); - } - out -} - /// Computes `W` with `GW = -H^{-1}A [ I | 0 ] mod q` fn compute_w(params: &GadgetParameters, tag: &MatZq, a: &MatZq) -> MatZ { let tag_inv = tag.inverse().unwrap(); @@ -388,99 +349,6 @@ mod test_gen_sa { } } -#[cfg(test)] -mod test_compute_s { - use crate::sample::g_trapdoor::{ - gadget_parameters::GadgetParameters, short_basis_classical::compute_s, - }; - use qfall_math::integer::{MatZ, Z}; - use std::str::FromStr; - - /// Ensure that the matrix s is computed correctly for a power-of-two modulus. - #[test] - fn base_2_power_two() { - let params = GadgetParameters::init_default(2, 16); - - let s = compute_s(¶ms); - - let s_cmp = MatZ::from_str( - "[[2, 0, 0, 0, 0, 0, 0, 0],\ - [-1, 2, 0, 0, 0, 0, 0, 0],\ - [0, -1, 2, 0, 0, 0, 0, 0],\ - [0, 0, -1, 2, 0, 0, 0, 0],\ - [0, 0, 0, 0, 2, 0, 0, 0],\ - [0, 0, 0, 0, -1, 2, 0, 0],\ - [0, 0, 0, 0, 0, -1, 2, 0],\ - [0, 0, 0, 0, 0, 0, -1, 2]]", - ) - .unwrap(); - assert_eq!(s_cmp, s) - } - - /// Ensure that the matrix s is computed correctly for a an arbitrary modulus. - #[test] - fn base_2_arbitrary() { - let q = Z::from(0b1100110); - let params = GadgetParameters::init_default(1, q); - - let s = compute_s(¶ms); - - let s_cmp = MatZ::from_str( - "[[2, 0, 0, 0, 0, 0, 0],\ - [-1, 2, 0, 0, 0, 0, 1],\ - [0, -1, 2, 0, 0, 0, 1],\ - [0, 0, -1, 2, 0, 0, 0],\ - [0, 0, 0, -1, 2, 0, 0],\ - [0, 0, 0, 0, -1, 2, 1],\ - [0, 0, 0, 0, 0, -1, 1]]", - ) - .unwrap(); - - assert_eq!(s_cmp, s) - } - - /// Ensure that the matrix s is computed correctly for a power-of-5 modulus. - #[test] - fn base_5_power_5() { - let mut params = GadgetParameters::init_default(1, 625); - params.k = Z::from(4); - params.base = Z::from(5); - - let s = compute_s(¶ms); - - let s_cmp = MatZ::from_str( - "[[5, 0, 0, 0],\ - [-1, 5, 0, 0],\ - [0, -1, 5, 0],\ - [0, 0, -1, 5]]", - ) - .unwrap(); - assert_eq!(s_cmp, s) - } - - /// Ensure that the matrix s is computed correctly for an arbitrary modulus with - /// base 5. - #[test] - fn base_5_arbitrary() { - let q = Z::from_str_b("4123", 5).unwrap(); - let mut params = GadgetParameters::init_default(1, q); - params.k = Z::from(4); - params.base = Z::from(5); - - let s = compute_s(¶ms); - - let s_cmp = MatZ::from_str( - "[[5, 0, 0, 3],\ - [-1, 5, 0, 2],\ - [0, -1, 5, 1],\ - [0, 0, -1, 4]]", - ) - .unwrap(); - - assert_eq!(s_cmp, s) - } -} - #[cfg(test)] mod test_compute_w { use super::compute_w;