Skip to main content

rune_ring/
math.rs

1//! Polynomial arithmetic and sampling.
2
3use std::ops::Index;
4
5use rand_core::{CryptoRng, RngCore};
6use zeroize::Zeroize;
7
8use crate::error::RuneError;
9use crate::params::Params;
10
11/// Maximum polynomial degree supported by this implementation.
12pub(crate) const MAX_N: usize = 512;
13
14/// A polynomial in `R_q = Z_q[X]/(X^n + 1)`, stored as a centered coefficient
15/// vector for the active parameter set.
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[derive(Clone, PartialEq, Eq, Zeroize)]
18pub struct Poly(pub(crate) Vec<i64>);
19
20impl Poly {
21    /// Builds a polynomial from centered coefficients.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`RuneError::MalformedPublicKey`] if `coefficients` does not
26    /// match `params.n()` or contains a non-canonical coefficient.
27    pub fn from_coefficients(
28        coefficients: impl Into<Vec<i64>>,
29        params: &Params,
30    ) -> Result<Self, RuneError> {
31        let poly = Self(coefficients.into());
32        if poly_is_well_formed(&poly, params) {
33            Ok(poly)
34        } else {
35            Err(RuneError::MalformedPublicKey)
36        }
37    }
38
39    /// Returns the number of stored coefficients.
40    #[must_use]
41    pub fn len(&self) -> usize {
42        self.0.len()
43    }
44
45    /// Returns `true` if the polynomial has no coefficients.
46    #[must_use]
47    pub fn is_empty(&self) -> bool {
48        self.0.is_empty()
49    }
50
51    /// Borrows the centered coefficient vector.
52    #[must_use]
53    pub fn coefficients(&self) -> &[i64] {
54        &self.0
55    }
56
57    pub(crate) fn coefficients_mut(&mut self) -> &mut [i64] {
58        &mut self.0
59    }
60}
61
62impl std::fmt::Debug for Poly {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(
65            f,
66            "Poly(n={}, ||.||_inf={})",
67            self.0.len(),
68            self.0
69                .iter()
70                .map(|coeff| i64::saturating_abs(*coeff))
71                .max()
72                .unwrap_or(0)
73        )
74    }
75}
76
77impl Index<usize> for Poly {
78    type Output = i64;
79
80    fn index(&self, index: usize) -> &Self::Output {
81        &self.0[index]
82    }
83}
84
85/// Wrapper that zeroizes polynomial storage on drop.
86pub(crate) struct ZeroizingPoly(pub(crate) Poly);
87
88impl Drop for ZeroizingPoly {
89    fn drop(&mut self) {
90        // Keep secret-derived coefficients out of long-lived heap storage.
91        self.0 .0.zeroize();
92    }
93}
94
95pub(crate) fn zero_poly(params: &Params) -> Poly {
96    Poly(vec![0; params.n()])
97}
98
99pub(crate) fn poly_reduce_mod_q(x: i128, q: i64) -> i64 {
100    let modulus = i128::from(q);
101    let mut reduced = x % modulus;
102    if reduced < 0 {
103        reduced += modulus;
104    }
105
106    let half = modulus / 2;
107    if reduced > half {
108        reduced -= modulus;
109    }
110
111    i64::try_from(reduced).expect("centered residue fits in i64")
112}
113
114pub(crate) fn poly_is_well_formed(poly: &Poly, params: &Params) -> bool {
115    if poly.0.len() != params.n() {
116        return false;
117    }
118
119    let half_q = (params.q() - 1) / 2;
120    poly.0
121        .iter()
122        .all(|coeff| (-half_q..=half_q).contains(coeff))
123}
124
125pub(crate) fn poly_add(a: &Poly, b: &Poly, params: &Params) -> Poly {
126    let coeffs =
127        a.0.iter()
128            .zip(&b.0)
129            .map(|(lhs, rhs)| poly_reduce_mod_q(i128::from(*lhs) + i128::from(*rhs), params.q()))
130            .collect();
131    Poly(coeffs)
132}
133
134pub(crate) fn poly_sub(a: &Poly, b: &Poly, params: &Params) -> Poly {
135    let coeffs =
136        a.0.iter()
137            .zip(&b.0)
138            .map(|(lhs, rhs)| poly_reduce_mod_q(i128::from(*lhs) - i128::from(*rhs), params.q()))
139            .collect();
140    Poly(coeffs)
141}
142
143/// Multiplies two polynomials in `R_q = Z_q[X]/(X^n + 1)`.
144///
145/// # Performance
146///
147/// This is an O(n²) schoolbook implementation. For n=512 (`RUNE_256`),
148/// signing is slower than an NTT-based implementation would be.
149/// NTT support for q=8380417 is planned for a future release.
150pub(crate) fn poly_mul_schoolbook(a: &Poly, b: &Poly, params: &Params) -> Poly {
151    let n = params.n();
152    let mut acc = vec![0_i128; n];
153
154    for (i, a_coeff) in a.0.iter().enumerate() {
155        for (j, b_coeff) in b.0.iter().enumerate() {
156            let product = i128::from(*a_coeff) * i128::from(*b_coeff);
157            let degree = i + j;
158            if degree < n {
159                acc[degree] += product;
160            } else {
161                acc[degree - n] -= product;
162            }
163        }
164    }
165
166    Poly(
167        acc.into_iter()
168            .map(|coeff| poly_reduce_mod_q(coeff, params.q()))
169            .collect(),
170    )
171}
172
173/// Computes the infinity norm of a polynomial.
174///
175/// This function is not constant-time and must not be called on secret values
176/// where timing leakage is within the caller's threat model.
177/// Coefficient values of [`i64::MIN`] are handled via saturating absolute value
178/// and will report a norm of [`i64::MAX`], which always fails the rejection
179/// bound check.
180pub(crate) fn poly_infinity_norm(a: &Poly) -> i64 {
181    a.0.iter()
182        .map(|coeff| i64::saturating_abs(*coeff))
183        .max()
184        .unwrap_or(0)
185}
186
187pub(crate) fn sample_uniform_poly(params: &Params, rng: &mut (impl CryptoRng + RngCore)) -> Poly {
188    let q_u = u64::try_from(params.q()).expect("modulus is positive");
189    let max_acceptable = u64::MAX - (u64::MAX % q_u);
190    let mut coeffs = Vec::with_capacity(params.n());
191
192    for _ in 0..params.n() {
193        loop {
194            let sample = rng.next_u64();
195            if sample < max_acceptable {
196                coeffs.push(poly_reduce_mod_q(i128::from(sample % q_u), params.q()));
197                break;
198            }
199        }
200    }
201
202    Poly(coeffs)
203}
204
205pub(crate) fn sample_cbd(params: &Params, rng: &mut (impl CryptoRng + RngCore)) -> Poly {
206    let mut coeffs = Vec::with_capacity(params.n());
207
208    for _ in 0..params.n() {
209        let mut lhs = 0_i64;
210        let mut rhs = 0_i64;
211        for _ in 0..params.eta() {
212            lhs += i64::from(rng.next_u32() & 1);
213            rhs += i64::from(rng.next_u32() & 1);
214        }
215        coeffs.push(lhs - rhs);
216    }
217
218    Poly(coeffs)
219}
220
221pub(crate) fn sample_bounded_poly(
222    bound: i64,
223    params: &Params,
224    rng: &mut (impl CryptoRng + RngCore),
225) -> Poly {
226    let range = u64::try_from(2 * bound + 1).expect("bound is positive");
227    let mut coeffs = Vec::with_capacity(params.n());
228
229    for _ in 0..params.n() {
230        let sample = sample_below(range, rng);
231        let signed = i64::try_from(sample).expect("range fits in i64") - bound;
232        coeffs.push(poly_reduce_mod_q(i128::from(signed), params.q()));
233    }
234
235    Poly(coeffs)
236}
237
238fn sample_below(bound: u64, rng: &mut (impl CryptoRng + RngCore)) -> u64 {
239    let max_acceptable = u64::MAX - (u64::MAX % bound);
240    loop {
241        let sample = rng.next_u64();
242        if sample < max_acceptable {
243            return sample % bound;
244        }
245    }
246}