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