Skip to main content

rsa/traits/
modular.rs

1// TODO: document the public surface once the trait shape settles.
2#![allow(missing_docs)]
3
4use core::borrow::Borrow;
5
6#[cfg(feature = "alloc")]
7use alloc::boxed::Box;
8use const_num_traits::PrimBits;
9#[cfg(not(feature = "modmath"))]
10use const_num_traits::PrimInt;
11use const_num_traits::{FromBytes as NumFromBytes, ToBytes as NumToBytes, Zero};
12#[cfg(feature = "alloc")]
13use crypto_bigint::{
14    modular::{BoxedMontyForm, BoxedMontyParams},
15    BoxedUint, Resize as CryptoResize,
16};
17#[cfg(feature = "alloc")]
18use crypto_bigint::{NonZero as CryptoNonZero, Odd as CryptoOdd};
19use zeroize::Zeroize;
20
21use crate::errors::{Error, Result};
22
23pub trait NumBytes: Borrow<[u8]> + AsRef<[u8]> {}
24
25impl<T> NumBytes for T where T: Borrow<[u8]> + AsRef<[u8]> {}
26
27#[repr(transparent)]
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct NonZero<T>(T);
30
31#[repr(transparent)]
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct Odd<T>(T);
34
35pub trait IntegerResize: Sized {
36    type Output;
37
38    fn resize_unchecked(self, at_least_bits_precision: u32) -> Self::Output;
39    fn try_resize(self, at_least_bits_precision: u32) -> Option<Self::Output>;
40}
41
42pub trait FixedWidthUnsignedInt: Zeroize + Clone + Copy {
43    type Bytes: NumBytes + Default + AsMut<[u8]>;
44
45    fn leading_zeros(&self) -> u32;
46    fn to_be_bytes(&self) -> Self::Bytes;
47    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self>;
48    fn bits_precision(&self) -> u32;
49}
50
51#[cfg(feature = "modmath")]
52impl<T> FixedWidthUnsignedInt for T
53where
54    T: Zeroize + Clone + Copy + PrimBits + Zero + NumToBytes + NumFromBytes,
55    T: NumToBytes<Bytes = <T as NumFromBytes>::Bytes>,
56    <T as NumToBytes>::Bytes: NumBytes + Default + AsMut<[u8]>,
57{
58    type Bytes = <T as NumToBytes>::Bytes;
59
60    fn leading_zeros(&self) -> u32 {
61        PrimBits::leading_zeros(*self)
62    }
63
64    fn to_be_bytes(&self) -> Self::Bytes {
65        NumToBytes::to_be_bytes(*self)
66    }
67
68    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
69        let mut repr = <T as NumFromBytes>::Bytes::default();
70        let out = repr.as_mut();
71        let out_len = out.len();
72        if bytes.len() > out_len {
73            return Err(Error::InvalidArguments);
74        }
75        out[out_len - bytes.len()..].copy_from_slice(bytes);
76        Ok(NumFromBytes::from_be_bytes(&repr))
77    }
78
79    fn bits_precision(&self) -> u32 {
80        PrimBits::count_zeros(<T as Zero>::zero())
81    }
82}
83
84#[cfg(not(feature = "modmath"))]
85impl<T> FixedWidthUnsignedInt for T
86where
87    T: Zeroize + Clone + Copy + PrimInt + NumToBytes + NumFromBytes,
88    T: NumToBytes<Bytes = <T as NumFromBytes>::Bytes>,
89    <T as NumToBytes>::Bytes: NumBytes + Default + AsMut<[u8]>,
90{
91    type Bytes = <T as NumToBytes>::Bytes;
92
93    fn leading_zeros(&self) -> u32 {
94        PrimBits::leading_zeros(*self)
95    }
96
97    fn to_be_bytes(&self) -> Self::Bytes {
98        NumToBytes::to_be_bytes(*self)
99    }
100
101    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
102        let mut repr = <T as NumFromBytes>::Bytes::default();
103        let out = repr.as_mut();
104        let out_len = out.len();
105        if bytes.len() > out_len {
106            return Err(Error::InvalidArguments);
107        }
108        out[out_len - bytes.len()..].copy_from_slice(bytes);
109        Ok(NumFromBytes::from_be_bytes(&repr))
110    }
111
112    fn bits_precision(&self) -> u32 {
113        <T as Zero>::zero().count_zeros()
114    }
115}
116
117#[cfg(not(feature = "alloc"))]
118impl<T> IntegerResize for T
119where
120    T: FixedWidthUnsignedInt,
121{
122    type Output = Self;
123
124    fn resize_unchecked(self, _at_least_bits_precision: u32) -> Self::Output {
125        self
126    }
127
128    fn try_resize(self, at_least_bits_precision: u32) -> Option<Self::Output> {
129        // Mirrors `crypto_bigint::Resize::try_resize`: returns `Some` iff
130        // the actual value fits in `at_least_bits_precision` bits. T is
131        // fixed-width and `resize_unchecked` is a no-op, but the check
132        // still needs to reject values that wouldn't survive a narrower
133        // precision.
134        let value_bits = self.bits_precision() - self.leading_zeros();
135        if value_bits <= at_least_bits_precision {
136            Some(self)
137        } else {
138            None
139        }
140    }
141}
142
143#[cfg(not(feature = "alloc"))]
144impl<T> UnsignedModularInt for T
145where
146    T: FixedWidthUnsignedInt + PartialOrd,
147{
148    type Bytes = <T as FixedWidthUnsignedInt>::Bytes;
149
150    fn leading_zeros(&self) -> u32 {
151        FixedWidthUnsignedInt::leading_zeros(self)
152    }
153
154    fn to_be_bytes(&self) -> Self::Bytes {
155        FixedWidthUnsignedInt::to_be_bytes(self)
156    }
157
158    fn as_nz_ref(&self) -> NonZero<Self> {
159        NonZero::new(*self).expect("value is non-zero")
160    }
161
162    fn bits(&self) -> u32 {
163        self.bits_precision() - self.leading_zeros()
164    }
165
166    fn bits_precision(&self) -> u32 {
167        FixedWidthUnsignedInt::bits_precision(self)
168    }
169
170    #[cfg(feature = "alloc")]
171    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
172        unreachable!("alloc-gated")
173    }
174}
175
176#[cfg(not(feature = "alloc"))]
177impl<T> TryFromBeBytes for T
178where
179    T: FixedWidthUnsignedInt,
180{
181    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
182        FixedWidthUnsignedInt::try_from_be_bytes_vartime(bytes)
183    }
184}
185
186pub trait TryFromBeBytes: Sized {
187    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self>;
188}
189
190pub trait UnsignedModularInt:
191    Zeroize + Clone + PartialOrd + IntegerResize<Output = Self> + TryFromBeBytes
192{
193    type Bytes: NumBytes + AsMut<[u8]>;
194    fn leading_zeros(&self) -> u32;
195    fn to_be_bytes(&self) -> Self::Bytes;
196    fn as_nz_ref(&self) -> NonZero<Self>;
197    fn bits(&self) -> u32;
198    fn bits_precision(&self) -> u32;
199    #[cfg(feature = "alloc")]
200    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]>;
201}
202
203impl<T> NonZero<T>
204where
205    T: UnsignedModularInt,
206{
207    pub fn new(value: T) -> Option<Self> {
208        if value.bits() == 0 {
209            None
210        } else {
211            Some(Self(value))
212        }
213    }
214
215    pub fn get(self) -> T {
216        self.0
217    }
218
219    #[allow(clippy::should_implement_trait)]
220    pub fn as_ref(&self) -> &T {
221        &self.0
222    }
223
224    pub fn bits(&self) -> u32 {
225        self.0.bits()
226    }
227
228    pub fn bits_precision(&self) -> u32 {
229        self.0.bits_precision()
230    }
231
232    pub fn to_be_bytes(&self) -> T::Bytes {
233        self.0.to_be_bytes()
234    }
235
236    #[cfg(feature = "alloc")]
237    pub fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
238        self.0.to_be_bytes_trimmed_vartime()
239    }
240}
241
242impl<T> Odd<T>
243where
244    T: UnsignedModularInt,
245{
246    pub fn new(value: T) -> Option<Self> {
247        let non_zero = NonZero::new(value)?;
248        let bytes = non_zero.as_ref().to_be_bytes();
249        let bytes = bytes.as_ref();
250        let is_odd = bytes.last().map(|byte| byte & 1 == 1).unwrap_or(false);
251        if is_odd {
252            Some(Self(non_zero.get()))
253        } else {
254            None
255        }
256    }
257
258    pub fn get(self) -> T {
259        self.0
260    }
261
262    #[allow(clippy::should_implement_trait)]
263    pub fn as_ref(&self) -> &T {
264        &self.0
265    }
266
267    pub fn as_nz_ref(&self) -> NonZero<T> {
268        NonZero::new(self.0.clone()).expect("odd values are non-zero")
269    }
270
271    pub fn bits_precision(&self) -> u32 {
272        self.0.bits_precision()
273    }
274}
275
276/// Build a Montgomery-domain value.
277///
278/// Two constructors with **different input contracts**:
279///
280/// - [`from_reduced`](Self::from_reduced) — caller guarantees `integer <
281///   params.modulus()`. Implementations may rely on this; no reduction is
282///   performed. Use this when you already know the value is reduced.
283/// - [`from_value`](Self::from_value) — accepts any `integer` in
284///   `[0, 2^bits_precision)`. Implementations MUST handle the unreduced
285///   case (either by reducing internally or by using a Montgomery primitive
286///   that tolerates unreduced inputs, e.g. CIOS with `raw * R²`).
287///
288/// No default `from_value` is provided on purpose. Forwarding to
289/// `from_reduced` would silently produce wrong results for unreduced
290/// inputs on backends that don't tolerate them — the trait makes this
291/// distinction explicit so each implementor confronts it.
292pub trait IntoMontyForm<P: ModulusParams>: Sized {
293    /// Build from an integer already reduced modulo `params.modulus()`.
294    fn from_reduced(integer: P::Modulus, params: &P) -> Self;
295
296    /// Build from any integer in `[0, 2^bits_precision)`, handling
297    /// reduction internally if needed.
298    fn from_value(integer: P::Modulus, params: &P) -> Self;
299}
300
301#[cfg(feature = "alloc")]
302impl IntoMontyForm<BoxedMontyParams> for BoxedMontyForm {
303    fn from_reduced(integer: BoxedUint, params: &BoxedMontyParams) -> Self {
304        BoxedMontyForm::new(integer, params)
305    }
306
307    fn from_value(integer: BoxedUint, params: &BoxedMontyParams) -> Self {
308        let modulus =
309            CryptoNonZero::new(params.modulus().as_ref().clone()).expect("modulus is non-zero");
310        let reduced = integer.rem_vartime(&modulus);
311        Self::from_reduced(reduced, params)
312    }
313}
314
315pub trait PowBoundedExp<M: ModulusParams>: Sized {
316    fn pow_bounded_exp(&self, exp: &M::Modulus, exp_bits: u32) -> Self;
317    fn retrieve(&self) -> M::Modulus;
318}
319
320#[cfg(feature = "alloc")]
321impl PowBoundedExp<BoxedMontyParams> for BoxedMontyForm {
322    fn pow_bounded_exp(&self, exp: &BoxedUint, exp_bits: u32) -> Self {
323        self.clone().pow_bounded_exp(exp, exp_bits)
324    }
325
326    fn retrieve(&self) -> BoxedUint {
327        self.clone().retrieve()
328    }
329}
330
331pub trait Pow<M: ModulusParams>: Sized {
332    fn pow(&self, exp: &M::Modulus) -> Self;
333}
334
335#[cfg(feature = "alloc")]
336impl Pow<BoxedMontyParams> for BoxedMontyForm {
337    fn pow(&self, exp: &BoxedUint) -> Self {
338        self.clone().pow(exp)
339    }
340}
341
342/// Constant-time multiplicative inverse in Montgomery form.
343///
344/// Returns `Some(self⁻¹ mod n)` when the value is coprime to the
345/// modulus; `None` means no inverse exists (`gcd(self, n) != 1`).
346/// Non-coprimality is astronomically rare when `self` came from a
347/// fresh random against RSA `n = p·q`, but real — retry with a fresh
348/// random a small constant number of times.
349///
350/// Used by the sign-path blinding
351/// (`crate::algorithms::rsa::rsa_private_op_blinded` — plain code
352/// span because the target is feature-gated and an intra-doc link
353/// would break `cargo doc --no-default-features`).
354pub trait InvertCt<M: ModulusParams>: Sized {
355    fn invert_ct(&self) -> Option<Self>;
356}
357
358#[cfg(feature = "alloc")]
359impl InvertCt<BoxedMontyParams> for BoxedMontyForm {
360    fn invert_ct(&self) -> Option<Self> {
361        self.invert().into_option()
362    }
363}
364
365/// Constant-time multiplication in Montgomery form.
366///
367/// The Montgomery form's native multiplication — CT on both supported
368/// backends (`BoxedMontyForm`'s `Mul` via crypto-bigint,
369/// `ModMathForm<T, Ct>`'s `Field::mul` via modmath's CIOS-Ct).
370///
371/// Both operands must share the same `ModulusParams` instance
372/// (invariant: same modulus / same Montgomery R). Not type-checked;
373/// impls may `debug_assert` it.
374pub trait MulCt<M: ModulusParams>: Sized {
375    fn mul_ct(&self, rhs: &Self) -> Self;
376}
377
378#[cfg(feature = "alloc")]
379impl MulCt<BoxedMontyParams> for BoxedMontyForm {
380    fn mul_ct(&self, rhs: &Self) -> Self {
381        self * rhs
382    }
383}
384
385/// Sample a uniform random value in `[0, modulus)` from a CSPRNG.
386///
387/// Rejection-sampled, so vartime — but only in the retry count, which
388/// depends on the modulus, not on the returned value (rejected
389/// candidates are discarded and never reach the caller). An n-bit
390/// modulus has its top bit set by definition, so acceptance is ≥ 50%.
391///
392/// Used to sample the blinding factor `r` in
393/// `crate::algorithms::rsa::rsa_private_op_and_check_blinded`.
394pub trait TryRandomMod: Sized {
395    fn try_random_mod<R>(rng: &mut R, modulus: &Self) -> Result<Self>
396    where
397        R: rand_core::TryCryptoRng + ?Sized;
398}
399
400#[cfg(feature = "alloc")]
401impl TryRandomMod for BoxedUint {
402    fn try_random_mod<R>(rng: &mut R, modulus: &Self) -> Result<Self>
403    where
404        R: rand_core::TryCryptoRng + ?Sized,
405    {
406        let nz = CryptoNonZero::new(modulus.clone())
407            .into_option()
408            .ok_or(Error::InvalidModulus)?;
409        <Self as crypto_bigint::RandomMod>::try_random_mod_vartime(rng, &nz).map_err(|_| Error::Rng)
410    }
411}
412
413pub trait ModulusParams: Sized {
414    type Modulus: UnsignedModularInt;
415    type MontgomeryForm: IntoMontyForm<Self> + PowBoundedExp<Self>;
416    fn modulus(&self) -> &Odd<Self::Modulus>;
417    fn bits_precision(&self) -> u32;
418}
419
420pub(crate) mod sealed {
421    /// Prevents external crates from implementing
422    /// [`super::CtModulusParams`] on backends we haven't audited —
423    /// see the security note on that trait.
424    pub trait CtModulusParamsSealed {}
425}
426
427/// Marker trait for [`ModulusParams`] backends whose Montgomery
428/// exponentiation itself is constant-time in the base value.
429///
430/// Bound the public-key encryption path on this so plaintext (which
431/// **is** secret) can't be routed through a vartime `pow_bounded_exp`.
432/// Signature verification stays unbounded — the "base" there is the
433/// public signature, so vartime is fine.
434///
435/// The trait is sealed — only backends impl'd by this crate can opt
436/// in. Downstream crates cannot claim the guarantee for their own
437/// backends without inviting the exact side-channel this bound is
438/// meant to keep out.
439///
440/// # Backends
441///
442/// - Under `feature = "alloc"`, `crypto_bigint::modular::BoxedMontyParams`
443///   opts in. Its `BoxedMontyForm::pow_bounded_exp` is CT in the
444///   base. **Caveat:** the pre-exponentiation conversion (see
445///   `IntoMontyForm::from_value` for `BoxedMontyForm`) still uses a
446///   vartime reduction (`BoxedUint::rem_vartime`) inherited from
447///   upstream `RustCrypto/RSA`. Callers requiring rigorous CT
448///   guarantees over the whole encrypt chain should use the no-alloc
449///   modmath backend at the `Ct` personality (below). The alloc
450///   impl is included primarily for API-surface compatibility with
451///   upstream and to keep the default alloc encrypt path functional.
452/// - Under `feature = "modmath"`, `ModMathParams<T, Ct>` opts in —
453///   the no-alloc CT-personality substitution, honestly CT top to
454///   bottom.
455/// - `ModMathParams<T, Nct>` **deliberately does not** opt in;
456///   `NctPublicKey`-derived encrypting keys fail the encrypt trait
457///   bound at compile time.
458pub trait CtModulusParams: ModulusParams + sealed::CtModulusParamsSealed {}
459
460#[cfg(feature = "alloc")]
461impl sealed::CtModulusParamsSealed for BoxedMontyParams {}
462#[cfg(feature = "alloc")]
463impl CtModulusParams for BoxedMontyParams {}
464
465#[cfg(feature = "alloc")]
466impl ModulusParams for BoxedMontyParams {
467    type Modulus = BoxedUint;
468    type MontgomeryForm = BoxedMontyForm;
469    fn modulus(&self) -> &Odd<Self::Modulus> {
470        // Our `Odd<T>` is `#[repr(transparent)]` over `T`. `crypto_bigint::Odd<T>`
471        // is a single-field tuple struct around `T`, not formally
472        // `#[repr(transparent)]` — verify layout at compile time so a future
473        // crypto_bigint version that changes representation fails to build
474        // instead of producing silent UB.
475        const _: () = assert!(
476            core::mem::size_of::<CryptoOdd<BoxedUint>>() == core::mem::size_of::<Odd<BoxedUint>>()
477        );
478        const _: () = assert!(
479            core::mem::align_of::<CryptoOdd<BoxedUint>>()
480                == core::mem::align_of::<Odd<BoxedUint>>()
481        );
482        unsafe {
483            &*(self.modulus() as *const CryptoOdd<Self::Modulus> as *const Odd<Self::Modulus>)
484        }
485    }
486    fn bits_precision(&self) -> u32 {
487        self.bits_precision()
488    }
489}
490
491#[cfg(feature = "alloc")]
492impl IntegerResize for BoxedUint {
493    type Output = Self;
494
495    fn resize_unchecked(self, at_least_bits_precision: u32) -> Self::Output {
496        CryptoResize::resize_unchecked(self, at_least_bits_precision)
497    }
498
499    fn try_resize(self, at_least_bits_precision: u32) -> Option<Self::Output> {
500        CryptoResize::try_resize(self, at_least_bits_precision)
501    }
502}
503
504#[cfg(feature = "alloc")]
505impl UnsignedModularInt for BoxedUint {
506    type Bytes = alloc::boxed::Box<[u8]>;
507
508    fn leading_zeros(&self) -> u32 {
509        self.leading_zeros()
510    }
511
512    fn to_be_bytes(&self) -> Self::Bytes {
513        self.to_be_bytes()
514    }
515    #[cfg(feature = "alloc")]
516    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
517        self.to_be_bytes_trimmed_vartime()
518    }
519    fn as_nz_ref(&self) -> NonZero<Self> {
520        NonZero::new(self.clone()).expect("Value is non-zero")
521    }
522    fn bits(&self) -> u32 {
523        self.bits()
524    }
525    fn bits_precision(&self) -> u32 {
526        self.bits_precision()
527    }
528}
529
530#[cfg(feature = "alloc")]
531impl TryFromBeBytes for BoxedUint {
532    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
533        Ok(BoxedUint::from_be_slice_vartime(bytes))
534    }
535}