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        // Fallible slice + byte-copy loop rather than `[..]` +
76        // `copy_from_slice`, so no panic path reaches the sign binary;
77        // `dst.len()` == `bytes.len()` by construction.
78        let dst = out
79            .get_mut(out_len - bytes.len()..)
80            .ok_or(Error::InvalidArguments)?;
81        for (d, s) in dst.iter_mut().zip(bytes.iter()) {
82            *d = *s;
83        }
84        Ok(NumFromBytes::from_be_bytes(&repr))
85    }
86
87    fn bits_precision(&self) -> u32 {
88        PrimBits::count_zeros(<T as Zero>::zero())
89    }
90}
91
92#[cfg(not(feature = "modmath"))]
93impl<T> FixedWidthUnsignedInt for T
94where
95    T: Zeroize + Clone + Copy + PrimInt + NumToBytes + NumFromBytes,
96    T: NumToBytes<Bytes = <T as NumFromBytes>::Bytes>,
97    <T as NumToBytes>::Bytes: NumBytes + Default + AsMut<[u8]>,
98{
99    type Bytes = <T as NumToBytes>::Bytes;
100
101    fn leading_zeros(&self) -> u32 {
102        PrimBits::leading_zeros(*self)
103    }
104
105    fn to_be_bytes(&self) -> Self::Bytes {
106        NumToBytes::to_be_bytes(*self)
107    }
108
109    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
110        let mut repr = <T as NumFromBytes>::Bytes::default();
111        let out = repr.as_mut();
112        let out_len = out.len();
113        if bytes.len() > out_len {
114            return Err(Error::InvalidArguments);
115        }
116        // Fallible slice + byte-copy loop rather than `[..]` +
117        // `copy_from_slice`, so no panic path reaches the sign binary;
118        // `dst.len()` == `bytes.len()` by construction.
119        let dst = out
120            .get_mut(out_len - bytes.len()..)
121            .ok_or(Error::InvalidArguments)?;
122        for (d, s) in dst.iter_mut().zip(bytes.iter()) {
123            *d = *s;
124        }
125        Ok(NumFromBytes::from_be_bytes(&repr))
126    }
127
128    fn bits_precision(&self) -> u32 {
129        <T as Zero>::zero().count_zeros()
130    }
131}
132
133#[cfg(not(feature = "alloc"))]
134impl<T> IntegerResize for T
135where
136    T: FixedWidthUnsignedInt,
137{
138    type Output = Self;
139
140    fn resize_unchecked(self, _at_least_bits_precision: u32) -> Self::Output {
141        self
142    }
143
144    fn try_resize(self, at_least_bits_precision: u32) -> Option<Self::Output> {
145        // Mirrors `crypto_bigint::Resize::try_resize`: returns `Some` iff
146        // the actual value fits in `at_least_bits_precision` bits. T is
147        // fixed-width and `resize_unchecked` is a no-op, but the check
148        // still needs to reject values that wouldn't survive a narrower
149        // precision.
150        let value_bits = self.bits_precision() - self.leading_zeros();
151        if value_bits <= at_least_bits_precision {
152            Some(self)
153        } else {
154            None
155        }
156    }
157}
158
159#[cfg(not(feature = "alloc"))]
160impl<T> UnsignedModularInt for T
161where
162    T: FixedWidthUnsignedInt + PartialOrd,
163{
164    type Bytes = <T as FixedWidthUnsignedInt>::Bytes;
165
166    fn leading_zeros(&self) -> u32 {
167        FixedWidthUnsignedInt::leading_zeros(self)
168    }
169
170    fn to_be_bytes(&self) -> Self::Bytes {
171        FixedWidthUnsignedInt::to_be_bytes(self)
172    }
173
174    fn as_nz_ref(&self) -> NonZero<Self> {
175        NonZero::new(*self).expect("value is non-zero")
176    }
177
178    fn bits(&self) -> u32 {
179        self.bits_precision() - self.leading_zeros()
180    }
181
182    fn bits_precision(&self) -> u32 {
183        FixedWidthUnsignedInt::bits_precision(self)
184    }
185
186    #[cfg(feature = "alloc")]
187    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
188        unreachable!("alloc-gated")
189    }
190}
191
192#[cfg(not(feature = "alloc"))]
193impl<T> TryFromBeBytes for T
194where
195    T: FixedWidthUnsignedInt,
196{
197    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
198        FixedWidthUnsignedInt::try_from_be_bytes_vartime(bytes)
199    }
200}
201
202pub trait TryFromBeBytes: Sized {
203    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self>;
204}
205
206pub trait UnsignedModularInt:
207    Zeroize + Clone + PartialOrd + IntegerResize<Output = Self> + TryFromBeBytes
208{
209    type Bytes: NumBytes + AsMut<[u8]>;
210    fn leading_zeros(&self) -> u32;
211    fn to_be_bytes(&self) -> Self::Bytes;
212    fn as_nz_ref(&self) -> NonZero<Self>;
213    fn bits(&self) -> u32;
214    fn bits_precision(&self) -> u32;
215    #[cfg(feature = "alloc")]
216    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]>;
217}
218
219impl<T> NonZero<T>
220where
221    T: UnsignedModularInt,
222{
223    pub fn new(value: T) -> Option<Self> {
224        if value.bits() == 0 {
225            None
226        } else {
227            Some(Self(value))
228        }
229    }
230
231    pub fn get(self) -> T {
232        self.0
233    }
234
235    #[allow(clippy::should_implement_trait)]
236    pub fn as_ref(&self) -> &T {
237        &self.0
238    }
239
240    pub fn bits(&self) -> u32 {
241        self.0.bits()
242    }
243
244    pub fn bits_precision(&self) -> u32 {
245        self.0.bits_precision()
246    }
247
248    pub fn to_be_bytes(&self) -> T::Bytes {
249        self.0.to_be_bytes()
250    }
251
252    #[cfg(feature = "alloc")]
253    pub fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
254        self.0.to_be_bytes_trimmed_vartime()
255    }
256}
257
258impl<T> Odd<T>
259where
260    T: UnsignedModularInt,
261{
262    pub fn new(value: T) -> Option<Self> {
263        let non_zero = NonZero::new(value)?;
264        let bytes = non_zero.as_ref().to_be_bytes();
265        let bytes = bytes.as_ref();
266        let is_odd = bytes.last().map(|byte| byte & 1 == 1).unwrap_or(false);
267        if is_odd {
268            Some(Self(non_zero.get()))
269        } else {
270            None
271        }
272    }
273
274    pub fn get(self) -> T {
275        self.0
276    }
277
278    #[allow(clippy::should_implement_trait)]
279    pub fn as_ref(&self) -> &T {
280        &self.0
281    }
282
283    pub fn as_nz_ref(&self) -> NonZero<T> {
284        NonZero::new(self.0.clone()).expect("odd values are non-zero")
285    }
286
287    pub fn bits_precision(&self) -> u32 {
288        self.0.bits_precision()
289    }
290}
291
292/// Build a Montgomery-domain value.
293///
294/// Two constructors with **different input contracts**:
295///
296/// - [`from_reduced`](Self::from_reduced) — caller guarantees `integer <
297///   params.modulus()`. Implementations may rely on this; no reduction is
298///   performed. Use this when you already know the value is reduced.
299/// - [`from_value`](Self::from_value) — accepts any `integer` in
300///   `[0, 2^bits_precision)`. Implementations MUST handle the unreduced
301///   case (either by reducing internally or by using a Montgomery primitive
302///   that tolerates unreduced inputs, e.g. CIOS with `raw * R²`).
303///
304/// No default `from_value` is provided on purpose. Forwarding to
305/// `from_reduced` would silently produce wrong results for unreduced
306/// inputs on backends that don't tolerate them — the trait makes this
307/// distinction explicit so each implementor confronts it.
308pub trait IntoMontyForm<P: ModulusParams>: Sized {
309    /// Build from an integer already reduced modulo `params.modulus()`.
310    fn from_reduced(integer: P::Modulus, params: &P) -> Self;
311
312    /// Build from any integer in `[0, 2^bits_precision)`, handling
313    /// reduction internally if needed.
314    fn from_value(integer: P::Modulus, params: &P) -> Self;
315}
316
317#[cfg(feature = "alloc")]
318impl IntoMontyForm<BoxedMontyParams> for BoxedMontyForm {
319    fn from_reduced(integer: BoxedUint, params: &BoxedMontyParams) -> Self {
320        BoxedMontyForm::new(integer, params)
321    }
322
323    fn from_value(integer: BoxedUint, params: &BoxedMontyParams) -> Self {
324        let modulus =
325            CryptoNonZero::new(params.modulus().as_ref().clone()).expect("modulus is non-zero");
326        let reduced = integer.rem_vartime(&modulus);
327        Self::from_reduced(reduced, params)
328    }
329}
330
331pub trait PowBoundedExp<M: ModulusParams>: Sized {
332    fn pow_bounded_exp(&self, exp: &M::Modulus, exp_bits: u32) -> Self;
333    fn retrieve(&self) -> M::Modulus;
334}
335
336#[cfg(feature = "alloc")]
337impl PowBoundedExp<BoxedMontyParams> for BoxedMontyForm {
338    fn pow_bounded_exp(&self, exp: &BoxedUint, exp_bits: u32) -> Self {
339        self.clone().pow_bounded_exp(exp, exp_bits)
340    }
341
342    fn retrieve(&self) -> BoxedUint {
343        self.clone().retrieve()
344    }
345}
346
347pub trait Pow<M: ModulusParams>: Sized {
348    fn pow(&self, exp: &M::Modulus) -> Self;
349}
350
351#[cfg(feature = "alloc")]
352impl Pow<BoxedMontyParams> for BoxedMontyForm {
353    fn pow(&self, exp: &BoxedUint) -> Self {
354        self.clone().pow(exp)
355    }
356}
357
358/// Constant-time multiplicative inverse in Montgomery form.
359///
360/// Returns `Some(self⁻¹ mod n)` when the value is coprime to the
361/// modulus; `None` means no inverse exists (`gcd(self, n) != 1`).
362/// Non-coprimality is astronomically rare when `self` came from a
363/// fresh random against RSA `n = p·q`, but real — retry with a fresh
364/// random a small constant number of times.
365///
366/// Used by the sign-path blinding
367/// (`crate::algorithms::rsa::rsa_private_op_blinded` — plain code
368/// span because the target is feature-gated and an intra-doc link
369/// would break `cargo doc --no-default-features`).
370pub trait InvertCt<M: ModulusParams>: Sized {
371    fn invert_ct(&self) -> Option<Self>;
372}
373
374#[cfg(feature = "alloc")]
375impl InvertCt<BoxedMontyParams> for BoxedMontyForm {
376    fn invert_ct(&self) -> Option<Self> {
377        self.invert().into_option()
378    }
379}
380
381/// Constant-time multiplication in Montgomery form.
382///
383/// The Montgomery form's native multiplication — CT on both supported
384/// backends (`BoxedMontyForm`'s `Mul` via crypto-bigint,
385/// `ModMathForm<T, Ct>`'s `Field::mul` via modmath's CIOS-Ct).
386///
387/// Both operands must share the same `ModulusParams` instance
388/// (invariant: same modulus / same Montgomery R). Not type-checked;
389/// impls may `debug_assert` it.
390pub trait MulCt<M: ModulusParams>: Sized {
391    fn mul_ct(&self, rhs: &Self) -> Self;
392}
393
394#[cfg(feature = "alloc")]
395impl MulCt<BoxedMontyParams> for BoxedMontyForm {
396    fn mul_ct(&self, rhs: &Self) -> Self {
397        self * rhs
398    }
399}
400
401/// Sample a uniform random value in `[0, modulus)` from a CSPRNG.
402///
403/// Rejection-sampled, so vartime — but only in the retry count, which
404/// depends on the modulus, not on the returned value (rejected
405/// candidates are discarded and never reach the caller). An n-bit
406/// modulus has its top bit set by definition, so acceptance is ≥ 50%.
407///
408/// Used to sample the blinding factor `r` in
409/// `crate::algorithms::rsa::rsa_private_op_and_check_blinded`.
410pub trait TryRandomMod: Sized {
411    fn try_random_mod<R>(rng: &mut R, modulus: &Self) -> Result<Self>
412    where
413        R: rand_core::TryCryptoRng + ?Sized;
414}
415
416#[cfg(feature = "alloc")]
417impl TryRandomMod for BoxedUint {
418    fn try_random_mod<R>(rng: &mut R, modulus: &Self) -> Result<Self>
419    where
420        R: rand_core::TryCryptoRng + ?Sized,
421    {
422        let nz = CryptoNonZero::new(modulus.clone())
423            .into_option()
424            .ok_or(Error::InvalidModulus)?;
425        <Self as crypto_bigint::RandomMod>::try_random_mod_vartime(rng, &nz).map_err(|_| Error::Rng)
426    }
427}
428
429pub trait ModulusParams: Sized {
430    type Modulus: UnsignedModularInt;
431    type MontgomeryForm: IntoMontyForm<Self> + PowBoundedExp<Self>;
432    fn modulus(&self) -> &Odd<Self::Modulus>;
433    fn bits_precision(&self) -> u32;
434}
435
436pub(crate) mod sealed {
437    /// Prevents external crates from implementing
438    /// [`super::CtModulusParams`] on backends we haven't audited —
439    /// see the security note on that trait.
440    pub trait CtModulusParamsSealed {}
441}
442
443/// Marker trait for [`ModulusParams`] backends whose Montgomery
444/// exponentiation itself is constant-time in the base value.
445///
446/// Bound the public-key encryption path on this so plaintext (which
447/// **is** secret) can't be routed through a vartime `pow_bounded_exp`.
448/// Signature verification stays unbounded — the "base" there is the
449/// public signature, so vartime is fine.
450///
451/// The trait is sealed — only backends impl'd by this crate can opt
452/// in. Downstream crates cannot claim the guarantee for their own
453/// backends without inviting the exact side-channel this bound is
454/// meant to keep out.
455///
456/// # Backends
457///
458/// - Under `feature = "alloc"`, `crypto_bigint::modular::BoxedMontyParams`
459///   opts in. Its `BoxedMontyForm::pow_bounded_exp` is CT in the
460///   base. **Caveat:** the pre-exponentiation conversion (see
461///   `IntoMontyForm::from_value` for `BoxedMontyForm`) still uses a
462///   vartime reduction (`BoxedUint::rem_vartime`) inherited from
463///   upstream `RustCrypto/RSA`. Callers requiring rigorous CT
464///   guarantees over the whole encrypt chain should use the no-alloc
465///   modmath backend at the `Ct` personality (below). The alloc
466///   impl is included primarily for API-surface compatibility with
467///   upstream and to keep the default alloc encrypt path functional.
468/// - Under `feature = "modmath"`, `ModMathParams<T, Ct>` opts in —
469///   the no-alloc CT-personality substitution, honestly CT top to
470///   bottom.
471/// - `ModMathParams<T, Nct>` **deliberately does not** opt in;
472///   `NctPublicKey`-derived encrypting keys fail the encrypt trait
473///   bound at compile time.
474pub trait CtModulusParams: ModulusParams + sealed::CtModulusParamsSealed {}
475
476#[cfg(feature = "alloc")]
477impl sealed::CtModulusParamsSealed for BoxedMontyParams {}
478#[cfg(feature = "alloc")]
479impl CtModulusParams for BoxedMontyParams {}
480
481#[cfg(feature = "alloc")]
482impl ModulusParams for BoxedMontyParams {
483    type Modulus = BoxedUint;
484    type MontgomeryForm = BoxedMontyForm;
485    fn modulus(&self) -> &Odd<Self::Modulus> {
486        // Our `Odd<T>` is `#[repr(transparent)]` over `T`. `crypto_bigint::Odd<T>`
487        // is a single-field tuple struct around `T`, not formally
488        // `#[repr(transparent)]` — verify layout at compile time so a future
489        // crypto_bigint version that changes representation fails to build
490        // instead of producing silent UB.
491        const _: () = assert!(
492            core::mem::size_of::<CryptoOdd<BoxedUint>>() == core::mem::size_of::<Odd<BoxedUint>>()
493        );
494        const _: () = assert!(
495            core::mem::align_of::<CryptoOdd<BoxedUint>>()
496                == core::mem::align_of::<Odd<BoxedUint>>()
497        );
498        unsafe {
499            &*(self.modulus() as *const CryptoOdd<Self::Modulus> as *const Odd<Self::Modulus>)
500        }
501    }
502    fn bits_precision(&self) -> u32 {
503        self.bits_precision()
504    }
505}
506
507#[cfg(feature = "alloc")]
508impl IntegerResize for BoxedUint {
509    type Output = Self;
510
511    fn resize_unchecked(self, at_least_bits_precision: u32) -> Self::Output {
512        CryptoResize::resize_unchecked(self, at_least_bits_precision)
513    }
514
515    fn try_resize(self, at_least_bits_precision: u32) -> Option<Self::Output> {
516        CryptoResize::try_resize(self, at_least_bits_precision)
517    }
518}
519
520#[cfg(feature = "alloc")]
521impl UnsignedModularInt for BoxedUint {
522    type Bytes = alloc::boxed::Box<[u8]>;
523
524    fn leading_zeros(&self) -> u32 {
525        self.leading_zeros()
526    }
527
528    fn to_be_bytes(&self) -> Self::Bytes {
529        self.to_be_bytes()
530    }
531    #[cfg(feature = "alloc")]
532    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
533        self.to_be_bytes_trimmed_vartime()
534    }
535    fn as_nz_ref(&self) -> NonZero<Self> {
536        NonZero::new(self.clone()).expect("Value is non-zero")
537    }
538    fn bits(&self) -> u32 {
539        self.bits()
540    }
541    fn bits_precision(&self) -> u32 {
542        self.bits_precision()
543    }
544}
545
546#[cfg(feature = "alloc")]
547impl TryFromBeBytes for BoxedUint {
548    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
549        Ok(BoxedUint::from_be_slice_vartime(bytes))
550    }
551}
552
553// ─── CT type-surface guarantees ─────────────────────────────────────
554//
555// Compile-time assertions that the constant-time markers stay attached
556// to exactly the Ct personality and no other. A regression that leaks
557// `CtModulusParams` / `InvertCt` / `MulCt` onto an `Nct` carrier — or
558// drops them from a `Ct` one — fails the build here rather than
559// silently routing secret material through a variable-time path.
560//
561// Lives in this fork-only trait module (the file that defines the
562// markers) so the sealed / `pub(crate)` traits are nameable without
563// widening their visibility or touching any upstream-shared file.
564#[cfg(all(test, feature = "modmath"))]
565mod ct_type_guarantees {
566    use super::{CtModulusParams, InvertCt, MulCt};
567    use crate::modmath_support::{ModMathForm, ModMathParams};
568    use const_num_traits::{Ct, Nct};
569    use fixed_bigint::FixedUInt;
570    use static_assertions::{assert_impl_all, assert_not_impl_any};
571    use zeroize::ZeroizeOnDrop;
572
573    // Types are spelled inline rather than through aliases: older
574    // rustc's dead_code lint (e.g. 1.87, the MSRV CI row) doesn't see
575    // uses inside static_assertions' macro-expanded `const _` items,
576    // so aliases trip -Dwarnings there.
577
578    // The sealed encrypt/sign gate: only the Ct params opt in. An `Nct`
579    // params type reaching a `CtModulusParams`-bounded entry point is a
580    // compile error at the call site; this pins the marker itself.
581    assert_impl_all!(ModMathParams<FixedUInt<u8, 64, Ct>, Ct>: CtModulusParams);
582    assert_not_impl_any!(ModMathParams<FixedUInt<u8, 64, Nct>, Nct>: CtModulusParams);
583
584    // The blinding primitives (`rsa_private_op_blinded` and its check
585    // wrapper) bound the Montgomery form on `InvertCt` + `MulCt`; both
586    // exist only on the Ct form.
587    assert_impl_all!(
588        ModMathForm<FixedUInt<u8, 64, Ct>, Ct>:
589        InvertCt<ModMathParams<FixedUInt<u8, 64, Ct>, Ct>>,
590        MulCt<ModMathParams<FixedUInt<u8, 64, Ct>, Ct>>
591    );
592    assert_not_impl_any!(
593        ModMathForm<FixedUInt<u8, 64, Nct>, Nct>:
594        InvertCt<ModMathParams<FixedUInt<u8, 64, Nct>, Nct>>,
595        MulCt<ModMathParams<FixedUInt<u8, 64, Nct>, Nct>>
596    );
597
598    // Secret-bearing Montgomery values wipe on drop regardless of
599    // personality (the public value in an Nct form is not secret, but
600    // the type is the same shape and the guarantee is cheap to keep
601    // symmetric).
602    assert_impl_all!(ModMathForm<FixedUInt<u8, 64, Ct>, Ct>: ZeroizeOnDrop);
603    assert_impl_all!(ModMathForm<FixedUInt<u8, 64, Nct>, Nct>: ZeroizeOnDrop);
604}