qfall_tools/primitive/psf/
gpv_ring.rs

1// Copyright © 2023 Marvin Beckmann
2//
3// This file is part of qFALL-tools.
4//
5// qFALL-tools is free software: you can redistribute it and/or modify it under
6// the terms of the Mozilla Public License Version 2.0 as published by the
7// Mozilla Foundation. See <https://mozilla.org/en-US/MPL/2.0/>.
8
9//! Implements a GPV [`PSF`] over the polynomial ring according to
10//! [\[1\]](<../index.html#:~:text=[1]>) and [\[2\]](<../index.html#:~:text=[2]>)
11//! using G-trapdoors to generate a short basis trapdoors.
12
13use super::PSF;
14use crate::{
15    sample::g_trapdoor::{
16        gadget_parameters::GadgetParametersRing, gadget_ring::gen_trapdoor_ring_lwe,
17        short_basis_ring::gen_short_basis_for_trapdoor_ring,
18    },
19    utils::rotation_matrix::rot_minus_matrix,
20};
21use qfall_math::{
22    integer::{MatPolyOverZ, MatZ, PolyOverZ},
23    integer_mod_q::{MatPolynomialRingZq, MatZq},
24    rational::{MatQ, PolyOverQ, Q},
25    traits::{
26        FromCoefficientEmbedding, IntoCoefficientEmbedding, MatrixDimensions, MatrixGetSubmatrix,
27        Pow,
28    },
29};
30use serde::{Deserialize, Serialize};
31
32/// A lattice-based implementation of a [`PSF`] according to
33/// [\[1\]](<index.html#:~:text=[1]>) and [\[2\]](<index.html#:~:text=[2]>)
34/// using G-Trapdoors where D_n = {e ∈ R^m | |ι(e)| <= s sqrt(m*n) }
35/// and R_n = R_q.
36///
37/// Attributes
38/// - `gp`: Describes the gadget parameters with which the G-Trapdoor is generated
39/// - `s`: The Gaussian parameter with which elements from the domain are sampled
40/// - `s:td`: The Gaussian parameter with which the trapdoor is sampled
41///
42/// # Examples
43/// ```
44/// use qfall_tools::primitive::psf::PSFGPVRing;
45/// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParametersRing;
46/// use qfall_math::rational::Q;
47/// use qfall_tools::primitive::psf::PSF;
48///
49/// let psf = PSFGPVRing {
50///     gp: GadgetParametersRing::init_default(8, 512),
51///     s: Q::from(100),
52///     s_td: Q::from(1.005_f64),
53/// };
54///
55/// let (a, (r, e)) = psf.trap_gen();
56/// let domain_sample = psf.samp_d();
57/// let range_fa = psf.f_a(&a, &domain_sample);
58/// let preimage = psf.samp_p(&a, &(r,e), &range_fa);
59///
60/// assert!(psf.check_domain(&preimage));
61/// ```
62#[derive(Serialize, Deserialize)]
63pub struct PSFGPVRing {
64    pub gp: GadgetParametersRing,
65    pub s: Q,
66    pub s_td: Q,
67}
68
69impl PSF for PSFGPVRing {
70    type A = MatPolynomialRingZq;
71    type Trapdoor = (MatPolyOverZ, MatPolyOverZ);
72    type Domain = MatPolyOverZ;
73    type Range = MatPolynomialRingZq;
74
75    /// Computes a G-Trapdoor according to the [`GadgetParametersRing`].
76    ///
77    /// # Examples
78    /// ```
79    /// use qfall_tools::primitive::psf::PSFGPVRing;
80    /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParametersRing;
81    /// use qfall_math::rational::Q;
82    /// use qfall_tools::primitive::psf::PSF;
83    ///
84    /// let psf = PSFGPVRing {
85    ///     gp: GadgetParametersRing::init_default(8, 512),
86    ///     s: Q::from(100),
87    ///     s_td: Q::from(1.005_f64),
88    /// };
89    /// let (a, (r, e)) = psf.trap_gen();
90    /// ```
91    fn trap_gen(&self) -> (MatPolynomialRingZq, (MatPolyOverZ, MatPolyOverZ)) {
92        let a_bar =
93            PolyOverZ::sample_uniform(self.gp.modulus.get_degree() - 1, 0, self.gp.modulus.get_q())
94                .unwrap();
95        let (a, r, e) = gen_trapdoor_ring_lwe(&self.gp, &a_bar, &self.s_td).unwrap();
96
97        (a, (r, e))
98    }
99
100    /// Samples in the domain using SampleD with the standard basis and center `0`.
101    ///
102    /// # Examples
103    /// ```
104    /// use qfall_tools::primitive::psf::PSFGPVRing;
105    /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParametersRing;
106    /// use qfall_math::rational::Q;
107    /// use qfall_tools::primitive::psf::PSF;
108    ///
109    /// let psf = PSFGPVRing {
110    ///     gp: GadgetParametersRing::init_default(8, 512),
111    ///     s: Q::from(100),
112    ///     s_td: Q::from(1.005_f64),
113    /// };
114    /// let (a, (r, e)) = psf.trap_gen();
115    ///
116    /// let domain_sample = psf.samp_d();
117    /// ```
118    fn samp_d(&self) -> MatPolyOverZ {
119        let dimension = self.gp.modulus.get_degree() * (&self.gp.k + 2);
120        let sample = MatZ::sample_discrete_gauss(dimension, 1, 0, &self.s).unwrap();
121        MatPolyOverZ::from_coefficient_embedding((&sample, self.gp.modulus.get_degree() - 1))
122    }
123
124    /// Samples an `e` in the domain using SampleD with a short basis that is generated
125    /// from the G-Trapdoor from the conditioned discrete Gaussian with
126    /// `f_a(a,e) = u` for a provided syndrome `u`.
127    ///
128    /// *Note*: the provided parameters `a, r, e, u` must fit together,
129    /// otherwise unexpected behavior such as panics may occur.
130    ///
131    /// Parameters:
132    /// - `a`: The parity-check matrix
133    /// - `r`: Together with `e` builds a G-Trapdoor for `a`
134    /// - `e`: Together with `r` builds a G-Trapdoor for `a`
135    /// - `u`: The syndrome from the range
136    ///
137    /// Returns a sample `e` from the domain on the conditioned discrete
138    /// Gaussian distribution `f_a(a,e) = u`.
139    ///
140    /// # Examples
141    /// ```
142    /// use qfall_tools::primitive::psf::PSFGPVRing;
143    /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParametersRing;
144    /// use qfall_math::rational::Q;
145    /// use qfall_tools::primitive::psf::PSF;
146    ///
147    /// let psf = PSFGPVRing {
148    ///     gp: GadgetParametersRing::init_default(8, 512),
149    ///     s: Q::from(100),
150    ///     s_td: Q::from(1.005_f64),
151    /// };
152    /// let (a, (r, e)) = psf.trap_gen();
153    ///
154    /// let domain_sample = psf.samp_d();
155    /// let range_fa = psf.f_a(&a, &domain_sample);
156    ///
157    /// let preimage = psf.samp_p(&a, &(r,e), &range_fa);
158    /// assert_eq!(range_fa, psf.f_a(&a, &preimage))
159    /// ```
160    fn samp_p(
161        &self,
162        a: &MatPolynomialRingZq,
163        (r, e): &(MatPolyOverZ, MatPolyOverZ),
164        u: &MatPolynomialRingZq,
165    ) -> MatPolyOverZ {
166        // compute solution to `a*x = u`
167        // the same as `Rot^-(ι(a)) ι(x) = ι(u)`
168
169        let short_basis = gen_short_basis_for_trapdoor_ring(&self.gp, a, r, e);
170
171        // solve `rot^-(ι(a)) ι(x) = ι(u)` to get solution
172        let u_embedded = u
173            .get_representative_least_nonnegative_residue()
174            .into_coefficient_embedding(self.gp.modulus.get_degree());
175        let a_embedded = a
176            .get_representative_least_nonnegative_residue()
177            .into_coefficient_embedding(self.gp.modulus.get_degree());
178        let rot_a = rot_minus_matrix(&a_embedded);
179
180        let u_embedded = MatZq::from((&u_embedded, &self.gp.modulus.get_q()));
181        let rot_a = MatZq::from((&rot_a, &self.gp.modulus.get_q()));
182        let sol: MatZ = rot_a
183            .solve_gaussian_elimination(&u_embedded)
184            .unwrap()
185            .get_representative_least_nonnegative_residue();
186
187        // turn center into a vector of polynomials over Q with maximal degree as the
188        // modulus
189        let center = MatQ::from(&(-1 * &sol));
190        let mut center_embedded = Vec::new();
191        for block in 0..(center.get_num_rows() / (self.gp.modulus.get_degree())) {
192            let sub_mat = center
193                .get_submatrix(
194                    block * self.gp.modulus.get_degree(),
195                    (block + 1) * self.gp.modulus.get_degree() - 1,
196                    0,
197                    0,
198                )
199                .unwrap();
200            let embedded_sub_mat = PolyOverQ::from_coefficient_embedding(&sub_mat);
201            center_embedded.push(embedded_sub_mat);
202        }
203
204        MatPolyOverZ::from_coefficient_embedding((&sol, self.gp.modulus.get_degree() - 1))
205            + MatPolyOverZ::sample_d(
206                &short_basis,
207                self.gp.modulus.get_degree(),
208                &center_embedded,
209                &self.s,
210            )
211            .unwrap()
212    }
213
214    /// Implements the efficiently computable function `f_a` which here corresponds to
215    /// `a*sigma`.
216    ///
217    /// Parameters:
218    /// - `a`: The parity-check matrix of dimensions `n x m`
219    /// - `sigma`: A column vector of length `m`
220    ///
221    /// Returns `a*sigma`
222    ///
223    /// # Examples
224    /// ```
225    /// use qfall_tools::primitive::psf::PSFGPVRing;
226    /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParametersRing;
227    /// use qfall_math::rational::Q;
228    /// use qfall_tools::primitive::psf::PSF;
229    ///
230    /// let psf = PSFGPVRing {
231    ///     gp: GadgetParametersRing::init_default(8, 512),
232    ///     s: Q::from(100),
233    ///     s_td: Q::from(1.005_f64),
234    /// };
235    /// let (a, (r, e)) = psf.trap_gen();
236    ///
237    /// let domain_sample = psf.samp_d();
238    /// let range_fa = psf.f_a(&a, &domain_sample);
239    /// ```
240    ///
241    /// # Panics ...
242    /// - if `sigma` is not in the domain.
243    fn f_a(&self, a: &MatPolynomialRingZq, sigma: &MatPolyOverZ) -> MatPolynomialRingZq {
244        assert!(self.check_domain(sigma));
245        let sigma = MatPolynomialRingZq::from((sigma, &a.get_mod()));
246        a * sigma
247    }
248
249    /// Checks whether a value `sigma` is in D_n = {e ∈ R^m | |ι(e)| <= s sqrt(m*n) }.
250    ///
251    /// Parameters:
252    /// - `sigma`: The value for which is checked, if it is in the domain
253    ///
254    /// Returns true, if `sigma` is in D_n.
255    ///
256    /// # Examples
257    /// ```
258    /// use qfall_tools::primitive::psf::PSFGPVRing;
259    /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParametersRing;
260    /// use qfall_math::rational::Q;
261    /// use qfall_tools::primitive::psf::PSF;
262    ///
263    /// let psf = PSFGPVRing {
264    ///     gp: GadgetParametersRing::init_default(8, 512),
265    ///     s: Q::from(100),
266    ///     s_td: Q::from(1.005_f64),
267    /// };
268    /// let (a, (r, e)) = psf.trap_gen();
269    ///
270    /// let vector = psf.samp_d();
271    ///
272    /// assert!(psf.check_domain(&vector));
273    /// ```
274    fn check_domain(&self, sigma: &MatPolyOverZ) -> bool {
275        let m = &self.gp.k + 2;
276        let nr_coeffs = self.gp.modulus.get_degree();
277        let sigma_embedded = sigma.into_coefficient_embedding(nr_coeffs);
278
279        sigma.is_column_vector()
280            && m == sigma.get_num_rows()
281            && sigma_embedded.norm_eucl_sqrd().unwrap()
282                <= self.s.pow(2).unwrap() * sigma_embedded.get_num_rows()
283    }
284}
285
286#[cfg(test)]
287mod test_gpv_psf {
288    use super::super::gpv_ring::PSFGPVRing;
289    use super::PSF;
290    use crate::sample::g_trapdoor::gadget_parameters::GadgetParametersRing;
291    use qfall_math::integer::{MatPolyOverZ, PolyOverZ};
292    use qfall_math::integer_mod_q::MatPolynomialRingZq;
293    use qfall_math::rational::Q;
294    use qfall_math::traits::*;
295
296    fn compute_s(n: i64) -> Q {
297        ((2 * 2 * Q::from(1.005_f64) * Q::from(n).sqrt() + 1) * 2) * 4
298    }
299
300    /// Ensures that `samp_d` actually computes values that are in D_n.
301    #[test]
302    fn samp_d_samples_from_dn() {
303        let (n, q) = (5, 123456789);
304        let psf = PSFGPVRing {
305            gp: GadgetParametersRing::init_default(n, q),
306            s: Q::from(1000),
307            s_td: Q::from(1.005_f64),
308        };
309
310        for _ in 0..5 {
311            assert!(psf.check_domain(&psf.samp_d()));
312        }
313    }
314
315    /// Ensures that `samp_p` actually computes preimages that are also in the correct
316    /// domain.
317    #[test]
318    fn samp_p_preimage_and_domain() {
319        for (n, q) in [(5, i32::MAX - 57), (6, i32::MAX)] {
320            let psf = PSFGPVRing {
321                gp: GadgetParametersRing::init_default(n, q),
322                s: compute_s(n),
323                s_td: Q::from(1.005_f64),
324            };
325            let (a, r) = psf.trap_gen();
326            let domain_sample = psf.samp_d();
327            let range_fa = psf.f_a(&a, &domain_sample);
328
329            let preimage = psf.samp_p(&a, &r, &range_fa);
330
331            assert_eq!(range_fa, psf.f_a(&a, &preimage));
332            assert!(psf.check_domain(&preimage));
333        }
334    }
335
336    /// Ensures that `f_a` returns `a*sigma`.
337    #[test]
338    fn f_a_works_as_expected() {
339        for (n, q) in [(5, 256), (6, 128)] {
340            let psf = PSFGPVRing {
341                gp: GadgetParametersRing::init_default(n, q),
342                s: compute_s(n),
343                s_td: Q::from(1.005_f64),
344            };
345            let (a, _) = psf.trap_gen();
346            let domain_sample = psf.samp_d();
347
348            let domain_sample_2 = MatPolynomialRingZq::from((&domain_sample, &a.get_mod()));
349            assert_eq!(&a * &domain_sample_2, psf.f_a(&a, &domain_sample));
350        }
351    }
352
353    /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
354    /// Sigma is not a vector.
355    #[test]
356    #[should_panic]
357    fn f_a_sigma_not_in_domain_matrix() {
358        let psf = PSFGPVRing {
359            gp: GadgetParametersRing::init_default(8, 1024),
360            s: compute_s(8),
361            s_td: Q::from(1.005_f64),
362        };
363        let (a, _) = psf.trap_gen();
364        let not_in_domain = MatPolyOverZ::new(a.get_num_columns(), 2);
365
366        let _ = psf.f_a(&a, &not_in_domain);
367    }
368
369    /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
370    /// Sigma is not of the correct length.
371    #[test]
372    #[should_panic]
373    fn f_a_sigma_not_in_domain_incorrect_length() {
374        let psf = PSFGPVRing {
375            gp: GadgetParametersRing::init_default(8, 1024),
376            s: compute_s(8),
377            s_td: Q::from(1.005_f64),
378        };
379        let (a, _) = psf.trap_gen();
380        let not_in_domain = MatPolyOverZ::new(a.get_num_columns() - 1, 1);
381
382        let _ = psf.f_a(&a, &not_in_domain);
383    }
384
385    /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
386    /// Sigma is too long.
387    #[test]
388    #[should_panic]
389    fn f_a_sigma_not_in_domain_too_long() {
390        let psf = PSFGPVRing {
391            gp: GadgetParametersRing::init_default(8, 1024),
392            s: compute_s(8),
393            s_td: Q::from(1.005_f64),
394        };
395        let (a, _) = psf.trap_gen();
396        let not_in_domain = psf.s.round()
397            * a.get_num_columns()
398            * 8
399            * MatPolyOverZ::identity(a.get_num_columns(), 1);
400
401        let _ = psf.f_a(&a, &not_in_domain);
402    }
403
404    /// Ensures that `check_domain` works for vectors with the correct length.
405    #[test]
406    fn check_domain_as_expected() {
407        let psf = PSFGPVRing {
408            gp: GadgetParametersRing::init_default(9, 1024),
409            s: compute_s(9),
410            s_td: Q::from(1.005_f64),
411        };
412        let (a, _) = psf.trap_gen();
413        let value = PolyOverZ::from(psf.s.round() * 3);
414        let mut in_domain = MatPolyOverZ::new(a.get_num_columns(), 1);
415        for i in 0..in_domain.get_num_rows() {
416            in_domain.set_entry(i, 0, &value).unwrap();
417        }
418
419        assert!(psf.check_domain(&MatPolyOverZ::new(a.get_num_columns(), 1)));
420        assert!(psf.check_domain(&in_domain));
421    }
422
423    /// Ensures that `check_domain` returns false for values that are not in the domain.
424    #[test]
425    fn check_domain_not_in_dn() {
426        let psf = PSFGPVRing {
427            gp: GadgetParametersRing::init_default(8, 1024),
428            s: compute_s(8),
429            s_td: Q::from(1.005_f64),
430        };
431        let (a, _) = psf.trap_gen();
432
433        let matrix = MatPolyOverZ::new(a.get_num_columns(), 2);
434        let too_short = MatPolyOverZ::new(a.get_num_columns() - 1, 1);
435        let too_long = MatPolyOverZ::new(a.get_num_columns() + 1, 1);
436        let entry_too_large = psf.s.round()
437            * a.get_num_columns()
438            * 8
439            * MatPolyOverZ::identity(a.get_num_columns(), 1);
440
441        assert!(!psf.check_domain(&matrix));
442        assert!(!psf.check_domain(&too_long));
443        assert!(!psf.check_domain(&too_short));
444        assert!(!psf.check_domain(&entry_too_large));
445    }
446}