qfall_tools/primitive/psf/mp_perturbation.rs
1// Copyright © 2025 Niklas Siemer
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 [`PSF`] based on perturbation sampling desribed in [\[1\]](<../index.html#:~:text=[1]>)
10//! using G-trapdoors.
11
12use super::PSF;
13use crate::sample::g_trapdoor::{
14 gadget_classical::short_basis_gadget,
15 gadget_classical::{find_solution_gadget_mat, gen_trapdoor},
16 gadget_parameters::GadgetParameters,
17};
18use qfall_math::{
19 integer::{MatZ, Z},
20 integer_mod_q::MatZq,
21 rational::{MatQ, Q},
22 traits::{Concatenate, MatrixDimensions, Pow},
23};
24use serde::{Deserialize, Serialize};
25
26/// A lattice-based implementation of a [`PSF`] according to
27/// [\[1\]](<index.html#:~:text=[1]>) using
28/// G-Trapdoors where D_n = {e ∈ Z^m | |e| <= s sqrt(m)}
29/// and R_n = Z_q^n.
30///
31/// Attributes
32/// - `gp`: Describes the gadget parameters with which the G-Trapdoor is generated
33/// - `r`: The rounding parameter
34/// - `s`: The Gaussian parameter with which is sampled
35///
36/// # Examples
37/// ```
38/// use qfall_tools::primitive::psf::PSFPerturbation;
39/// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
40/// use qfall_math::rational::Q;
41/// use qfall_tools::primitive::psf::PSF;
42///
43/// let psf = PSFPerturbation {
44/// gp: GadgetParameters::init_default(8, 64),
45/// r: Q::from(3),
46/// s: Q::from(25),
47/// };
48///
49/// let (a, td) = psf.trap_gen();
50/// let domain_sample = psf.samp_d();
51/// let range_fa = psf.f_a(&a, &domain_sample);
52/// let preimage = psf.samp_p(&a, &td, &range_fa);
53///
54/// assert!(psf.check_domain(&preimage));
55/// assert_eq!(a * preimage, range_fa);
56/// ```
57#[derive(Serialize, Deserialize)]
58pub struct PSFPerturbation {
59 pub gp: GadgetParameters,
60 pub r: Q,
61 pub s: Q,
62}
63
64impl PSFPerturbation {
65 /// Computes √Σ_2 = √(r^2 * (b^2 + 1) * [R^t | I]^t * [R^t | I] - r^2 * I)
66 /// to perform non-spherical Gaussian sampling according to Algorithm 1 in
67 /// [\[3\]](<index.html#:~:text=[3]>).
68 /// This matrix is the second part of the secret key and needs to be precomputed
69 /// to execute [PSFPerturbation::samp_p].
70 /// [PSFPerturbation::trap_gen] outputs this matrix for `s^2 * I`, i.e. for discrete
71 /// Gaussian preimages centered around `0`. This function enables changing the
72 /// covariance matrix to any covariance matrix s.t. Σ_2 is positive definite.
73 ///
74 /// Parameters:
75 /// - `mat_r`: The trapdoor matrix `R`
76 /// - `mat_sigma`: The covariance matrix `Σ` to sample [`Self::samp_p`] with
77 ///
78 /// Returns a [`MatQ`] containing √Σ_2 = √(r^2 * (b^2 + 1) * [R^t | I]^t * [R^t | I] - r^2 * I)
79 /// if Σ_2 was positive definite.
80 ///
81 /// # Examples
82 /// ```
83 /// use qfall_tools::primitive::psf::PSFPerturbation;
84 /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
85 /// use qfall_math::rational::{Q, MatQ};
86 /// use qfall_tools::primitive::psf::PSF;
87 /// use qfall_math::traits::*;
88 ///
89 /// let psf = PSFPerturbation {
90 /// gp: GadgetParameters::init_default(8, 64),
91 /// r: Q::from(3),
92 /// s: Q::from(25),
93 /// };
94 /// let different_s: f64 = 35.0;
95 ///
96 /// let (a, td) = psf.trap_gen();
97 ///
98 /// let cov_mat = different_s.powi(2) * MatQ::identity(a.get_num_columns(), a.get_num_columns());
99 /// let mat_sqrt_sigma_2 = psf.compute_sqrt_sigma_2(&td.0, &cov_mat);
100 /// let new_td = (td.0, mat_sqrt_sigma_2, td.2);
101 ///
102 /// let domain_sample = psf.samp_d();
103 /// let range_fa = psf.f_a(&a, &domain_sample);
104 /// let preimage = psf.samp_p(&a, &new_td, &range_fa);
105 ///
106 /// assert!(psf.check_domain(&preimage));
107 /// ```
108 ///
109 /// # Panics ...
110 /// - if Σ_2 is not positive definite.
111 pub fn compute_sqrt_sigma_2(&self, mat_r: &MatZ, mat_sigma: &MatQ) -> MatQ {
112 // Normalization factor according to MP12, Section 2.3
113 let normalization_factor = 1.0 / (2.0 * Q::PI);
114
115 // full_td = [R^t | I]^t
116 let full_td = mat_r
117 .concat_vertical(&MatZ::identity(
118 mat_sigma.get_num_rows() - mat_r.get_num_rows(),
119 mat_r.get_num_columns(),
120 ))
121 .unwrap();
122
123 // Assemble Σ_p = Σ - [R^t|I]^t * Σ_G * [R^t|I] with √Σ_G ≥ η (Λ^⊥(G))
124 // and assumption Σ_G = (base^2 + 1) * I as ||S|| = √(b^2 + 1), Theorem 4.1, MP12
125 let mat_sigma_p: MatQ =
126 mat_sigma - (self.gp.base.pow(2).unwrap() + 1) * &full_td * full_td.transpose();
127
128 // r^2 * Σ_p <=> r * √Σ_p
129 // Compute Σ_2 according to the requirements of Algorithm 1 in [3]
130 // Assume Σ_1 = r^2 * B_1 * B_1^t with B_1 = I as basis for ZZ^n
131 // Then, Σ_2 = Σ - Σ_1, where Σ = r^2 * Σ_p
132 let sigma_2: MatQ = normalization_factor
133 * self.r.pow(2).unwrap()
134 * (&mat_sigma_p
135 - MatQ::identity(mat_sigma_p.get_num_rows(), mat_sigma_p.get_num_columns()));
136
137 // Compute √Σ_2
138 sigma_2.cholesky_decomposition_flint()
139 }
140}
141
142/// Samples a preimage with respect to the gadget matrix `G` of `vec_u`.
143///
144/// Parameters:
145/// - `psf`: The [`PSFPerturbation`] that sets the parameters for this function
146/// - `vec_u`: The target vector
147/// - `short_basis_gadget`: The short basis of the corresponding gadget-matrix - just required to speed up the algorithm
148/// - `short_basis_gadget_gso`: The GSO of the short basis of the gadget-matrix - just required to speed up the algorithm
149///
150/// Returns a discrete Gaussian sampled preimage of `u` under `G` with Gaussian parameter `r * √(b^2 + 1)`,
151/// where `b` is the base used for the G-trapdoor.
152///
153/// # Examples
154/// ```compile_fail
155/// use qfall_tools::primitive::psf::PSFPerturbation;
156/// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
157/// use qfall_tools::sample::g_trapdoor::gadget_classical::short_basis_gadget;
158/// use qfall_math::rational::Q;
159/// use qfall_tools::primitive::psf::PSF;
160///
161/// let psf = PSFPerturbation {
162/// gp: GadgetParameters::init_default(8, 64),
163/// r: Q::from(3),
164/// s: Q::from(25),
165/// };
166///
167/// let short_basis_g = short_basis_gadget(&psf.gp);
168/// let short_basis_g_gso = MatQ::from(&short_basis_g).gso();
169///
170/// let target = MatZq::sample_uniform(8, 1, 64);
171/// let preimage = randomized_nearest_plane_gadget(&osf, &target, &short_basis_g, &short_basis_g_gso);
172/// ```
173pub(crate) fn randomized_nearest_plane_gadget(
174 psf: &PSFPerturbation,
175 vec_u: &MatZq,
176 short_basis_gadget: &MatZ,
177 short_basis_gadget_gso: &MatQ,
178) -> MatZ {
179 // s = r * √(b^2 + 1) according to Algorithm 3 in [1]
180 let s = &psf.r * (psf.gp.base.pow(2).unwrap() + Z::ONE).sqrt();
181
182 // find solution s.t. G * long_solution = vec_u
183 let long_solution = find_solution_gadget_mat(vec_u, &psf.gp.k, &psf.gp.base);
184
185 let center = MatQ::from(&(-1 * &long_solution));
186
187 // just as PSFGPV::samp_p
188 long_solution
189 + MatZ::sample_d_precomputed_gso(short_basis_gadget, short_basis_gadget_gso, ¢er, &s)
190 .unwrap()
191}
192
193impl PSF for PSFPerturbation {
194 type A = MatZq;
195 type Trapdoor = (MatZ, MatQ, (MatZ, MatQ));
196 type Domain = MatZ;
197 type Range = MatZq;
198
199 /// Computes a G-Trapdoor according to the [`GadgetParameters`] defined in the struct.
200 /// It returns a matrix `A` together with the short trapdoor matrix `R` and a precomputed √Σ_2
201 /// for covariance matrix `s^2 * I`, i.e. for preimage sampling centered around `0`.
202 /// The last part of the trapdoor tuple contains a short basis for the gadget matrix `G` and its
203 /// GSO, which removes the need to recompute the GSO for each iteration and speeds up preimage sampling
204 /// drastically.
205 ///
206 /// # Examples
207 /// ```
208 /// use qfall_tools::primitive::psf::PSFPerturbation;
209 /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
210 /// use qfall_math::rational::Q;
211 /// use qfall_tools::primitive::psf::PSF;
212 ///
213 /// let psf = PSFPerturbation {
214 /// gp: GadgetParameters::init_default(8, 64),
215 /// r: Q::from(3),
216 /// s: Q::from(25),
217 /// };
218 ///
219 /// let (mat_a, (mat_r, mat_sqrt_sigma_2, (sh_b_g, sh_b_g_gso))) = psf.trap_gen();
220 /// ```
221 fn trap_gen(&self) -> (MatZq, (MatZ, MatQ, (MatZ, MatQ))) {
222 let mat_a_bar = MatZq::sample_uniform(&self.gp.n, &self.gp.m_bar, &self.gp.q);
223 let tag = MatZq::identity(&self.gp.n, &self.gp.n, &self.gp.q);
224
225 let (mat_a, mat_r) = gen_trapdoor(&self.gp, &mat_a_bar, &tag).unwrap();
226
227 let mat_sqrt_sigma_2 = self.compute_sqrt_sigma_2(
228 &mat_r,
229 &(&self.s.pow(2).unwrap()
230 * MatQ::identity(mat_a.get_num_columns(), mat_a.get_num_columns())),
231 );
232
233 let short_basis_gadget = short_basis_gadget(&self.gp);
234 let short_basis_gadget_gso = MatQ::from(&short_basis_gadget).gso();
235
236 (
237 mat_a,
238 (
239 mat_r,
240 mat_sqrt_sigma_2,
241 (short_basis_gadget, short_basis_gadget_gso),
242 ),
243 )
244 }
245
246 /// Samples in the domain using SampleD with the standard basis and center `0`.
247 ///
248 /// # Examples
249 /// ```
250 /// use qfall_tools::primitive::psf::PSFPerturbation;
251 /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
252 /// use qfall_math::rational::Q;
253 /// use qfall_tools::primitive::psf::PSF;
254 ///
255 /// let psf = PSFPerturbation {
256 /// gp: GadgetParameters::init_default(8, 64),
257 /// r: Q::from(3),
258 /// s: Q::from(25),
259 /// };
260 /// let (mat_a, td) = psf.trap_gen();
261 ///
262 /// let domain_sample = psf.samp_d();
263 /// ```
264 fn samp_d(&self) -> MatZ {
265 let m = &self.gp.n * &self.gp.k + &self.gp.m_bar;
266 MatZ::sample_discrete_gauss(m, 1, 0, &self.s * &self.r).unwrap()
267 }
268
269 /// Samples an `e` in the domain using SampleD that is generated
270 /// from the G-Trapdoor from the conditioned
271 /// discrete Gaussian with `f_a(a,e) = u` for a provided syndrome `u`.
272 ///
273 /// *Note*: the provided parameters `mat_a, mat_r, vec_u` must fit together,
274 /// otherwise unexpected behavior such as panics may occur.
275 ///
276 /// Parameters:
277 /// - `mat_a`: The parity-check matrix
278 /// - `mat_r`: The short trapdoor matrix `R`
279 /// - `mat_sqrt_sigma_2`: The precomputed √Σ_2
280 /// - `vec_u`: The syndrome from the range
281 ///
282 /// Returns a sample `e` from the domain on the conditioned discrete
283 /// Gaussian distribution `f_a(a,e) = u` with covariance matrix Σ depending on `mat_sqrt_sigma_2`.
284 ///
285 /// # Examples
286 /// ```
287 /// use qfall_tools::primitive::psf::PSFPerturbation;
288 /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
289 /// use qfall_math::rational::Q;
290 /// use qfall_tools::primitive::psf::PSF;
291 ///
292 /// let psf = PSFPerturbation {
293 /// gp: GadgetParameters::init_default(8, 64),
294 /// r: Q::from(3),
295 /// s: Q::from(25),
296 /// };
297 /// let (mat_a, td) = psf.trap_gen();
298 /// let domain_sample = psf.samp_d();
299 /// let range_fa = psf.f_a(&mat_a, &domain_sample);
300 ///
301 /// let preimage = psf.samp_p(&mat_a, &td, &range_fa);
302 /// assert_eq!(range_fa, psf.f_a(&mat_a, &preimage))
303 /// ```
304 fn samp_p(
305 &self,
306 mat_a: &MatZq,
307 (mat_r, mat_sqrt_sigma_2, (short_basis_gadget, short_basis_gadget_gso)): &(
308 MatZ,
309 MatQ,
310 (MatZ, MatQ),
311 ),
312 vec_u: &MatZq,
313 ) -> MatZ {
314 // Sample perturbation p <- D_{ZZ^m, r * √Σ_2}
315 let vec_p = MatZ::sample_d_common_non_spherical(mat_sqrt_sigma_2, &self.r).unwrap();
316
317 // v = u - A * p
318 let vec_v = vec_u - mat_a * &vec_p;
319
320 // z <- D_{Λ_v^⊥(G), r * √Σ_G}
321 let vec_z = randomized_nearest_plane_gadget(
322 self,
323 &vec_v,
324 short_basis_gadget,
325 short_basis_gadget_gso,
326 );
327
328 let full_td = mat_r
329 .concat_vertical(&MatZ::identity(
330 mat_r.get_num_columns(),
331 mat_r.get_num_columns(),
332 ))
333 .unwrap();
334
335 vec_p + full_td * vec_z
336 }
337
338 /// Implements the efficiently computable function `f_a` which here corresponds to
339 /// `mat_a * sigma`. The sigma must be from the domain, i.e. D_n.
340 ///
341 /// Parameters:
342 /// - `mat_a`: The parity-check matrix of dimensions `n x m`
343 /// - `sigma`: A column vector of length `m`
344 ///
345 /// Returns `mat_a * sigma`.
346 ///
347 /// # Examples
348 /// ```
349 /// use qfall_tools::primitive::psf::PSFPerturbation;
350 /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
351 /// use qfall_math::rational::Q;
352 /// use qfall_tools::primitive::psf::PSF;
353 ///
354 /// let psf = PSFPerturbation {
355 /// gp: GadgetParameters::init_default(8, 64),
356 /// r: Q::from(3),
357 /// s: Q::from(25),
358 /// };
359 /// let (mat_a, td) = psf.trap_gen();
360 /// let domain_sample = psf.samp_d();
361 /// let range_fa = psf.f_a(&mat_a, &domain_sample);
362 /// ```
363 ///
364 /// # Panics ...
365 /// - if `sigma` is not in the domain.
366 fn f_a(&self, mat_a: &MatZq, sigma: &MatZ) -> MatZq {
367 assert!(self.check_domain(sigma));
368 mat_a * sigma
369 }
370
371 /// Checks whether a value `sigma` is in D_n = {e ∈ Z^m | |e| <= s * r * sqrt(m)}.
372 ///
373 /// Parameters:
374 /// - `sigma`: The value for which is checked, if it is in the domain
375 ///
376 /// Returns true, if `sigma` is in D_n.
377 ///
378 /// # Examples
379 /// ```
380 /// use qfall_tools::primitive::psf::PSF;
381 /// use qfall_tools::primitive::psf::PSFPerturbation;
382 /// use qfall_tools::sample::g_trapdoor::gadget_parameters::GadgetParameters;
383 /// use qfall_math::rational::Q;
384 ///
385 /// let psf = PSFPerturbation {
386 /// gp: GadgetParameters::init_default(8, 64),
387 /// r: Q::from(3),
388 /// s: Q::from(25),
389 /// };
390 /// let (mat_a, td) = psf.trap_gen();
391 ///
392 /// let vector = psf.samp_d();
393 ///
394 /// assert!(psf.check_domain(&vector));
395 /// ```
396 fn check_domain(&self, sigma: &MatZ) -> bool {
397 let m = &self.gp.n * &self.gp.k + &self.gp.m_bar;
398 sigma.is_column_vector()
399 && m == sigma.get_num_rows()
400 && sigma.norm_eucl_sqrd().unwrap()
401 <= self.s.pow(2).unwrap() * &m * &self.r.pow(2).unwrap()
402 }
403}
404
405#[cfg(test)]
406mod test_psf_perturbation {
407 use super::PSF;
408 use super::PSFPerturbation;
409 use crate::sample::g_trapdoor::gadget_parameters::GadgetParameters;
410 use qfall_math::integer::MatZ;
411 use qfall_math::rational::Q;
412 use qfall_math::traits::*;
413
414 /// Ensures that `samp_d` actually computes values that are in D_n.
415 #[test]
416 fn samp_d_samples_from_dn() {
417 for (n, q) in [(5, 256), (10, 128), (15, 157)] {
418 let psf = PSFPerturbation {
419 gp: GadgetParameters::init_default(n, q),
420 r: Q::from(n).log(2).unwrap(),
421 s: Q::from(25),
422 };
423
424 for _ in 0..5 {
425 assert!(psf.check_domain(&psf.samp_d()));
426 }
427 }
428 }
429
430 /// Ensures that `samp_p` actually computes preimages that are also in the correct
431 /// domain.
432 #[test]
433 fn samp_p_preimage_and_domain() {
434 for (n, q) in [(5, 256), (6, 128)] {
435 let psf = PSFPerturbation {
436 gp: GadgetParameters::init_default(n, q),
437 r: Q::from(n).log(2).unwrap(),
438 s: Q::from(25),
439 };
440 let (a, r) = psf.trap_gen();
441 let domain_sample = psf.samp_d();
442 let range_fa = psf.f_a(&a, &domain_sample);
443
444 let preimage = psf.samp_p(&a, &r, &range_fa);
445 assert_eq!(range_fa, psf.f_a(&a, &preimage));
446 assert!(psf.check_domain(&preimage));
447 }
448 }
449
450 /// Ensures that `f_a` returns `a * sigma`.
451 #[test]
452 fn f_a_works_as_expected() {
453 for (n, q) in [(5, 256), (6, 128)] {
454 let psf = PSFPerturbation {
455 gp: GadgetParameters::init_default(n, q),
456 r: Q::from(n).log(2).unwrap(),
457 s: Q::from(25),
458 };
459 let (a, _) = psf.trap_gen();
460 let domain_sample = psf.samp_d();
461
462 assert_eq!(&a * &domain_sample, psf.f_a(&a, &domain_sample));
463 }
464 }
465
466 /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
467 /// Sigma is not a vector.
468 #[test]
469 #[should_panic]
470 fn f_a_sigma_not_in_domain_matrix() {
471 let psf = PSFPerturbation {
472 gp: GadgetParameters::init_default(8, 128),
473 r: Q::from(8).log(2).unwrap(),
474 s: Q::from(25),
475 };
476 let (a, _) = psf.trap_gen();
477 let not_in_domain = MatZ::new(a.get_num_columns(), 2);
478
479 let _ = psf.f_a(&a, ¬_in_domain);
480 }
481
482 /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
483 /// Sigma is not of the correct length.
484 #[test]
485 #[should_panic]
486 fn f_a_sigma_not_in_domain_incorrect_length() {
487 let psf = PSFPerturbation {
488 gp: GadgetParameters::init_default(8, 128),
489 r: Q::from(8).log(2).unwrap(),
490 s: Q::from(25),
491 };
492 let (a, _) = psf.trap_gen();
493 let not_in_domain = MatZ::new(a.get_num_columns() - 1, 1);
494
495 let _ = psf.f_a(&a, ¬_in_domain);
496 }
497
498 /// Ensures that `f_a` panics if a value is provided, that is not within the domain.
499 /// Sigma is too long.
500 #[test]
501 #[should_panic]
502 fn f_a_sigma_not_in_domain_too_long() {
503 let psf = PSFPerturbation {
504 gp: GadgetParameters::init_default(8, 128),
505 r: Q::from(8).log(2).unwrap(),
506 s: Q::from(25),
507 };
508 let (a, _) = psf.trap_gen();
509 let not_in_domain =
510 psf.s.round() * a.get_num_columns() * MatZ::identity(a.get_num_columns(), 1);
511
512 let _ = psf.f_a(&a, ¬_in_domain);
513 }
514
515 /// Ensures that `check_domain` works for vectors with the correct length.
516 #[test]
517 fn check_domain_as_expected() {
518 let psf = PSFPerturbation {
519 gp: GadgetParameters::init_default(8, 128),
520 r: Q::from(8).log(2).unwrap(),
521 s: Q::from(25),
522 };
523 let (a, _) = psf.trap_gen();
524 let value = psf.s.round();
525 let mut in_domain = MatZ::new(a.get_num_columns(), 1);
526 for i in 0..in_domain.get_num_rows() {
527 in_domain.set_entry(i, 0, &value).unwrap();
528 }
529
530 assert!(psf.check_domain(&MatZ::new(a.get_num_columns(), 1)));
531 assert!(psf.check_domain(&in_domain));
532 }
533
534 /// Ensures that `check_domain` returns false for values that are not in the domain.
535 #[test]
536 fn check_domain_not_in_dn() {
537 let psf = PSFPerturbation {
538 gp: GadgetParameters::init_default(8, 128),
539 r: Q::from(8).log(2).unwrap(),
540 s: Q::from(25),
541 };
542 let (a, _) = psf.trap_gen();
543
544 let matrix = MatZ::new(a.get_num_columns(), 2);
545 let too_short = MatZ::new(a.get_num_columns() - 1, 1);
546 let too_long = MatZ::new(a.get_num_columns() + 1, 1);
547 let entry_too_large =
548 psf.s.round() * a.get_num_columns() * MatZ::identity(a.get_num_columns(), 1);
549
550 assert!(!psf.check_domain(&matrix));
551 assert!(!psf.check_domain(&too_long));
552 assert!(!psf.check_domain(&too_short));
553 assert!(!psf.check_domain(&entry_too_large));
554 }
555}