sunscreen_math/
security.rs

1use log::warn;
2use statrs::distribution::{ContinuousCDF, Normal};
3
4use crate::geometry::{ConvexPolytope2D, HalfSpace2D, Point2D};
5
6/// Error for when a value is outside the constraints of a polytope.
7#[derive(Debug)]
8pub struct OutsideConstraintsError {
9    /// The name of the dimensions that were outside the constraints.
10    dimensions: [String; 2],
11
12    /// The value that was outside the constraints.
13    value: (f64, f64),
14
15    /// The polytope it was supposed to be in.
16    polytope: ConvexPolytope2D,
17}
18
19impl OutsideConstraintsError {
20    /// The name of the dimensions that were outside the constraints.
21    pub fn dimensions(&self) -> &[String; 2] {
22        &self.dimensions
23    }
24
25    /// The value that was outside the constraints.
26    pub fn value(&self) -> (f64, f64) {
27        self.value
28    }
29
30    /// The polytope it was supposed to be in.
31    pub fn polytope(&self) -> &ConvexPolytope2D {
32        &self.polytope
33    }
34}
35
36impl std::fmt::Display for OutsideConstraintsError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        write!(
39            f,
40            "Value {:?} is outside the constraints of polytope {:?}",
41            self.value, self.polytope
42        )
43    }
44}
45
46/// Result type for [`lwe_security_level_to_std`].
47pub type StandardDeviationResult = Result<f64, OutsideConstraintsError>;
48
49/// Result type for [`lwe_std_to_security_level`].
50pub type SecurityLevelResult = Result<f64, OutsideConstraintsError>;
51
52/// Evaluate a polynomial with coefficients in increasing order of degree.
53fn evaluate_polynomial(coeffs: &[f64], x: f64) -> f64 {
54    let mut result = 0.0;
55
56    for (i, c) in coeffs.iter().enumerate() {
57        result += c * x.powi(i as i32);
58    }
59
60    result
61}
62
63/// Evaluate a 2D polynomial with coefficients in increasing order of degree
64/// along both dimensions.
65pub fn evaluate_polynomial_2d<const M: usize, const N: usize>(
66    coeffs: &[[f64; N]; M],
67    x: f64,
68    y: f64,
69) -> f64 {
70    let mut result = 0.0;
71
72    // Clippy comaplins but this is the simplest way to read this loop.
73    #[allow(clippy::needless_range_loop)]
74    for i in 0..M {
75        for j in 0..N {
76            result += coeffs[i][j] * x.powi(i as i32) * y.powi(j as i32);
77        }
78    }
79
80    result
81}
82
83// Exact probability of being farther than x away from the mean given a standard
84// deviation that works when the ratio of x to the standard deviation is below
85// 7.
86fn probability_away_from_mean_gaussian_low(x: f64, std: f64) -> f64 {
87    let normal = Normal::new(0.0, 1.0).unwrap();
88
89    let single_tail_area = 1.0 - normal.cdf(x / std);
90    let both_tail_area = 2.0 * single_tail_area;
91
92    both_tail_area.log10()
93}
94
95// Very low error approximation when the ratio of how far x is from the mean
96// given the standard deviation is above 7. Works up to a ratio of 30
97// (probability of 1e-197), afterwards it may diverge from the real value.
98fn probability_away_from_mean_gaussian_high(x: f64, std: f64) -> f64 {
99    let ratio = x / std;
100
101    if ratio > 30.0 {
102        warn!("Ratio too high for approximation, not validated for this region");
103    }
104
105    // Quintic interpolation works nicely (a maximum of 0.00145% error). Listed
106    // with highest order first.
107    let coeffs = [
108        -0.31904236601958913,
109        -0.13390834324063405,
110        -0.20902566462352498,
111        -0.0003178660849038345,
112        6.75504783552659e-06,
113        -5.91907446763691e-08,
114    ];
115
116    evaluate_polynomial(&coeffs, ratio)
117}
118
119/// Returns the log10 of the probability of being farther than x away from the
120/// mean given a standard deviation. We return the log to handle very low
121/// probabilities.
122///
123/// # Arguments
124///
125/// * `x` - The distance from the mean.
126/// * `std` - The standard deviation.
127///
128/// # Returns
129/// The log10 of the probability of being x away from the mean given a standard
130/// deviation.
131///
132/// # Examples
133/// ```
134/// use sunscreen_math::security::probability_away_from_mean_gaussian;
135///
136/// // Probability of being 1 standard deviation away from the mean. Should be
137/// // approximately 32%. If you know z-scores then this should be familiar.
138/// let log_prob = probability_away_from_mean_gaussian(1.0, 1.0);
139/// let prob = 10.0f64.powf(log_prob);
140/// let rounded_prob = (prob * 10000.0).round() / 10000.0;
141/// assert_eq!(rounded_prob, 0.3173);
142/// ```
143pub fn probability_away_from_mean_gaussian(x: f64, std: f64) -> f64 {
144    if x / std < 7.0 {
145        probability_away_from_mean_gaussian_low(x, std)
146    } else {
147        probability_away_from_mean_gaussian_high(x, std)
148    }
149}
150
151/// Returns the LWE standard deviation for a given dimension and security level,
152/// normalized to the ciphertext modulus (calculated with 2^64 as the modulus).
153/// Valid from 368 to 2048 dimensions and 78 to 130 bits of security. Assumes
154/// that the private key is binary.
155///
156/// There are constraints on the input space above 1472 dimensions, where the
157/// security level at the smallest amount of noise possible is higher than 78
158/// bits.
159///
160/// This approximation has an error of 0.021% +- 0.014%, max error 0.11%.
161///
162/// Simulation data used for fit from
163/// lattice-estimator commit 25f9e88 (Nov 8th 2023).
164/// <https://github.com/malb/lattice-estimator>
165pub fn lwe_security_level_to_std(dimension: usize, security_level: f64) -> StandardDeviationResult {
166    let security_polytope = ConvexPolytope2D {
167        half_spaces: vec![
168            HalfSpace2D::new((-1.0, 0.0), -368.0),
169            HalfSpace2D::new((1.0, 0.0), 2048.0),
170            HalfSpace2D::new((0.0, -1.0), -78.0),
171            HalfSpace2D::new((0.0, 1.0), 130.0),
172            // Above 1472 dimensions the security level at the smallest amount of
173            // noise possible is higher than 78 bits.
174            HalfSpace2D::new((0.05678074392712544, -1.0), 3.5151045883938177),
175        ],
176    };
177
178    if !security_polytope.inside(Point2D::new(dimension as f64, security_level)) {
179        return Err(OutsideConstraintsError {
180            dimensions: ["dimension".to_string(), "security_level".to_string()],
181            value: (dimension as f64, security_level),
182            polytope: security_polytope,
183        });
184    }
185
186    let coeffs = [
187        [
188            2.89630547e+00,
189            -1.26321873e-01,
190            2.13993467e-03,
191            -1.49515549e-05,
192            3.84468453e-08,
193        ],
194        [
195            -5.60568533e-02,
196            1.33311189e-03,
197            -1.56200244e-05,
198            8.93067686e-08,
199            -2.00996854e-10,
200        ],
201        [
202            7.39088707e-07,
203            -9.61269520e-08,
204            2.15766569e-09,
205            -1.82462028e-11,
206            5.45243818e-14,
207        ],
208        [
209            1.49456164e-09,
210            -4.28264022e-11,
211            4.30538855e-13,
212            -1.50621118e-15,
213            0.00000000e+00,
214        ],
215        [
216            9.49334890e-14,
217            -2.17539853e-15,
218            1.22195316e-17,
219            0.00000000e+00,
220            0.00000000e+00,
221        ],
222    ];
223
224    let log_std = evaluate_polynomial_2d(&coeffs, dimension as f64, security_level);
225
226    Ok(10.0f64.powf(log_std))
227}
228
229/// Returns the LWE security level for a given dimension and standard deviation,
230/// normalized to the ciphertext modulus (calculated with 2^64 as the modulus).
231/// Valid from 368 to 2048 dimensions and 78 to 130 bits of security. Assumes
232/// that the private key is binary.
233///
234/// The valid standard deviations are functions of the dimension, and hence not
235/// all standard deviations are valid for all dimensions. If a standard
236/// deviation is not valid for a given dimension, an error is returned defining
237/// the valid region of standard deviations.
238///
239/// This approximation has an error of 0.019% +- 0.014%, max error 0.11%.
240///
241/// Simulation data used for fit from
242/// lattice-estimator commit 25f9e88 (Nov 8th 2023).
243/// <https://github.com/malb/lattice-estimator>
244pub fn lwe_std_to_security_level(dimension: usize, std: f64) -> SecurityLevelResult {
245    let log_std = std.log10();
246
247    let std_polytope = ConvexPolytope2D {
248        half_spaces: vec![
249            HalfSpace2D::new((-1.0, 0.0), -386.0),
250            HalfSpace2D::new((1.0, 0.0), 2048.0),
251            // Half spaces to define the general region where the standard deviation is valid.
252            HalfSpace2D::new((-0.012501482876757172, -1.0), -0.5040411014606384),
253            HalfSpace2D::new((0.0077927720025765665, 1.0), 0.7390928205510939),
254            // Minimum bound on the standard deviation
255            HalfSpace2D::new((0.0, -1.0), 17.67),
256        ],
257    };
258
259    if !std_polytope.inside(Point2D::new(dimension as f64, log_std)) {
260        return Err(OutsideConstraintsError {
261            dimensions: ["dimension".to_string(), "log_std".to_string()],
262            value: (dimension as f64, log_std),
263            polytope: std_polytope,
264        });
265    }
266
267    let coeffs = [
268        [
269            6.90381015e+01,
270            5.02853460e+01,
271            1.94568148e+01,
272            4.20275108e+00,
273            5.70115313e-01,
274            3.84445029e-02,
275            1.01123781e-03,
276        ],
277        [
278            5.74446364e-01,
279            2.16090358e-01,
280            4.33027422e-02,
281            5.96469779e-03,
282            3.47705471e-05,
283            -3.75600129e-05,
284            -1.73396859e-06,
285        ],
286        [
287            1.38947894e-04,
288            -1.97798175e-06,
289            6.18022031e-06,
290            -8.44553282e-06,
291            -9.87061302e-07,
292            -1.98799589e-08,
293            7.73239565e-10,
294        ],
295        [
296            -1.76700147e-07,
297            4.46397961e-08,
298            -8.48859329e-08,
299            -6.50906497e-09,
300            2.29684491e-10,
301            2.23006735e-11,
302            0.00000000e+00,
303        ],
304        [
305            2.73798876e-10,
306            -4.27647020e-10,
307            -1.56129840e-12,
308            5.18444880e-12,
309            2.50320308e-13,
310            0.00000000e+00,
311            0.00000000e+00,
312        ],
313        [
314            -9.58735744e-13,
315            1.71390444e-13,
316            3.36603110e-14,
317            1.30767385e-15,
318            0.00000000e+00,
319            0.00000000e+00,
320            0.00000000e+00,
321        ],
322        [
323            5.98968287e-16,
324            7.74296283e-17,
325            2.66615159e-18,
326            0.00000000e+00,
327            0.00000000e+00,
328            0.00000000e+00,
329            0.00000000e+00,
330        ],
331    ];
332
333    Ok(evaluate_polynomial_2d(&coeffs, dimension as f64, log_std))
334}
335
336#[cfg(test)]
337mod tests {
338    use super::{lwe_security_level_to_std, lwe_std_to_security_level};
339
340    #[test]
341    fn lwe_security_to_std_and_back() {
342        let tolerance = 0.05;
343
344        for dimension in 368..=2048 {
345            for security_level in 80..=128 {
346                let std = if let Ok(value) =
347                    lwe_security_level_to_std(dimension, security_level as f64)
348                {
349                    value
350                } else {
351                    continue;
352                };
353
354                let recovered_security_level =
355                    if let Ok(value) = lwe_std_to_security_level(dimension, std) {
356                        value
357                    } else {
358                        continue;
359                    };
360
361                let diff = (recovered_security_level - security_level as f64).abs();
362                assert!(
363                    diff < tolerance,
364                    "Security level tolerance violated. Dimension: {}, std: {}, security_level: {}, recovered_level: {}",
365                    dimension,
366                    std,
367                    security_level,
368                    recovered_security_level
369                );
370            }
371        }
372    }
373}