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 ¢er_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, ¬_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, ¬_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, ¬_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}