Skip to main content

rsa/
modmath_support.rs

1//! Generic `modmath` backend adapters for fixed-width RSA public-key paths.
2//!
3
4// TODO: document the public surface once the trait shape settles.
5#![allow(missing_docs)]
6
7#[cfg(feature = "alloc")]
8use alloc::boxed::Box;
9use core::ops::{Shr, ShrAssign};
10
11use const_num_traits::ops::overflowing::OverflowingAdd;
12use const_num_traits::ops::wrapping::{WrappingAdd, WrappingMul, WrappingSub};
13use const_num_traits::FromByteSlice;
14use const_num_traits::WithPrecision;
15use const_num_traits::{Ct, HasPersonality, Nct, Personality};
16use const_num_traits::{One, Zero};
17use modmath::{CiosMontMul, CiosMontMulCt, Field as ModmathField, Parity, WideMul};
18use zeroize::Zeroize;
19
20use crate::{
21    algorithms::rsa::rsa_encrypt,
22    errors::{Error, Result},
23    key::GenericRsaPublicKey,
24    traits::modular::{
25        FixedWidthUnsignedInt, IntegerResize, IntoMontyForm, ModulusParams, NonZero, Odd, Pow,
26        PowBoundedExp, TryFromBeBytes, UnsignedModularInt,
27    },
28};
29
30pub trait ModMathInt:
31    FixedWidthUnsignedInt
32    + From<u8>
33    + PartialEq
34    + PartialOrd
35    + One
36    + Zero
37    + Parity
38    + OverflowingAdd<Output = Self>
39    + WideMul
40    + CiosMontMul
41    + WrappingAdd<Output = Self>
42    + WrappingMul<Output = Self>
43    + WrappingSub<Output = Self>
44    + Shr<usize, Output = Self>
45    + ShrAssign<usize>
46    + HasPersonality
47{
48}
49
50impl<T> ModMathInt for T where
51    T: FixedWidthUnsignedInt
52        + From<u8>
53        + PartialEq
54        + PartialOrd
55        + One
56        + Zero
57        + Parity
58        + OverflowingAdd<Output = Self>
59        + WideMul
60        + CiosMontMul
61        + WrappingAdd<Output = Self>
62        + WrappingMul<Output = Self>
63        + WrappingSub<Output = Self>
64        + Shr<usize, Output = Self>
65        + ShrAssign<usize>
66        + HasPersonality
67{
68}
69
70pub trait ModMathIntCt:
71    FixedWidthUnsignedInt
72    + From<u8>
73    + PartialEq
74    + PartialOrd
75    + One
76    + Zero
77    + Parity
78    + OverflowingAdd<Output = Self>
79    + WideMul
80    + CiosMontMulCt
81    + WrappingAdd<Output = Self>
82    + WrappingMul<Output = Self>
83    + WrappingSub<Output = Self>
84    + Shr<usize, Output = Self>
85    + ShrAssign<usize>
86    + subtle::ConditionallySelectable
87    + subtle::ConstantTimeLess
88    + core::ops::BitAnd<Output = Self>
89    + HasPersonality
90    + const_num_traits::CtIsZero
91{
92}
93
94impl<T> ModMathIntCt for T where
95    T: FixedWidthUnsignedInt
96        + From<u8>
97        + PartialEq
98        + PartialOrd
99        + One
100        + Zero
101        + Parity
102        + OverflowingAdd<Output = Self>
103        + WideMul
104        + CiosMontMulCt
105        + WrappingAdd<Output = Self>
106        + WrappingMul<Output = Self>
107        + WrappingSub<Output = Self>
108        + Shr<usize, Output = Self>
109        + ShrAssign<usize>
110        + subtle::ConditionallySelectable
111        + subtle::ConstantTimeLess
112        + core::ops::BitAnd<Output = Self>
113        + HasPersonality
114        + const_num_traits::CtIsZero
115{
116}
117
118#[cfg(feature = "alloc")]
119fn wrap_value<T>(value: T) -> ModMathValue<T> {
120    ModMathValue(value)
121}
122
123#[cfg(not(feature = "alloc"))]
124fn wrap_value<T>(value: T) -> ModMathValue<T> {
125    value
126}
127
128/// Cross-config constructor for [`ModMathValue`]. Under `alloc` it is
129/// the newtype constructor; under no-alloc `ModMathValue<T>` is a
130/// transparent alias for `T`, so it is the identity.
131///
132/// Prefer this over writing `ModMathValue(x)` directly: the tuple form
133/// only compiles on the `alloc` build and breaks the no-alloc one
134/// (`expected function / tuple struct, found type alias`).
135#[cfg(feature = "alloc")]
136pub fn wrap<T>(value: T) -> ModMathValue<T> {
137    ModMathValue(value)
138}
139
140/// See [`wrap`] — the no-alloc identity form.
141#[cfg(not(feature = "alloc"))]
142pub fn wrap<T>(value: T) -> ModMathValue<T> {
143    value
144}
145
146/// Cross-config unwrap for [`ModMathValue`]: `.0` under `alloc`, the
147/// identity under no-alloc. Prefer this over `.0` in code that must
148/// compile under both feature configurations. By-value (does not
149/// require `T: Copy`).
150#[cfg(feature = "alloc")]
151pub fn into_inner<T>(value: ModMathValue<T>) -> T {
152    value.0
153}
154
155/// See [`into_inner`] — the no-alloc identity form.
156#[cfg(not(feature = "alloc"))]
157pub fn into_inner<T>(value: ModMathValue<T>) -> T {
158    value
159}
160
161#[cfg(feature = "alloc")]
162fn unwrap_value<T: Copy>(value: &ModMathValue<T>) -> T {
163    value.0
164}
165
166#[cfg(feature = "alloc")]
167fn unwrap_value_ref<T>(value: &ModMathValue<T>) -> &T {
168    &value.0
169}
170
171#[cfg(not(feature = "alloc"))]
172fn unwrap_value_ref<T>(value: &ModMathValue<T>) -> &T {
173    value
174}
175
176#[cfg(not(feature = "alloc"))]
177fn unwrap_value<T: Copy>(value: &ModMathValue<T>) -> T {
178    *value
179}
180
181#[cfg(feature = "alloc")]
182#[repr(transparent)]
183#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
184pub struct ModMathValue<T>(pub T);
185
186#[cfg(feature = "alloc")]
187impl<T> ModMathValue<T> {
188    pub fn from_inner(inner: T) -> Self {
189        Self(inner)
190    }
191
192    pub fn inner(&self) -> &T {
193        &self.0
194    }
195}
196
197#[cfg(feature = "alloc")]
198impl<T> Zeroize for ModMathValue<T>
199where
200    T: Zeroize,
201{
202    fn zeroize(&mut self) {
203        self.0.zeroize();
204    }
205}
206
207#[cfg(feature = "alloc")]
208impl<T> From<u8> for ModMathValue<T>
209where
210    T: From<u8>,
211{
212    fn from(value: u8) -> Self {
213        Self(<T as From<u8>>::from(value))
214    }
215}
216
217#[cfg(feature = "alloc")]
218impl<T> IntegerResize for ModMathValue<T>
219where
220    T: FixedWidthUnsignedInt + PartialOrd,
221{
222    type Output = Self;
223
224    fn resize_unchecked(self, at_least_bits_precision: u32) -> Self::Output {
225        // `T` is our fixed-bigint carrier (it satisfies `FixedWidthUnsignedInt`,
226        // hence `WithPrecision`), so establish the operating width on the
227        // wrapped value — a real widen on a runtime-length carrier, the
228        // identity on a fixed-width one. Matches the no-alloc `T` impl in
229        // `traits::modular`; this is not the upstream `BoxedUint` path
230        // (that has its own `IntegerResize` via `crypto_bigint::Resize`).
231        Self(WithPrecision::widen_to_precision(
232            self.0,
233            at_least_bits_precision,
234        ))
235    }
236
237    fn try_resize(self, at_least_bits_precision: u32) -> Option<Self::Output> {
238        // Mirrors `crypto_bigint::Resize::try_resize`: `Some` iff the value
239        // fits in `at_least_bits_precision` bits, resized to that width.
240        let value_bits = self.0.bit_length();
241        if value_bits <= at_least_bits_precision {
242            Some(Self(WithPrecision::widen_to_precision(
243                self.0,
244                at_least_bits_precision,
245            )))
246        } else {
247            None
248        }
249    }
250}
251
252#[cfg(feature = "alloc")]
253impl<T> UnsignedModularInt for ModMathValue<T>
254where
255    T: FixedWidthUnsignedInt + PartialOrd,
256{
257    type Bytes = <T as FixedWidthUnsignedInt>::Bytes;
258
259    fn to_be_bytes(&self) -> Self::Bytes {
260        FixedWidthUnsignedInt::to_be_bytes(&self.0)
261    }
262
263    #[cfg(feature = "alloc")]
264    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
265        let bytes = self.to_be_bytes();
266        let bytes = bytes.as_ref();
267        let first_non_zero = bytes
268            .iter()
269            .position(|b| *b != 0)
270            .unwrap_or(bytes.len().saturating_sub(1));
271        bytes[first_non_zero..].to_vec().into_boxed_slice()
272    }
273
274    fn as_nz_ref(&self) -> NonZero<Self> {
275        NonZero::new(*self).expect("value is non-zero")
276    }
277
278    fn bits(&self) -> u32 {
279        FixedWidthUnsignedInt::bit_length(&self.0)
280    }
281
282    fn bits_precision(&self) -> u32 {
283        FixedWidthUnsignedInt::bits_precision(&self.0)
284    }
285}
286
287#[cfg(feature = "alloc")]
288impl<T> TryFromBeBytes for ModMathValue<T>
289where
290    T: FixedWidthUnsignedInt,
291{
292    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
293        Ok(Self(
294            <T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(bytes)?,
295        ))
296    }
297}
298
299// Opt the alloc-side newtype into raw `(public_key, d)` private-key
300// construction. The heapless-build blanket on
301// `FixedWidthUnsignedInt + PartialOrd` doesn't reach `ModMathValue<T>`
302// (a newtype, not itself `FixedWidthUnsignedInt`), so impl it here.
303#[cfg(feature = "alloc")]
304impl<T> crate::traits::keys::RawPrivateKeyConstructible for ModMathValue<T> where
305    T: FixedWidthUnsignedInt + PartialOrd
306{
307}
308
309#[cfg(not(feature = "alloc"))]
310pub type ModMathValue<T> = T;
311
312// Shared rejection-sampled `try_random_mod` body for the modmath
313// backend. Called from both the alloc-side `ModMathValue<T>` newtype
314// impl and the no-alloc `T` impl below — the only difference is the
315// wrapping function applied to the sampled `T` before the modulus
316// check.
317//
318// **Critical: mask the sampled candidate down to `modulus.bits()`
319// bits before checking.** When the modulus is `lz` bits narrower
320// than `T`'s container width, an unmasked sampler's acceptance rate
321// is ~2⁻ˡᶻ and `MAX_TRIES = 128` would exhaust almost every time.
322// After masking to `modulus.bits()` bits, we sample from `[0, 2^k)`
323// where the modulus's top bit is set, so acceptance is ≥ 50%.
324//
325// See the `TryRandomMod` trait doc for the CT-property discussion.
326#[cfg(feature = "modmath")]
327fn try_random_mod_masked<R, T, W, F>(
328    rng: &mut R,
329    modulus_bits: u32,
330    leading_zero_bits: u32,
331    modulus: &W,
332    wrap: F,
333) -> Result<W>
334where
335    R: rand_core::TryCryptoRng + ?Sized,
336    T: FixedWidthUnsignedInt,
337    W: PartialOrd,
338    F: Fn(T) -> W,
339{
340    let zero_bytes = (leading_zero_bits / 8) as usize;
341    let zero_bits_in_next = (leading_zero_bits % 8) as u8;
342
343    const MAX_TRIES: u32 = 128;
344    let mut bytes = <T as FixedWidthUnsignedInt>::Bytes::default();
345    // The `Bytes` holder is capacity-width, but the modulus operates at
346    // `modulus_bits` (its `bits_precision`, always a whole number of
347    // limbs). Sample only the trailing `window_bytes` (big-endian: the
348    // low-order bytes) so the mask (`leading_zero_bits` = the modulus's
349    // own headroom) is relative to the modulus width — acceptance is
350    // >= 50% even when the modulus is far narrower than the capacity.
351    let window_bytes = (modulus_bits as usize).div_ceil(8);
352    let lo = bytes.as_ref().len().saturating_sub(window_bytes);
353    for _ in 0..MAX_TRIES {
354        rng.try_fill_bytes(bytes.as_mut()).map_err(|_| Error::Rng)?;
355        // Big-endian within the window: mask the leading bytes of the
356        // window. `skip(lo)` / `get_mut(lo + …)` keep the accesses
357        // bounds-check-free (folded into the iterator / `Some` arm), same
358        // discipline as the panic-free audit requires elsewhere.
359        let buf = bytes.as_mut();
360        for byte in buf.iter_mut().skip(lo).take(zero_bytes) {
361            *byte = 0;
362        }
363        if zero_bits_in_next > 0 {
364            if let Some(b) = buf.get_mut(lo + zero_bytes) {
365                *b &= 0xFFu8 >> zero_bits_in_next;
366            }
367        }
368        let window = bytes.as_ref().get(lo..).ok_or(Error::Internal)?;
369        // `from_be_slice`, not the `Bytes`-holder `try_from_be_bytes_vartime`:
370        // its output width is the window length — the (public) modulus
371        // width — so on the runtime-length carrier `r` lands at the field
372        // width the blinded arithmetic needs, rather than padded to
373        // capacity. `window.len()` is derived from `modulus_bits`, never
374        // from `r`'s bytes, so `r`'s width leaks nothing. On a fixed-width
375        // carrier it's the same value zero-extended to capacity (a no-op).
376        let candidate = <T as FromByteSlice>::from_be_slice(window).map_err(|_| Error::Internal)?;
377        let wrapped = wrap(candidate);
378        if wrapped < *modulus {
379            return Ok(wrapped);
380        }
381    }
382    Err(Error::Internal)
383}
384
385#[cfg(feature = "alloc")]
386impl<T> crate::traits::modular::TryRandomMod for ModMathValue<T>
387where
388    T: FixedWidthUnsignedInt + PartialOrd,
389{
390    fn try_random_mod<R>(rng: &mut R, modulus: &Self) -> Result<Self>
391    where
392        R: rand_core::TryCryptoRng + ?Sized,
393    {
394        let container_bits = <T as FixedWidthUnsignedInt>::bits_precision(&modulus.0);
395        let leading_zero_bits = <T as FixedWidthUnsignedInt>::leading_zeros(&modulus.0);
396        if leading_zero_bits >= container_bits {
397            return Err(Error::InvalidModulus);
398        }
399        try_random_mod_masked::<R, T, _, _>(
400            rng,
401            container_bits,
402            leading_zero_bits,
403            modulus,
404            wrap_value::<T>,
405        )
406    }
407}
408
409#[cfg(not(feature = "alloc"))]
410impl<T> crate::traits::modular::TryRandomMod for T
411where
412    T: FixedWidthUnsignedInt + PartialOrd,
413{
414    fn try_random_mod<R>(rng: &mut R, modulus: &Self) -> Result<Self>
415    where
416        R: rand_core::TryCryptoRng + ?Sized,
417    {
418        let container_bits = <T as FixedWidthUnsignedInt>::bits_precision(modulus);
419        let leading_zero_bits = <T as FixedWidthUnsignedInt>::leading_zeros(modulus);
420        if leading_zero_bits >= container_bits {
421            return Err(Error::InvalidModulus);
422        }
423        try_random_mod_masked::<R, T, T, _>(rng, container_bits, leading_zero_bits, modulus, |x| x)
424    }
425}
426
427#[derive(Clone, Debug)]
428pub struct ModMathParams<T, P: Personality = Nct> {
429    // Owns the modulus + precomputed Montgomery constants. `Clone` is a
430    // trivial 4×T memcpy per modmath::Field's documented guarantee — does
431    // NOT re-run `compute_r_mod_n` / `compute_r2_mod_n`.
432    field: ModmathField<T, P>,
433    // Parallel copy of the modulus, wrapped in `Odd` for the
434    // `ModulusParams::modulus() -> &Odd<...>` trait interface. Duplicates
435    // `field.modulus()` (one extra T per params, one extra T-sized memcpy
436    // per clone) — cheap, and lets `modulus()` return a real reference
437    // instead of transmuting through `repr(transparent)`.
438    modulus_odd: Odd<ModMathValue<T>>,
439}
440
441impl<T: ModMathInt + HasPersonality<P = Nct>> ModMathParams<T, Nct> {
442    pub fn new(modulus: T) -> Result<Self> {
443        let field = ModmathField::<T, Nct>::new(modulus).ok_or(Error::InvalidModulus)?;
444        let modulus_odd = Odd::new(wrap_value(modulus)).ok_or(Error::InvalidModulus)?;
445        Ok(Self { field, modulus_odd })
446    }
447}
448
449impl<T: ModMathIntCt + HasPersonality<P = Ct>> ModMathParams<T, Ct> {
450    /// Create CT (encrypt) Montgomery parameters for an odd, non-zero
451    /// modulus.
452    pub fn new(modulus: T) -> Result<Self> {
453        let field = ModmathField::<T, Ct>::new(modulus).ok_or(Error::InvalidModulus)?;
454        let modulus_odd = Odd::new(wrap_value(modulus)).ok_or(Error::InvalidModulus)?;
455        Ok(Self { field, modulus_odd })
456    }
457}
458
459impl<T, P: Personality> ModMathParams<T, P> {
460    pub(crate) fn field(&self) -> &ModmathField<T, P> {
461        &self.field
462    }
463}
464
465/// Construct an **NCT** public key from big-endian modulus bytes and a public
466/// exponent. Use this for signature verification.
467pub fn public_key_from_be_bytes<T>(
468    modulus: &[u8],
469    exponent: u32,
470) -> Result<GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Nct>>>
471where
472    T: ModMathInt + HasPersonality<P = Nct>,
473{
474    let n = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
475        modulus,
476    )?);
477    let exponent = exponent.to_be_bytes();
478    let e = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
479        &exponent,
480    )?);
481    GenericRsaPublicKey::from_components(n, e, ModMathParams::<T, Nct>::new(unwrap_value(&n))?)
482}
483
484/// Apply the raw RSA public operation to a fixed-width block using the **NCT**
485/// (vartime) Montgomery path. Intended for signature verification.
486pub fn rsa_public_op<T>(
487    key: &GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Nct>>,
488    input: &[u8],
489) -> Result<<ModMathValue<T> as UnsignedModularInt>::Bytes>
490where
491    T: ModMathInt + HasPersonality<P = Nct>,
492{
493    let input = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
494        input,
495    )?);
496    Ok(rsa_encrypt(key, &input)?.to_be_bytes())
497}
498
499/// Construct a **CT** public key. Use this when the resulting key will feed
500/// PKCS#1 v1.5 / OAEP encryption (or any other path where the plaintext is
501/// secret). `T` must be a Ct-typed FixedUInt; the bound is enforced by the
502/// `CiosMontMulCt` requirement inside [`ModMathIntCt`].
503pub fn public_key_ct_from_be_bytes<T>(
504    modulus: &[u8],
505    exponent: u32,
506) -> Result<GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Ct>>>
507where
508    T: ModMathIntCt + HasPersonality<P = Ct>,
509{
510    let n = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
511        modulus,
512    )?);
513    let exponent = exponent.to_be_bytes();
514    let e = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
515        &exponent,
516    )?);
517    GenericRsaPublicKey::from_components(n, e, ModMathParams::<T, Ct>::new(unwrap_value(&n))?)
518}
519
520pub fn rsa_public_op_ct<T>(
521    key: &GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Ct>>,
522    input: &[u8],
523) -> Result<<ModMathValue<T> as UnsignedModularInt>::Bytes>
524where
525    T: ModMathIntCt + HasPersonality<P = Ct>,
526{
527    let input = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
528        input,
529    )?);
530    Ok(rsa_encrypt(key, &input)?.to_be_bytes())
531}
532
533// `T: Zeroize` (not just `Clone`) is locked in to satisfy `Drop` coherence
534// below — loosening it silently disables the auto-wipe.
535#[derive(Clone, Debug)]
536pub struct ModMathForm<T, P: Personality = Nct>
537where
538    T: Clone + Zeroize,
539{
540    integer_mont: ModMathValue<T>,
541    params: ModMathParams<T, P>,
542}
543
544// `integer_mont` is secret-derived Montgomery state; `params` is public.
545impl<T, P: Personality> Zeroize for ModMathForm<T, P>
546where
547    T: Clone + Zeroize,
548{
549    fn zeroize(&mut self) {
550        self.integer_mont.zeroize();
551    }
552}
553
554impl<T, P: Personality> Drop for ModMathForm<T, P>
555where
556    T: Clone + Zeroize,
557{
558    fn drop(&mut self) {
559        self.zeroize();
560    }
561}
562
563impl<T, P: Personality> zeroize::ZeroizeOnDrop for ModMathForm<T, P> where T: Clone + Zeroize {}
564
565impl<T: ModMathInt + HasPersonality<P = Nct>> IntoMontyForm<ModMathParams<T, Nct>>
566    for ModMathForm<T, Nct>
567{
568    fn from_reduced(integer: ModMathValue<T>, params: &ModMathParams<T, Nct>) -> Self {
569        let field = params.field();
570        let r = field.reduce(unwrap_value_ref(&integer));
571        Self {
572            integer_mont: wrap_value(*r.mont_value()),
573            params: params.clone(),
574        }
575    }
576
577    /// `Field::reduce` is `raw * R² mod modulus` via CIOS — well-defined for
578    /// any `raw < R = 2^W`. Same body as `from_reduced` because the
579    /// underlying primitive already handles unreduced input.
580    fn from_value(integer: ModMathValue<T>, params: &ModMathParams<T, Nct>) -> Self {
581        Self::from_reduced(integer, params)
582    }
583}
584
585impl<T: ModMathInt + HasPersonality<P = Nct>> ModMathForm<T, Nct> {
586    fn pow_loop(&self, exp_raw: T) -> T {
587        let field = self.params.field();
588        let base = field.residue_from_mont(unwrap_value(&self.integer_mont));
589        *field.exp(&base, &exp_raw).mont_value()
590    }
591
592    fn to_reduced(&self) -> T {
593        let field = self.params.field();
594        let r = field.residue_from_mont(unwrap_value(&self.integer_mont));
595        field.into_raw(&r)
596    }
597}
598
599impl<T: ModMathInt + HasPersonality<P = Nct>> Pow<ModMathParams<T, Nct>> for ModMathForm<T, Nct> {
600    fn pow(&self, exp: &ModMathValue<T>) -> Self {
601        let result_mont = self.pow_loop(unwrap_value(exp));
602        Self {
603            integer_mont: wrap_value(result_mont),
604            params: self.params.clone(),
605        }
606    }
607}
608
609impl<T: ModMathInt + HasPersonality<P = Nct>> PowBoundedExp<ModMathParams<T, Nct>>
610    for ModMathForm<T, Nct>
611{
612    fn pow_bounded_exp(&self, exp: &ModMathValue<T>, _exp_bits: u32) -> Self {
613        // The LSB-first loop exits naturally when the exponent reaches zero,
614        // so the `_exp_bits` hint is unused here.
615        let result_mont = self.pow_loop(unwrap_value(exp));
616        Self {
617            integer_mont: wrap_value(result_mont),
618            params: self.params.clone(),
619        }
620    }
621
622    fn retrieve(&self) -> ModMathValue<T> {
623        wrap_value(self.to_reduced())
624    }
625}
626
627impl<T: ModMathInt + HasPersonality<P = Nct>> ModulusParams for ModMathParams<T, Nct> {
628    type Modulus = ModMathValue<T>;
629    type MontgomeryForm = ModMathForm<T, Nct>;
630
631    fn modulus(&self) -> &Odd<Self::Modulus> {
632        &self.modulus_odd
633    }
634
635    fn bits_precision(&self) -> u32 {
636        FixedWidthUnsignedInt::bits_precision(self.field.modulus())
637    }
638}
639
640impl<T: ModMathIntCt + HasPersonality<P = Ct>> IntoMontyForm<ModMathParams<T, Ct>>
641    for ModMathForm<T, Ct>
642{
643    fn from_reduced(integer: ModMathValue<T>, params: &ModMathParams<T, Ct>) -> Self {
644        let field = params.field();
645        let r = field.reduce(unwrap_value_ref(&integer));
646        Self {
647            integer_mont: wrap_value(*r.mont_value()),
648            params: params.clone(),
649        }
650    }
651
652    /// Same as the Nct variant: `FieldCt::reduce` uses `wide_montgomery_mul_ct`
653    /// with `R² mod modulus`, which handles arbitrary `raw < R = 2^W`.
654    fn from_value(integer: ModMathValue<T>, params: &ModMathParams<T, Ct>) -> Self {
655        Self::from_reduced(integer, params)
656    }
657}
658
659impl<T: ModMathIntCt + HasPersonality<P = Ct>> ModMathForm<T, Ct> {
660    // Secret-exponent ladder. Used by `Pow::pow`, which is the path RSA
661    // signing and unblinded decryption reduce to — the exponent is `d`,
662    // never disclosed in timing. Routes to modmath's `Field<T, Ct>::exp`,
663    // a fixed-iteration Montgomery ladder with branchless per-bit select.
664    fn pow_loop_ct(&self, exp_raw: T) -> T {
665        let field = self.params.field();
666        let base = field.residue_from_mont(unwrap_value(&self.integer_mont));
667        *field.exp(&base, &exp_raw).mont_value()
668    }
669
670    // Public-exponent ladder. Used by `PowBoundedExp::pow_bounded_exp`,
671    // which acknowledges variable-time-in-exponent semantics — the
672    // exponent is `e` (RSA public verify/encrypt), already disclosed.
673    // Routes to modmath's `Field<T, Ct>::exp_public_exp`.
674    fn pow_loop_public_exp(&self, exp_raw: T) -> T {
675        let field = self.params.field();
676        let base = field.residue_from_mont(unwrap_value(&self.integer_mont));
677        *field.exp_public_exp(&base, &exp_raw).mont_value()
678    }
679
680    fn to_reduced(&self) -> T {
681        let field = self.params.field();
682        let r = field.residue_from_mont(unwrap_value(&self.integer_mont));
683        field.into_raw(&r)
684    }
685}
686
687impl<T: ModMathIntCt + HasPersonality<P = Ct>> Pow<ModMathParams<T, Ct>> for ModMathForm<T, Ct> {
688    fn pow(&self, exp: &ModMathValue<T>) -> Self {
689        let result_mont = self.pow_loop_ct(unwrap_value(exp));
690        Self {
691            integer_mont: wrap_value(result_mont),
692            params: self.params.clone(),
693        }
694    }
695}
696
697impl<T: ModMathIntCt + HasPersonality<P = Ct>> PowBoundedExp<ModMathParams<T, Ct>>
698    for ModMathForm<T, Ct>
699{
700    fn pow_bounded_exp(&self, exp: &ModMathValue<T>, _exp_bits: u32) -> Self {
701        let result_mont = self.pow_loop_public_exp(unwrap_value(exp));
702        Self {
703            integer_mont: wrap_value(result_mont),
704            params: self.params.clone(),
705        }
706    }
707
708    fn retrieve(&self) -> ModMathValue<T> {
709        wrap_value(self.to_reduced())
710    }
711}
712
713// CT modular inverse for RSA-blinding on the modmath backend. Routes
714// to modmath's `Field<T, Ct>::inv_safegcd_ct` (Bernstein-Yang). The
715// modulus may fill the carrier's full width; `None` means the value
716// is not coprime with `n` (astronomically rare, retryable).
717impl<T> crate::traits::modular::InvertCt<ModMathParams<T, Ct>> for ModMathForm<T, Ct>
718where
719    T: ModMathIntCt
720        + HasPersonality<P = Ct>
721        + modmath_cios::CiosRowOps
722        + core::ops::BitOr<Output = T>,
723    <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
724{
725    fn invert_ct(&self) -> Option<Self> {
726        let field = self.params.field();
727        let residue = field.residue_from_mont(unwrap_value(&self.integer_mont));
728        let ct_option = field.inv_safegcd_ct(&residue);
729        ct_option.into_option().map(|inv_res| Self {
730            integer_mont: wrap_value(*inv_res.mont_value()),
731            params: self.params.clone(),
732        })
733    }
734}
735
736// CT Montgomery multiplication. Both operands share this
737// `ModMathParams` (invariant, not type-checked). Routes to modmath's
738// `Field<T, Ct>::mul` — the CIOS-Ct primitive, branchless in both
739// inputs.
740impl<T: ModMathIntCt + HasPersonality<P = Ct>> crate::traits::modular::MulCt<ModMathParams<T, Ct>>
741    for ModMathForm<T, Ct>
742{
743    fn mul_ct(&self, rhs: &Self) -> Self {
744        // Guard: MulCt's precondition is that both operands share the
745        // same modulus. `debug_assert_eq!` would need `T: Debug` for
746        // the failure message; use `debug_assert!` with a fixed
747        // message to avoid widening the trait bound just for a
748        // debug-only check.
749        debug_assert!(
750            self.params.modulus_odd == rhs.params.modulus_odd,
751            "MulCt operands must share the same modulus"
752        );
753        let field = self.params.field();
754        let lhs_res = field.residue_from_mont(unwrap_value(&self.integer_mont));
755        let rhs_res = field.residue_from_mont(unwrap_value(&rhs.integer_mont));
756        let product = field.mul(&lhs_res, &rhs_res);
757        Self {
758            integer_mont: wrap_value(*product.mont_value()),
759            params: self.params.clone(),
760        }
761    }
762}
763
764impl<T: ModMathIntCt + HasPersonality<P = Ct>> ModulusParams for ModMathParams<T, Ct> {
765    type Modulus = ModMathValue<T>;
766    type MontgomeryForm = ModMathForm<T, Ct>;
767
768    fn modulus(&self) -> &Odd<Self::Modulus> {
769        &self.modulus_odd
770    }
771
772    fn bits_precision(&self) -> u32 {
773        FixedWidthUnsignedInt::bits_precision(self.field.modulus())
774    }
775}
776
777// Opt the Ct personality into the CT-encrypt gate. Deliberately no
778// impl for `ModMathParams<T, Nct>` — Nct exponentiation is vartime in
779// the base, so `NctPublicKey`-derived encrypting keys fail the encrypt
780// trait bound at compile time. See
781// `crate::traits::modular::CtModulusParams`.
782impl<T: ModMathIntCt + HasPersonality<P = Ct>> crate::traits::modular::sealed::CtModulusParamsSealed
783    for ModMathParams<T, Ct>
784{
785}
786impl<T: ModMathIntCt + HasPersonality<P = Ct>> crate::traits::modular::CtModulusParams
787    for ModMathParams<T, Ct>
788{
789}
790
791#[cfg(test)]
792#[cfg(feature = "alloc")]
793mod tests {
794    use const_num_traits::Ct;
795    use fixed_bigint::FixedUInt;
796    use rand::rngs::ChaCha8Rng;
797    use rand_core::SeedableRng;
798    use sha1::Sha1;
799    use signature::hazmat::PrehashVerifier;
800
801    use super::{
802        public_key_ct_from_be_bytes, public_key_from_be_bytes, ModMathForm, ModMathParams,
803        ModMathValue,
804    };
805    use crate::key::GenericRsaPublicKey;
806    use crate::pkcs1v15::{GenericEncryptingKey, GenericSignature, GenericVerifyingKey};
807    use crate::{traits::RandomizedEncryptor, BoxedUint, Pkcs1v15Encrypt, RsaPublicKey};
808
809    type SmallU = FixedUInt<u8, 64>;
810    type SmallUCt = FixedUInt<u8, 64, Ct>;
811
812    #[test]
813    fn brand_round_trip() {
814        let params = ModMathParams::<SmallU>::new(SmallU::from(13u8)).unwrap();
815        let f = params.field();
816        let r = f.reduce(&SmallU::from(7u8));
817        assert_eq!(f.into_raw(&r), SmallU::from(7u8));
818    }
819
820    #[test]
821    fn brand_mul_exp() {
822        let params = ModMathParams::<SmallU>::new(SmallU::from(13u8)).unwrap();
823        let f = params.field();
824        // 7 * 11 = 77 ≡ 12 (mod 13)
825        let a = f.reduce(&SmallU::from(7u8));
826        let b = f.reduce(&SmallU::from(11u8));
827        assert_eq!(f.into_raw(&f.mul(&a, &b)), SmallU::from(12u8));
828        // 2^10 = 1024 ≡ 10 (mod 13)
829        let base = f.reduce(&SmallU::from(2u8));
830        assert_eq!(
831            f.into_raw(&f.exp(&base, &SmallU::from(10u8))),
832            SmallU::from(10u8)
833        );
834    }
835
836    #[test]
837    fn brand_ct_matches_nct() {
838        let p_nct = ModMathParams::<SmallU>::new(SmallU::from(13u8)).unwrap();
839        let p_ct = ModMathParams::<SmallUCt, Ct>::new(SmallUCt::from(13u8)).unwrap();
840        let f_nct = p_nct.field();
841        let f_ct = p_ct.field();
842        let nct = f_nct.into_raw(&f_nct.mul(
843            &f_nct.reduce(&SmallU::from(7u8)),
844            &f_nct.reduce(&SmallU::from(11u8)),
845        ));
846        let ct = f_ct.into_raw(&f_ct.mul(
847            &f_ct.reduce(&SmallUCt::from(7u8)),
848            &f_ct.reduce(&SmallUCt::from(11u8)),
849        ));
850        // Distinct types — compare via underlying byte representation.
851        let mut nct_bytes = [0u8; 64];
852        let mut ct_bytes = [0u8; 64];
853        let _ = nct.to_be_bytes(&mut nct_bytes);
854        let _ = ct.to_be_bytes(&mut ct_bytes);
855        assert_eq!(nct_bytes, ct_bytes);
856    }
857
858    #[test]
859    fn mod_math_form_zeroize_on_drop() {
860        fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
861        assert_zeroize_on_drop::<ModMathForm<SmallU>>();
862        assert_zeroize_on_drop::<ModMathForm<SmallUCt, Ct>>();
863    }
864
865    #[test]
866    fn verify_pkcs1v15_signature_with_modmath_fixed_uint() {
867        type U512 = FixedUInt<u8, 64>;
868
869        let digest: [u8; 20] = [
870            0x43, 0x0c, 0xe3, 0x4d, 0x02, 0x07, 0x24, 0xed, 0x75, 0xa1, 0x96, 0xdf, 0xc2, 0xad,
871            0x67, 0xc7, 0x77, 0x72, 0xd1, 0x69,
872        ];
873        let modulus: [u8; 64] = [
874            0x96, 0x9D, 0x03, 0xFF, 0xA9, 0x8D, 0x88, 0x8F, 0x3A, 0xA4, 0xF2, 0xFE, 0xD2, 0x32,
875            0xE6, 0x1C, 0x4A, 0xCF, 0x06, 0x63, 0xA9, 0x2F, 0x99, 0x03, 0x4C, 0xF7, 0xB7, 0x24,
876            0x5A, 0x1A, 0x1E, 0x5E, 0xAF, 0xA5, 0x65, 0xAF, 0xB9, 0x0B, 0xAB, 0x22, 0x85, 0x71,
877            0x2F, 0xAA, 0x50, 0x39, 0x39, 0xA0, 0x65, 0xFB, 0x60, 0xDD, 0x08, 0x28, 0xA3, 0x84,
878            0xF2, 0x6D, 0x8A, 0xFC, 0x28, 0x6D, 0xF6, 0xCF,
879        ];
880        let signature: [u8; 64] = [
881            0x45, 0x53, 0xF3, 0xAF, 0x16, 0xAF, 0x63, 0x97, 0xB0, 0xD3, 0x2F, 0x8A, 0xEC, 0xD5,
882            0x4C, 0xF1, 0xF3, 0xD0, 0x0C, 0x9F, 0x42, 0xDC, 0x68, 0xCB, 0xD7, 0x05, 0xCE, 0xA5,
883            0xA9, 0x70, 0x95, 0x3E, 0xC0, 0xBC, 0x4A, 0x18, 0xED, 0x91, 0xA3, 0x5D, 0x66, 0xEC,
884            0xDA, 0x4A, 0x83, 0x32, 0xCF, 0xC3, 0xA3, 0xAB, 0x21, 0xAD, 0x59, 0xB2, 0x2E, 0x87,
885            0xC2, 0x73, 0xFF, 0x08, 0x88, 0xDD, 0x4D, 0xE0,
886        ];
887
888        let key = public_key_from_be_bytes::<U512>(&modulus, 3).unwrap();
889        let verifying_key = GenericVerifyingKey::<Sha1, _, _>::new(key);
890        let signature =
891            GenericSignature::from(ModMathValue::from_inner(U512::from_be_bytes(&signature)));
892        verifying_key.verify_prehash(&digest, &signature).unwrap();
893    }
894
895    #[test]
896    fn verify_pkcs1v15_signature_with_modmath_fixed_uint32() {
897        type U512 = FixedUInt<u32, 16>;
898
899        let digest: [u8; 20] = [
900            0x43, 0x0c, 0xe3, 0x4d, 0x02, 0x07, 0x24, 0xed, 0x75, 0xa1, 0x96, 0xdf, 0xc2, 0xad,
901            0x67, 0xc7, 0x77, 0x72, 0xd1, 0x69,
902        ];
903        let modulus: [u8; 64] = [
904            0x96, 0x9D, 0x03, 0xFF, 0xA9, 0x8D, 0x88, 0x8F, 0x3A, 0xA4, 0xF2, 0xFE, 0xD2, 0x32,
905            0xE6, 0x1C, 0x4A, 0xCF, 0x06, 0x63, 0xA9, 0x2F, 0x99, 0x03, 0x4C, 0xF7, 0xB7, 0x24,
906            0x5A, 0x1A, 0x1E, 0x5E, 0xAF, 0xA5, 0x65, 0xAF, 0xB9, 0x0B, 0xAB, 0x22, 0x85, 0x71,
907            0x2F, 0xAA, 0x50, 0x39, 0x39, 0xA0, 0x65, 0xFB, 0x60, 0xDD, 0x08, 0x28, 0xA3, 0x84,
908            0xF2, 0x6D, 0x8A, 0xFC, 0x28, 0x6D, 0xF6, 0xCF,
909        ];
910        let signature: [u8; 64] = [
911            0x45, 0x53, 0xF3, 0xAF, 0x16, 0xAF, 0x63, 0x97, 0xB0, 0xD3, 0x2F, 0x8A, 0xEC, 0xD5,
912            0x4C, 0xF1, 0xF3, 0xD0, 0x0C, 0x9F, 0x42, 0xDC, 0x68, 0xCB, 0xD7, 0x05, 0xCE, 0xA5,
913            0xA9, 0x70, 0x95, 0x3E, 0xC0, 0xBC, 0x4A, 0x18, 0xED, 0x91, 0xA3, 0x5D, 0x66, 0xEC,
914            0xDA, 0x4A, 0x83, 0x32, 0xCF, 0xC3, 0xA3, 0xAB, 0x21, 0xAD, 0x59, 0xB2, 0x2E, 0x87,
915            0xC2, 0x73, 0xFF, 0x08, 0x88, 0xDD, 0x4D, 0xE0,
916        ];
917
918        let n = U512::from_be_bytes(&modulus);
919        let e = U512::from(3u8);
920        // Turbofish the personality: `ModMathParams::new` is ambiguous
921        // between the Nct and Ct impl blocks (the `P = Nct` default doesn't
922        // fire in inference contexts). Pin Nct explicitly.
923        let key = GenericRsaPublicKey::from_components(
924            ModMathValue::from_inner(n),
925            ModMathValue::from_inner(e),
926            ModMathParams::<U512, const_num_traits::Nct>::new(n).unwrap(),
927        )
928        .unwrap();
929        let verifying_key = GenericVerifyingKey::<Sha1, _, _>::new(key);
930        let signature =
931            GenericSignature::from(ModMathValue::from_inner(U512::from_be_bytes(&signature)));
932        verifying_key.verify_prehash(&digest, &signature).unwrap();
933    }
934
935    #[test]
936    fn encrypt_pkcs1v15_with_modmath_fixed_uint_matches_boxeduint() {
937        // Encrypt path takes a secret plaintext, so type the modulus as
938        // Ct-personality — `CiosMontMulCt` only resolves for Ct-typed
939        // FixedUInts under the personality typestate.
940        type U512 = FixedUInt<u8, 64, Ct>;
941
942        let modulus: [u8; 64] = [
943            0x96, 0x9D, 0x03, 0xFF, 0xA9, 0x8D, 0x88, 0x8F, 0x3A, 0xA4, 0xF2, 0xFE, 0xD2, 0x32,
944            0xE6, 0x1C, 0x4A, 0xCF, 0x06, 0x63, 0xA9, 0x2F, 0x99, 0x03, 0x4C, 0xF7, 0xB7, 0x24,
945            0x5A, 0x1A, 0x1E, 0x5E, 0xAF, 0xA5, 0x65, 0xAF, 0xB9, 0x0B, 0xAB, 0x22, 0x85, 0x71,
946            0x2F, 0xAA, 0x50, 0x39, 0x39, 0xA0, 0x65, 0xFB, 0x60, 0xDD, 0x08, 0x28, 0xA3, 0x84,
947            0xF2, 0x6D, 0x8A, 0xFC, 0x28, 0x6D, 0xF6, 0xCF,
948        ];
949        let msg = b"hello world!";
950
951        let modmath_key = public_key_ct_from_be_bytes::<U512>(&modulus, 3).unwrap();
952        let boxed_key = RsaPublicKey::new(
953            BoxedUint::from_be_slice(&modulus, 512).unwrap(),
954            3u64.into(),
955        )
956        .unwrap();
957
958        let mut modmath_rng = ChaCha8Rng::from_seed([42; 32]);
959        let mut boxed_rng = ChaCha8Rng::from_seed([42; 32]);
960        let mut storage = [0u8; 64];
961
962        let modmath_ciphertext = GenericEncryptingKey::new(modmath_key)
963            .encrypt_with_rng_into(&mut modmath_rng, msg, &mut storage)
964            .unwrap();
965        let boxed_ciphertext = boxed_key
966            .encrypt(&mut boxed_rng, Pkcs1v15Encrypt, msg)
967            .unwrap();
968
969        assert_eq!(modmath_ciphertext, boxed_ciphertext.as_slice());
970    }
971}
972
973// Tests for the `rsa_private_op` primitive on the heapless / Ct path.
974// Gated independently of the alloc block above so they compile and
975// run in no_alloc mode.
976#[cfg(test)]
977mod private_op_tests {
978    use super::*;
979    use const_num_traits::Ct;
980    use fixed_bigint::FixedUInt;
981
982    type SmallUCt = FixedUInt<u8, 64, Ct>;
983
984    // ── Carrier toy tests ──────────────────────────────────────────
985    //
986    // Most tests are written as a `fn inner<T>()` and instantiated for
987    // both carriers: `SmallUCt` is the fixed-width reference, `HeaplessCt`
988    // the runtime-length carrier. The sub-capacity tests further down are
989    // `HeaplessCt`-specific — only a runtime-length carrier can hold a
990    // modulus narrower than its capacity.
991    type HeaplessCt = fixed_bigint::HeaplessBigInt<u8, 64, Ct>;
992
993    // The full carrier bound for the CT sign+blind path: enough for
994    // `ModMathParams<T, Ct>` and its `MontgomeryForm` to satisfy
995    // `Pow + PowBoundedExp + InvertCt + MulCt`, and for `T` /
996    // `ModMathValue<T>` to satisfy `TryRandomMod`.
997    trait TestCt:
998        super::ModMathIntCt
999        + HasPersonality<P = Ct>
1000        + modmath_cios::CiosRowOps
1001        + core::ops::BitOr<Output = Self>
1002        + core::fmt::Debug
1003    where
1004        <Self as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1005    {
1006    }
1007    impl<T> TestCt for T
1008    where
1009        T: super::ModMathIntCt
1010            + HasPersonality<P = Ct>
1011            + modmath_cios::CiosRowOps
1012            + core::ops::BitOr<Output = T>
1013            + core::fmt::Debug,
1014        <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1015    {
1016    }
1017
1018    // n = 35 = 5 · 7, φ(n) = 24. e = 5, d = 29 (since 5·29 = 145 ≡ 1 mod 24).
1019    // m = 2 → c = 2^5 mod 35 = 32 → m_recovered = 32^29 mod 35 = 2.
1020    fn toy_params<T: TestCt>() -> ModMathParams<T, Ct>
1021    where
1022        <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1023    {
1024        ModMathParams::<T, Ct>::new(
1025            <T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&[35]).unwrap(),
1026        )
1027        .unwrap()
1028    }
1029
1030    // A 512-bit odd modulus used by the `sign_into` defensive-error
1031    // tests below — they need the actual modulus bit-length (which
1032    // `sign_into` checks) to match `SMALL_K * 8`
1033    // so `k` passes the up-front width check and the specific error
1034    // path (small buffer, wrong hash length, etc.) is what fires.
1035    // Value is `2^511 + 1`: MSB set, LSB=1 (odd). (`FixedUInt<u8, 64>`
1036    // only — this shape drives the fixed-width defensive checks.)
1037    fn toy_params_wide() -> ModMathParams<SmallUCt, Ct> {
1038        let mut bytes = [0u8; 64];
1039        bytes[0] = 0x80;
1040        bytes[63] = 0x01;
1041        let n = <SmallUCt as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&bytes).unwrap();
1042        ModMathParams::<SmallUCt, Ct>::new(n).unwrap()
1043    }
1044
1045    #[test]
1046    fn rsa_private_op_round_trip() {
1047        fn inner<T: TestCt>()
1048        where
1049            <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1050        {
1051            let n_params = toy_params::<T>();
1052            let c = wrap_value(T::from(32u8));
1053            let d = wrap_value(T::from(29u8));
1054            let expected = wrap_value(T::from(2u8));
1055            let recovered = crate::algorithms::rsa::rsa_private_op(&c, &d, &n_params);
1056            assert_eq!(recovered, expected);
1057        }
1058        inner::<SmallUCt>();
1059        inner::<HeaplessCt>();
1060    }
1061
1062    // Blinded RSA private op must produce the same plaintext as the
1063    // unblinded op, regardless of the caller-supplied `r`. Toy modulus
1064    // n = 35, e = 5, d = 29, c = 32; expected m = 2. r = 6 (coprime
1065    // with 35). The blinded body should recover m = 2 the same way
1066    // rsa_private_op does.
1067    fn blinded_matches_unblinded_inner<T: TestCt>()
1068    where
1069        <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1070    {
1071        let n_params = toy_params::<T>();
1072        let c = wrap_value(T::from(32u8));
1073        let d = wrap_value(T::from(29u8));
1074        let e = wrap_value(T::from(5u8));
1075        let r = wrap_value(T::from(6u8));
1076        let expected = wrap_value(T::from(2u8));
1077        let recovered =
1078            crate::algorithms::rsa::rsa_private_op_blinded(&r, &c, &d, &e, &n_params).unwrap();
1079        assert_eq!(recovered, expected);
1080    }
1081    #[test]
1082    fn rsa_private_op_blinded_matches_unblinded() {
1083        blinded_matches_unblinded_inner::<SmallUCt>();
1084        blinded_matches_unblinded_inner::<HeaplessCt>();
1085    }
1086
1087    // Blinded op must fail if `r` shares a factor with `n` — the inverse
1088    // doesn't exist, `invert_ct` returns None, and the primitive returns
1089    // Err. Toy: n = 35 = 5·7, r = 5 (shares factor with n). No retry at
1090    // the primitive level — caller policy. Runs on both carriers: 5 is
1091    // genuinely non-invertible mod 35 on each, so `invert_ct` returns
1092    // None for the *right* reason (not vacuously).
1093    #[test]
1094    fn rsa_private_op_blinded_rejects_non_coprime_r() {
1095        fn inner<T: TestCt>()
1096        where
1097            <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1098        {
1099            let n_params = toy_params::<T>();
1100            let c = wrap_value(T::from(32u8));
1101            let d = wrap_value(T::from(29u8));
1102            let e = wrap_value(T::from(5u8));
1103            let r_bad = wrap_value(T::from(5u8)); // shares factor 5 with n = 35
1104            let result =
1105                crate::algorithms::rsa::rsa_private_op_blinded(&r_bad, &c, &d, &e, &n_params);
1106            assert!(result.is_err());
1107        }
1108        inner::<SmallUCt>();
1109        inner::<HeaplessCt>();
1110    }
1111
1112    // Full-stack blinded op with RNG-driven `r` sampling. Same toy
1113    // setup as the unblinded round-trip; the wrapper samples r via
1114    // TryRandomMod, retries on non-coprime, then verifies m^e ≡ c
1115    // before returning. For toy n=35, non-coprime probability per
1116    // draw is ~31% — the 10-retry cap gives failure prob ~8e-6, so
1117    // the test is reliable.
1118    fn and_check_blinded_round_trip_inner<T: TestCt>()
1119    where
1120        <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1121    {
1122        use rand::rngs::ChaCha8Rng;
1123        use rand_core::SeedableRng;
1124
1125        let n_params = toy_params::<T>();
1126        let c = wrap_value(T::from(32u8));
1127        let d = wrap_value(T::from(29u8));
1128        let e = wrap_value(T::from(5u8));
1129        let expected = wrap_value(T::from(2u8));
1130        let mut rng = ChaCha8Rng::from_seed([42; 32]);
1131        let recovered = crate::algorithms::rsa::rsa_private_op_and_check_blinded(
1132            &mut rng, &c, &d, &e, &n_params,
1133        )
1134        .unwrap();
1135        assert_eq!(recovered, expected);
1136    }
1137    #[test]
1138    fn rsa_private_op_and_check_blinded_round_trip() {
1139        and_check_blinded_round_trip_inner::<SmallUCt>();
1140        and_check_blinded_round_trip_inner::<HeaplessCt>();
1141    }
1142
1143    // A 512-bit modulus (`2^511 + 1`, top bit set) so acceptance is
1144    // essentially 50% and the 128-try cap isn't exhausted. Sampler-only
1145    // path — no inverse — so it runs on both carriers.
1146    fn try_random_mod_below_modulus_inner<T: TestCt>(n: ModMathValue<T>)
1147    where
1148        <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1149    {
1150        use crate::traits::modular::TryRandomMod;
1151        use rand::rngs::ChaCha8Rng;
1152        use rand_core::SeedableRng;
1153
1154        let mut rng = ChaCha8Rng::from_seed([42; 32]);
1155        let mut samples = [ModMathValue::<T>::from(0u8); 16];
1156        for slot in samples.iter_mut() {
1157            let r = ModMathValue::<T>::try_random_mod(&mut rng, &n).unwrap();
1158            assert!(r < n, "sample must be < modulus");
1159            *slot = r;
1160        }
1161        let first = samples[0];
1162        assert!(
1163            samples.iter().any(|s| *s != first),
1164            "samples are trivially all equal — RNG or sampler broken"
1165        );
1166    }
1167    #[test]
1168    fn try_random_mod_modmath_stays_below_modulus() {
1169        // 2^511 + 1 in each carrier.
1170        let mut bytes = [0u8; 64];
1171        bytes[0] = 0x80;
1172        bytes[63] = 0x01;
1173        let nf = <SmallUCt as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&bytes).unwrap();
1174        try_random_mod_below_modulus_inner::<SmallUCt>(wrap_value(nf));
1175        let nh = <HeaplessCt as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&bytes).unwrap();
1176        try_random_mod_below_modulus_inner::<HeaplessCt>(wrap_value(nh));
1177    }
1178
1179    // An unmasked sampler's acceptance rate against a modulus whose
1180    // value is many bits narrower than its width is ~2⁻ˡᶻ, blowing the
1181    // 128-tries cap. Masking must let sampling succeed even when the
1182    // value occupies only ~6 bits of a full-width modulus — this is
1183    // `toy_params()` (n = 35, built at full carrier width `len == CAP`),
1184    // so here `bits_precision` equals the sample-buffer width and the
1185    // ~6-bit *value* is the narrow quantity the mask brings the candidate
1186    // down to. The complementary case — a modulus narrow in *width*
1187    // (`len < CAP`), where the window is a strict sub-slice of the buffer
1188    // — is `try_random_mod_accepts_narrow_modulus`.
1189    #[test]
1190    fn try_random_mod_modmath_succeeds_on_narrow_modulus_wide_carrier() {
1191        fn inner<T: TestCt>()
1192        where
1193            <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1194        {
1195            use crate::traits::modular::TryRandomMod;
1196            use rand::rngs::ChaCha8Rng;
1197            use rand_core::SeedableRng;
1198
1199            let n_params = toy_params::<T>();
1200            let n = *n_params.modulus().as_ref();
1201            let mut rng = ChaCha8Rng::from_seed([42; 32]);
1202            for _ in 0..64 {
1203                let r = ModMathValue::<T>::try_random_mod(&mut rng, &n).unwrap();
1204                assert!(r < n);
1205            }
1206        }
1207        inner::<SmallUCt>();
1208        inner::<HeaplessCt>();
1209    }
1210
1211    #[test]
1212    fn rsa_private_op_and_check_round_trip() {
1213        fn inner<T: TestCt>()
1214        where
1215            <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1216        {
1217            let n_params = toy_params::<T>();
1218            let c = wrap_value(T::from(32u8));
1219            let d = wrap_value(T::from(29u8));
1220            let e = wrap_value(T::from(5u8));
1221            let expected = wrap_value(T::from(2u8));
1222            let recovered =
1223                crate::algorithms::rsa::rsa_private_op_and_check(&c, &d, &e, &n_params).unwrap();
1224            assert_eq!(recovered, expected);
1225        }
1226        inner::<SmallUCt>();
1227        inner::<HeaplessCt>();
1228    }
1229
1230    // Verify the `InvertCt` primitive on the modmath backend against
1231    // a known-answer inverse. n = 35, 3⁻¹ mod 35 = 12 (since 3·12 = 36 ≡ 1).
1232    // Exercises the modmath `Field::inv_safegcd_ct` bridge.
1233    fn invert_ct_known_answer_inner<T: TestCt>()
1234    where
1235        <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1236    {
1237        use crate::traits::modular::{IntoMontyForm, InvertCt, PowBoundedExp};
1238        let n_params = toy_params::<T>();
1239        let three = wrap_value(T::from(3u8));
1240        let mont_three = ModMathForm::<T, Ct>::from_reduced(three, &n_params);
1241        let mont_inv = mont_three.invert_ct().expect("3 is coprime to 35");
1242        let recovered = PowBoundedExp::<ModMathParams<T, Ct>>::retrieve(&mont_inv);
1243        assert_eq!(recovered, wrap_value(T::from(12u8)));
1244    }
1245    // `invert_ct` works on both carriers at every width — the toy `n = 35`
1246    // here, and the 2048-bit full-CAP deployment modulus end-to-end in the
1247    // `heapless_bigint_smoke` blinded-sign test.
1248    #[test]
1249    fn invert_ct_modmath_known_answer() {
1250        invert_ct_known_answer_inner::<SmallUCt>();
1251        invert_ct_known_answer_inner::<HeaplessCt>();
1252    }
1253
1254    // Verify the `MulCt` primitive on the modmath backend against a
1255    // known-answer product. n = 35, 3·12 = 36 ≡ 1 (mod 35). Exercises
1256    // the modmath `Field::mul` bridge; also completes the round-trip
1257    // with `InvertCt` — inverting 3 and multiplying back gives 1.
1258    fn mul_ct_inverse_round_trip_inner<T: TestCt>()
1259    where
1260        <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1261    {
1262        use crate::traits::modular::{IntoMontyForm, InvertCt, MulCt, PowBoundedExp};
1263        let n_params = toy_params::<T>();
1264        let three = wrap_value(T::from(3u8));
1265        let mont_three = ModMathForm::<T, Ct>::from_reduced(three, &n_params);
1266        let mont_inv = mont_three.invert_ct().expect("3 is coprime to 35");
1267        let product = mont_three.mul_ct(&mont_inv);
1268        let recovered = PowBoundedExp::<ModMathParams<T, Ct>>::retrieve(&product);
1269        assert_eq!(recovered, wrap_value(T::from(1u8)));
1270    }
1271    #[test]
1272    fn mul_ct_modmath_inverse_round_trip() {
1273        mul_ct_inverse_round_trip_inner::<SmallUCt>();
1274        mul_ct_inverse_round_trip_inner::<HeaplessCt>();
1275    }
1276
1277    // ── Sub-capacity operation: a runtime-length modulus narrower than
1278    // the carrier capacity (`len < CAP`). This is the deployment target
1279    // for one binary serving multiple key sizes — e.g. a 4096-bit-CAP
1280    // `HeaplessBigInt` operating on 1024/2048-bit keys at their own width
1281    // rather than padded to capacity. `HeaplessCt` is `u8 × 64` (512-bit
1282    // CAP); a natural-width `n = 35` (built with the inherent slice
1283    // constructor) has `bits_precision = 8`, so `len = 1` (of 64 limbs).
1284
1285    // The rejection sampler must accept on a narrow modulus. Its buffer is
1286    // capacity-width, so the mask/window has to be taken relative to the
1287    // modulus width — otherwise `leading_zeros(modulus)` (headroom within
1288    // the modulus width, here 0) masks nothing of a 512-bit buffer and the
1289    // candidate is ~2^512 ≫ 35, rejected every try.
1290    #[test]
1291    fn try_random_mod_accepts_narrow_modulus() {
1292        use crate::traits::modular::TryRandomMod;
1293        use rand::rngs::ChaCha8Rng;
1294        use rand_core::SeedableRng;
1295        let n_nat = HeaplessCt::from_be_bytes(&[35]); // inherent -> len 1
1296        assert_eq!(
1297            const_num_traits::BitsPrecision::bits_precision(&n_nat),
1298            8,
1299            "modulus must be at its natural width, not padded to CAP"
1300        );
1301        let n = wrap_value(n_nat);
1302        let mut rng = ChaCha8Rng::from_seed([7; 32]);
1303        for _ in 0..16 {
1304            let r = <ModMathValue<HeaplessCt> as TryRandomMod>::try_random_mod(&mut rng, &n)
1305                .expect("sampler must accept on a narrow modulus");
1306            assert!(r < n, "sampled r must be < modulus");
1307        }
1308    }
1309
1310    // The field itself operates correctly at a narrow width: plain op and
1311    // blinded op both recover m = 2 when every operand shares the modulus
1312    // width. (c = 32, d = 29, e = 5, r = 6, all len 1.)
1313    #[test]
1314    fn narrow_modulus_private_ops_round_trip() {
1315        let n_params =
1316            ModMathParams::<HeaplessCt, Ct>::new(HeaplessCt::from_be_bytes(&[35])).unwrap();
1317        let c = wrap_value(HeaplessCt::from(32u8));
1318        let d = wrap_value(HeaplessCt::from(29u8));
1319        let e = wrap_value(HeaplessCt::from(5u8));
1320        let two = HeaplessCt::from(2u8);
1321
1322        let m = crate::algorithms::rsa::rsa_private_op(&c, &d, &n_params);
1323        assert_eq!(into_inner(m), two);
1324
1325        let r_nat = wrap_value(HeaplessCt::from(6u8)); // coprime with 35, len 1
1326        let blinded =
1327            crate::algorithms::rsa::rsa_private_op_blinded(&r_nat, &c, &d, &e, &n_params).unwrap();
1328        assert_eq!(into_inner(blinded), two);
1329    }
1330
1331    // Full sampled blinded op on a narrow modulus: the sampler parses
1332    // its modulus-width window with `FromByteSlice::from_be_slice`, so the
1333    // sampled `r` lands at the field width and the blinded arithmetic (and
1334    // verify-after-sign) closes. This is the end-to-end sub-capacity proof
1335    // — the randomized sign path a `len < CAP` deployment actually runs.
1336    #[test]
1337    fn narrow_modulus_sampled_blinded_op() {
1338        use rand::rngs::ChaCha8Rng;
1339        use rand_core::SeedableRng;
1340        let n_params =
1341            ModMathParams::<HeaplessCt, Ct>::new(HeaplessCt::from_be_bytes(&[35])).unwrap();
1342        let c = wrap_value(HeaplessCt::from(32u8));
1343        let d = wrap_value(HeaplessCt::from(29u8));
1344        let e = wrap_value(HeaplessCt::from(5u8));
1345        let mut rng = ChaCha8Rng::from_seed([9; 32]);
1346        let recovered = crate::algorithms::rsa::rsa_private_op_and_check_blinded(
1347            &mut rng, &c, &d, &e, &n_params,
1348        )
1349        .unwrap();
1350        assert_eq!(into_inner(recovered), HeaplessCt::from(2u8));
1351    }
1352
1353    #[test]
1354    fn rsa_private_op_and_check_rejects_wrong_exponent() {
1355        fn inner<T: TestCt>()
1356        where
1357            <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
1358        {
1359            // Same modulus + e, wrong `d` (11 vs 29). The recovered `m`
1360            // won't re-encrypt back to `c`, so the check should fail.
1361            let n_params = toy_params::<T>();
1362            let c = wrap_value(T::from(32u8));
1363            let bad_d = wrap_value(T::from(11u8));
1364            let e = wrap_value(T::from(5u8));
1365            let result =
1366                crate::algorithms::rsa::rsa_private_op_and_check(&c, &bad_d, &e, &n_params);
1367            assert!(result.is_err());
1368        }
1369        inner::<SmallUCt>();
1370        inner::<HeaplessCt>();
1371    }
1372
1373    // 2048-bit RSA keypair fixture — same `(n, e=65537, d)` used in
1374    // `algorithms::rsa::tests::recover_primes_works`, duplicated here
1375    // so the no-alloc test path can roundtrip-sign. `e` is rendered as
1376    // 3-byte BE (`0x010001`) and resized into `U2048` at test time.
1377    const N_2048: [u8; 256] = hex_literal::hex!(
1378        "d397b84d98a4c26138ed1b695a8106ead91d553bf06041b62d3fdc50a041e222
1379         b8f4529689c1b82c5e71554f5dd69fa2f4b6158cf0dbeb57811a0fc327e1f28e
1380         74fe74d3bc166c1eabdc1b8b57b934ca8be5b00b4f29975bcc99acaf415b59bb
1381         28a6782bb41a2c3c2976b3c18dbadef62f00c6bb226640095096c0cc60d22fe7
1382         ef987d75c6a81b10d96bf292028af110dc7cc1bbc43d22adab379a0cd5d8078c
1383         c780ff5cd6209dea34c922cf784f7717e428d75b5aec8ff30e5f0141510766e2
1384         e0ab8d473c84e8710b2b98227c3db095337ad3452f19e2b9bfbccdd8148abf67
1385         76fa552775e6e75956e45229ae5a9c46949bab1e622f0e48f56524a84ed3483b"
1386    );
1387    const D_2048: [u8; 256] = hex_literal::hex!(
1388        "c4e70c689162c94c660828191b52b4d8392115df486a9adbe831e458d7395832
1389         0dc1b755456e93701e9702d76fb0b92f90e01d1fe248153281fe79aa9763a92f
1390         ae69d8d7ecd144de29fa135bd14f9573e349e45031e3b76982f583003826c552
1391         e89a397c1a06bd2163488630d92e8c2bb643d7abef700da95d685c941489a46f
1392         54b5316f62b5d2c3a7f1bbd134cb37353a44683fdc9d95d36458de22f6c44057
1393         fe74a0a436c4308f73f4da42f35c47ac16a7138d483afc91e41dc3a1127382e0
1394         c0f5119b0221b4fc639d6b9c38177a6de9b526ebd88c38d7982c07f98a0efd87
1395         7d508aae275b946915c02e2e1106d175d74ec6777f5e80d12c053d9c7be1e341"
1396    );
1397
1398    #[test]
1399    fn pkcs1v15_sign_into_round_trip_2048_sha1() {
1400        use crate::algorithms::pkcs1v15::{
1401            pkcs1v15_generate_prefix_into, pkcs1v15_sign_pad_into, sign_into,
1402        };
1403        use crate::traits::PublicKeyParts;
1404        use sha1::Sha1;
1405
1406        type U2048 = FixedUInt<u8, 256, Ct>;
1407        const K: usize = 256;
1408
1409        let key = public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1410        let d_int = <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap();
1411        let d = wrap_value(d_int);
1412        let e_int =
1413            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&[0x01, 0x00, 0x01])
1414                .unwrap();
1415        let e = wrap_value(e_int);
1416
1417        let digest = [0xAAu8; 20];
1418        let mut prefix_storage = [0u8; 32];
1419        let prefix = pkcs1v15_generate_prefix_into::<Sha1>(&mut prefix_storage).unwrap();
1420
1421        let mut em_storage = [0u8; K];
1422        let mut sig_storage = [0u8; K];
1423        let sig = sign_into(
1424            key.n_params(),
1425            &d,
1426            &e,
1427            prefix,
1428            &digest,
1429            K,
1430            &mut em_storage,
1431            &mut sig_storage,
1432        )
1433        .unwrap();
1434        assert_eq!(sig.len(), K);
1435
1436        // Roundtrip via public op: `sig^e mod n` must recover the padded EM
1437        // that `pkcs1v15_sign_pad_into` produces for the same (prefix, digest).
1438        let recovered = public_key_op_ct(&key, sig).unwrap();
1439        let mut expected_em_storage = [0u8; K];
1440        let expected_em =
1441            pkcs1v15_sign_pad_into(prefix, &digest, K, &mut expected_em_storage).unwrap();
1442        assert_eq!(recovered.as_ref(), expected_em);
1443    }
1444
1445    #[test]
1446    fn pss_sign_into_round_trip_2048_sha1() {
1447        use crate::algorithms::pss::{emsa_pss_verify, sign_into};
1448        use crate::traits::PublicKeyParts;
1449        use digest::Digest;
1450        use sha1::Sha1;
1451
1452        type U2048 = FixedUInt<u8, 256, Ct>;
1453        const K: usize = 256;
1454        const KEY_BITS: usize = 2048;
1455
1456        let key = public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1457        let d = wrap_value(
1458            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap(),
1459        );
1460        let e = wrap_value(
1461            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&[0x01, 0x00, 0x01])
1462                .unwrap(),
1463        );
1464
1465        let digest = [0xAAu8; 20];
1466        let salt: &[u8] = &[]; // empty salt → deterministic encoding
1467        let mut hash = Sha1::new();
1468
1469        let mut em_storage = [0u8; K];
1470        let mut sig_storage = [0u8; K];
1471        let sig = sign_into(
1472            key.n_params(),
1473            &d,
1474            &e,
1475            &digest,
1476            salt,
1477            K,
1478            &mut hash,
1479            &mut em_storage,
1480            &mut sig_storage,
1481        )
1482        .unwrap();
1483        assert_eq!(sig.len(), K);
1484
1485        // Roundtrip via public op: `sig^e mod n` must yield a valid PSS-encoded
1486        // EM for `(digest, salt)`. `emsa_pss_verify` modifies `em` in place
1487        // (MGF unmask), so copy the recovered bytes into a mutable buffer.
1488        let recovered = public_key_op_ct(&key, sig).unwrap();
1489        let mut em_copy = [0u8; K];
1490        em_copy.copy_from_slice(recovered.as_ref());
1491        let mut verify_hash = Sha1::new();
1492        emsa_pss_verify(
1493            &digest,
1494            &mut em_copy,
1495            Some(salt.len()),
1496            &mut verify_hash,
1497            KEY_BITS,
1498        )
1499        .unwrap();
1500    }
1501
1502    // Local alias for `rsa_public_op_ct` — keeps the test's call-site short.
1503    fn public_key_op_ct<T>(
1504        key: &crate::key::GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Ct>>,
1505        input: &[u8],
1506    ) -> Result<<ModMathValue<T> as UnsignedModularInt>::Bytes>
1507    where
1508        T: ModMathIntCt + HasPersonality<P = Ct>,
1509    {
1510        crate::modmath_support::rsa_public_op_ct(key, input)
1511    }
1512
1513    // ─── defensive-error tests for `sign_into` upfront checks ───────────
1514    //
1515    // These tests trip `sign_into`'s fast-fail guards. None reach the
1516    // RSA exponentiation, so `d`/`e` can be dummy values
1517    // and the toy `SmallUCt` (512-bit) `n_params` is sufficient.
1518
1519    fn dummy_de() -> (ModMathValue<SmallUCt>, ModMathValue<SmallUCt>) {
1520        (
1521            wrap_value(SmallUCt::from(1u8)),
1522            wrap_value(SmallUCt::from(1u8)),
1523        )
1524    }
1525
1526    // SmallUCt = FixedUInt<u8, 64, Ct> → bits_precision = 512 → k = 64.
1527    const SMALL_K: usize = 64;
1528
1529    #[test]
1530    fn pkcs1v15_sign_into_rejects_wrong_k() {
1531        use crate::algorithms::pkcs1v15::sign_into;
1532        let n_params = toy_params();
1533        let (d, e) = dummy_de();
1534        let mut em = [0u8; SMALL_K];
1535        let mut sig = [0u8; SMALL_K];
1536        let result = sign_into(
1537            &n_params,
1538            &d,
1539            &e,
1540            &[],
1541            &[0u8; 20],
1542            SMALL_K - 1, // wrong: should be SMALL_K
1543            &mut em,
1544            &mut sig,
1545        );
1546        assert!(matches!(result, Err(Error::InvalidArguments)));
1547    }
1548
1549    #[test]
1550    fn pkcs1v15_sign_into_rejects_small_sig_storage() {
1551        use crate::algorithms::pkcs1v15::sign_into;
1552        let n_params = toy_params_wide();
1553        let (d, e) = dummy_de();
1554        let mut em = [0u8; SMALL_K];
1555        let mut sig = [0u8; SMALL_K - 1]; // one byte short
1556        let result = sign_into(
1557            &n_params,
1558            &d,
1559            &e,
1560            &[],
1561            &[0u8; 20],
1562            SMALL_K,
1563            &mut em,
1564            &mut sig,
1565        );
1566        assert!(matches!(result, Err(Error::OutputBufferTooSmall)));
1567    }
1568
1569    #[test]
1570    fn pkcs1v15_sign_into_propagates_message_too_long() {
1571        // prefix + hashed + 11 > k → `pkcs1v15_sign_pad_into` returns
1572        // MessageTooLong. Confirms errors from the padding step bubble up.
1573        use crate::algorithms::pkcs1v15::sign_into;
1574        let n_params = toy_params_wide();
1575        let (d, e) = dummy_de();
1576        let mut em = [0u8; SMALL_K];
1577        let mut sig = [0u8; SMALL_K];
1578        let oversize_prefix = [0u8; SMALL_K]; // 64-byte prefix alone exceeds k - 11
1579        let result = sign_into(
1580            &n_params,
1581            &d,
1582            &e,
1583            &oversize_prefix,
1584            &[0u8; 20],
1585            SMALL_K,
1586            &mut em,
1587            &mut sig,
1588        );
1589        assert!(matches!(result, Err(Error::MessageTooLong)));
1590    }
1591
1592    #[test]
1593    fn pss_sign_into_rejects_wrong_k() {
1594        use crate::algorithms::pss::sign_into;
1595        use digest::Digest;
1596        use sha1::Sha1;
1597        let n_params = toy_params_wide();
1598        let (d, e) = dummy_de();
1599        let mut em = [0u8; SMALL_K];
1600        let mut sig = [0u8; SMALL_K];
1601        let mut hash = Sha1::new();
1602        let result = sign_into(
1603            &n_params,
1604            &d,
1605            &e,
1606            &[0u8; 20],
1607            &[],
1608            SMALL_K - 1, // wrong
1609            &mut hash,
1610            &mut em,
1611            &mut sig,
1612        );
1613        assert!(matches!(result, Err(Error::InvalidArguments)));
1614    }
1615
1616    #[test]
1617    fn pss_sign_into_rejects_small_sig_storage() {
1618        use crate::algorithms::pss::sign_into;
1619        use digest::Digest;
1620        use sha1::Sha1;
1621        let n_params = toy_params_wide();
1622        let (d, e) = dummy_de();
1623        let mut em = [0u8; SMALL_K];
1624        let mut sig = [0u8; SMALL_K - 1];
1625        let mut hash = Sha1::new();
1626        let result = sign_into(
1627            &n_params,
1628            &d,
1629            &e,
1630            &[0u8; 20],
1631            &[],
1632            SMALL_K,
1633            &mut hash,
1634            &mut em,
1635            &mut sig,
1636        );
1637        assert!(matches!(result, Err(Error::OutputBufferTooSmall)));
1638    }
1639
1640    #[test]
1641    fn pss_sign_into_rejects_small_em_storage() {
1642        use crate::algorithms::pss::sign_into;
1643        use digest::Digest;
1644        use sha1::Sha1;
1645        let n_params = toy_params_wide();
1646        let (d, e) = dummy_de();
1647        // em_bits = key_bits - 1 = 511 → em_len = 64. Pass 63 to fail.
1648        let mut em = [0u8; SMALL_K - 1];
1649        let mut sig = [0u8; SMALL_K];
1650        let mut hash = Sha1::new();
1651        let result = sign_into(
1652            &n_params,
1653            &d,
1654            &e,
1655            &[0u8; 20],
1656            &[],
1657            SMALL_K,
1658            &mut hash,
1659            &mut em,
1660            &mut sig,
1661        );
1662        assert!(matches!(result, Err(Error::OutputBufferTooSmall)));
1663    }
1664
1665    #[test]
1666    fn pss_sign_into_rejects_wrong_hash_length() {
1667        // emsa_pss_encode_into returns InputNotHashed when m_hash.len()
1668        // != hash output size. Confirms errors from the encode step bubble up.
1669        use crate::algorithms::pss::sign_into;
1670        use digest::Digest;
1671        use sha1::Sha1;
1672        let n_params = toy_params_wide();
1673        let (d, e) = dummy_de();
1674        let mut em = [0u8; SMALL_K];
1675        let mut sig = [0u8; SMALL_K];
1676        let mut hash = Sha1::new();
1677        let result = sign_into(
1678            &n_params,
1679            &d,
1680            &e,
1681            &[0u8; 21], // SHA-1 produces 20 bytes, not 21
1682            &[],
1683            SMALL_K,
1684            &mut hash,
1685            &mut em,
1686            &mut sig,
1687        );
1688        assert!(matches!(result, Err(Error::InputNotHashed)));
1689    }
1690
1691    // `sign_into`'s `k` check must use the actual modulus bit-length,
1692    // not the container's `bits_precision()`, otherwise a shorter
1693    // modulus stored in a wider container spuriously rejects the only
1694    // valid `k`.
1695    #[test]
1696    fn pkcs1v15_sign_into_k_uses_modulus_bits_not_container() {
1697        use crate::algorithms::pkcs1v15::sign_into;
1698        // 128-byte (1024-bit) container storing a ~512-bit modulus.
1699        type WideUCt = FixedUInt<u8, 128, Ct>;
1700        let mut mod_bytes = [0u8; 128];
1701        mod_bytes[64] = 0x80; // MSB of the low 512 bits
1702        mod_bytes[127] = 0x01; // LSB odd
1703        let n = <WideUCt as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&mod_bytes).unwrap();
1704        let n_params = ModMathParams::<WideUCt, Ct>::new(n).unwrap();
1705        let d = wrap_value(WideUCt::from(29u8));
1706        let e = wrap_value(WideUCt::from(5u8));
1707
1708        const CORRECT_K: usize = 64; // 512 modulus bits div_ceil 8
1709        const CONTAINER_K: usize = 128; // what `bits_precision()` would say
1710
1711        // k = modulus_bits.div_ceil(8) must pass the width check even
1712        // though the container is wider (it then fails later on the
1713        // toy (d, e) — that's expected and asserted below).
1714        let mut em = [0u8; CORRECT_K];
1715        let mut sig = [0u8; CORRECT_K];
1716        let result = sign_into(
1717            &n_params,
1718            &d,
1719            &e,
1720            &[],
1721            &[0u8; 20],
1722            CORRECT_K,
1723            &mut em,
1724            &mut sig,
1725        );
1726        assert!(
1727            !matches!(result, Err(Error::InvalidArguments)),
1728            "correct k (= modulus_bits.div_ceil(8)) must pass the width check, got {:?}",
1729            result
1730        );
1731
1732        // Container-width k must be rejected — that's the whole point.
1733        let mut em = [0u8; CONTAINER_K];
1734        let mut sig = [0u8; CONTAINER_K];
1735        let result = sign_into(
1736            &n_params,
1737            &d,
1738            &e,
1739            &[],
1740            &[0u8; 20],
1741            CONTAINER_K,
1742            &mut em,
1743            &mut sig,
1744        );
1745        assert!(matches!(result, Err(Error::InvalidArguments)));
1746    }
1747
1748    // ─── PrivateKeyParts smoke tests ─────────────────────────────
1749
1750    #[test]
1751    fn pkcs1v15_signing_key_round_trip_2048_sha1() {
1752        use crate::key::{GenericRsaPrivateKey, GenericRsaPublicKey};
1753        use crate::pkcs1v15::{GenericSignature, GenericSigningKey, GenericVerifyingKey};
1754        use digest::Digest;
1755        use sha1::Sha1;
1756        use signature::hazmat::PrehashVerifier;
1757
1758        type U2048 = FixedUInt<u8, 256, Ct>;
1759        const K: usize = 256;
1760
1761        let public =
1762            crate::modmath_support::public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1763        let public_clone: GenericRsaPublicKey<ModMathValue<U2048>, ModMathParams<U2048, Ct>> =
1764            public.clone();
1765        let d = wrap_value(
1766            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap(),
1767        );
1768        let priv_key = GenericRsaPrivateKey::from_public_and_d(public, d);
1769
1770        let signing_key = GenericSigningKey::<Sha1, _, _>::new(priv_key);
1771        let verifying_key = GenericVerifyingKey::<Sha1, _, _>::new(public_clone);
1772
1773        let msg: &[u8] = b"deterministic test message";
1774        let mut em_storage = [0u8; K];
1775        let mut sig_storage = [0u8; K];
1776        let sig_slice = signing_key
1777            .try_sign_into(msg, &mut em_storage, &mut sig_storage)
1778            .unwrap();
1779        assert_eq!(sig_slice.len(), K);
1780
1781        // Round-trip: build a `GenericSignature` over the same modulus type
1782        // and verify against the prehash via the existing verifier.
1783        let sig_int =
1784            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(sig_slice).unwrap();
1785        let sig = GenericSignature::from(wrap_value(sig_int));
1786        let digest = Sha1::digest(msg);
1787        verifying_key.verify_prehash(&digest, &sig).unwrap();
1788    }
1789
1790    #[test]
1791    fn pkcs1v15_signing_key_rejects_wrong_prehash_length() {
1792        use crate::key::GenericRsaPrivateKey;
1793        use crate::pkcs1v15::GenericSigningKey;
1794        use sha1::Sha1;
1795
1796        type U2048 = FixedUInt<u8, 256, Ct>;
1797        const K: usize = 256;
1798
1799        let public =
1800            crate::modmath_support::public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1801        let d = wrap_value(
1802            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap(),
1803        );
1804        let priv_key = GenericRsaPrivateKey::from_public_and_d(public, d);
1805        let signing_key = GenericSigningKey::<Sha1, _, _>::new(priv_key);
1806
1807        let bad_prehash = [0u8; 21]; // SHA-1 outputs 20 bytes, not 21.
1808        let mut em_storage = [0u8; K];
1809        let mut sig_storage = [0u8; K];
1810        let result =
1811            signing_key.try_sign_prehash_into(&bad_prehash, &mut em_storage, &mut sig_storage);
1812        assert!(matches!(result, Err(Error::InputNotHashed)));
1813    }
1814
1815    #[test]
1816    fn pss_signing_key_round_trip_2048_sha1() {
1817        use crate::algorithms::pss::emsa_pss_verify;
1818        use crate::key::GenericRsaPrivateKey;
1819        use crate::pss::GenericSigningKey;
1820        use digest::Digest;
1821        use sha1::Sha1;
1822
1823        type U2048 = FixedUInt<u8, 256, Ct>;
1824        const K: usize = 256;
1825        const KEY_BITS: usize = 2048;
1826
1827        let key =
1828            crate::modmath_support::public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1829        let d = wrap_value(
1830            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap(),
1831        );
1832        let priv_key = GenericRsaPrivateKey::from_public_and_d(key.clone(), d);
1833        // Salt length = 0 → deterministic encoding, easy roundtrip.
1834        let signing_key = GenericSigningKey::<Sha1, _, _>::new_with_salt_len(priv_key, 0);
1835
1836        let msg: &[u8] = b"pss-roundtrip test message";
1837        let digest = Sha1::digest(msg);
1838        let mut em_storage = [0u8; K];
1839        let mut sig_storage = [0u8; K];
1840        let sig_slice = signing_key
1841            .try_sign_prehash_with_salt_into(&digest, &[], &mut em_storage, &mut sig_storage)
1842            .unwrap();
1843        assert_eq!(sig_slice.len(), K);
1844
1845        // Roundtrip: `sig^e mod n` should yield a valid PSS-encoded EM
1846        // for `(digest, salt_len=0)`. `emsa_pss_verify` modifies em
1847        // in place (MGF unmask), so copy first.
1848        let recovered = public_key_op_ct(&key, sig_slice).unwrap();
1849        let mut em_copy = [0u8; K];
1850        em_copy.copy_from_slice(recovered.as_ref());
1851        let mut verify_hash = Sha1::new();
1852        emsa_pss_verify(&digest, &mut em_copy, Some(0), &mut verify_hash, KEY_BITS).unwrap();
1853    }
1854
1855    // Exact-width blinded sign round trip on the modmath backend:
1856    // a 2048-bit modulus in exactly `U2048` must blind, invert, and
1857    // sign successfully — no carrier headroom over the modulus is
1858    // required.
1859    #[test]
1860    fn pss_signing_key_try_sign_prehash_with_rng_into_round_trip_2048_sha1() {
1861        use crate::algorithms::pss::emsa_pss_verify;
1862        use crate::key::GenericRsaPrivateKey;
1863        use crate::pss::GenericSigningKey;
1864        use digest::Digest;
1865        use rand::rngs::ChaCha8Rng;
1866        use rand_core::SeedableRng;
1867        use sha1::Sha1;
1868
1869        type U2048 = FixedUInt<u8, 256, Ct>;
1870        const K: usize = 256;
1871        const KEY_BITS: usize = 2048;
1872
1873        let key =
1874            crate::modmath_support::public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1875        let d = wrap_value(
1876            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap(),
1877        );
1878        let priv_key = GenericRsaPrivateKey::from_public_and_d(key.clone(), d);
1879        // Salt length = 0 → deterministic PSS encoding.
1880        let signing_key = GenericSigningKey::<Sha1, _, _>::new_with_salt_len(priv_key, 0);
1881
1882        let msg: &[u8] = b"pss-blinded-roundtrip test message";
1883        let digest = Sha1::digest(msg);
1884        let mut rng = ChaCha8Rng::from_seed([42; 32]);
1885        let mut em_storage = [0u8; K];
1886        let mut sig_storage = [0u8; K];
1887        let mut salt_storage = [0u8; 0];
1888        let sig_slice = signing_key
1889            .try_sign_prehash_with_rng_into(
1890                &mut rng,
1891                &digest,
1892                &mut em_storage,
1893                &mut sig_storage,
1894                &mut salt_storage,
1895            )
1896            .unwrap();
1897        assert_eq!(sig_slice.len(), K);
1898
1899        let recovered = public_key_op_ct(&key, sig_slice).unwrap();
1900        let mut em_copy = [0u8; K];
1901        em_copy.copy_from_slice(recovered.as_ref());
1902        let mut verify_hash = Sha1::new();
1903        emsa_pss_verify(&digest, &mut em_copy, Some(0), &mut verify_hash, KEY_BITS).unwrap();
1904    }
1905
1906    #[test]
1907    fn pss_signing_key_rejects_wrong_prehash_length() {
1908        use crate::key::GenericRsaPrivateKey;
1909        use crate::pss::GenericSigningKey;
1910        use sha1::Sha1;
1911
1912        type U2048 = FixedUInt<u8, 256, Ct>;
1913        const K: usize = 256;
1914
1915        let key =
1916            crate::modmath_support::public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1917        let d = wrap_value(
1918            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap(),
1919        );
1920        let priv_key = GenericRsaPrivateKey::from_public_and_d(key, d);
1921        let signing_key = GenericSigningKey::<Sha1, _, _>::new_with_salt_len(priv_key, 0);
1922
1923        let bad_prehash = [0u8; 21]; // SHA-1 is 20 bytes.
1924        let mut em_storage = [0u8; K];
1925        let mut sig_storage = [0u8; K];
1926        let result = signing_key.try_sign_prehash_with_salt_into(
1927            &bad_prehash,
1928            &[],
1929            &mut em_storage,
1930            &mut sig_storage,
1931        );
1932        assert!(matches!(result, Err(Error::InputNotHashed)));
1933    }
1934
1935    #[test]
1936    fn pss_signing_key_rejects_salt_len_mismatch() {
1937        use crate::key::GenericRsaPrivateKey;
1938        use crate::pss::GenericSigningKey;
1939        use sha1::Sha1;
1940
1941        type U2048 = FixedUInt<u8, 256, Ct>;
1942        const K: usize = 256;
1943
1944        let key =
1945            crate::modmath_support::public_key_ct_from_be_bytes::<U2048>(&N_2048, 65537).unwrap();
1946        let d = wrap_value(
1947            <U2048 as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(&D_2048).unwrap(),
1948        );
1949        let priv_key = GenericRsaPrivateKey::from_public_and_d(key, d);
1950        // salt_len configured to 20; supply 16 -> mismatch.
1951        let signing_key = GenericSigningKey::<Sha1, _, _>::new_with_salt_len(priv_key, 20);
1952
1953        let prehash = [0u8; 20];
1954        let wrong_salt = [0u8; 16];
1955        let mut em_storage = [0u8; K];
1956        let mut sig_storage = [0u8; K];
1957        let result = signing_key.try_sign_prehash_with_salt_into(
1958            &prehash,
1959            &wrong_salt,
1960            &mut em_storage,
1961            &mut sig_storage,
1962        );
1963        assert!(matches!(result, Err(Error::InvalidArguments)));
1964    }
1965
1966    #[test]
1967    fn pss_signing_key_satisfies_zeroize() {
1968        use crate::key::GenericRsaPrivateKey;
1969        use crate::pss::GenericSigningKey;
1970        use sha1::Sha1;
1971        fn assert_zeroize<Z: Zeroize>() {}
1972        assert_zeroize::<
1973            GenericSigningKey<Sha1, ModMathValue<SmallUCt>, ModMathParams<SmallUCt, Ct>>,
1974        >();
1975
1976        let public =
1977            crate::modmath_support::public_key_ct_from_be_bytes::<SmallUCt>(&[35u8], 5).unwrap();
1978        let priv_key =
1979            GenericRsaPrivateKey::from_public_and_d(public, wrap_value(SmallUCt::from(29u8)));
1980        let mut signing_key = GenericSigningKey::<Sha1, _, _>::new(priv_key);
1981        signing_key.zeroize();
1982    }
1983
1984    #[test]
1985    fn pkcs1v15_signing_key_satisfies_zeroize() {
1986        use crate::key::GenericRsaPrivateKey;
1987        use crate::pkcs1v15::GenericSigningKey;
1988        use sha1::Sha1;
1989        fn assert_zeroize<Z: Zeroize>() {}
1990        assert_zeroize::<
1991            GenericSigningKey<Sha1, ModMathValue<SmallUCt>, ModMathParams<SmallUCt, Ct>>,
1992        >();
1993
1994        // Construct one and exercise .zeroize() at runtime to confirm the
1995        // delegation compiles end-to-end.
1996        let public =
1997            crate::modmath_support::public_key_ct_from_be_bytes::<SmallUCt>(&[35u8], 5).unwrap();
1998        let priv_key =
1999            GenericRsaPrivateKey::from_public_and_d(public, wrap_value(SmallUCt::from(29u8)));
2000        let mut signing_key = GenericSigningKey::<Sha1, _, _>::new(priv_key);
2001        signing_key.zeroize();
2002    }
2003
2004    #[test]
2005    fn generic_rsa_private_key_satisfies_traits() {
2006        // Compile-time assertion: GenericRsaPrivateKey<SmallUCt, ModMathParams<SmallUCt, Ct>>
2007        // satisfies both PublicKeyParts and PrivateKeyParts at the
2008        // matching (T, M) substitution. The fn-bound dance below is the
2009        // standard "type-satisfies-trait" check.
2010        use crate::key::GenericRsaPrivateKey;
2011        use crate::traits::keys::{PrivateKeyParts, PublicKeyParts};
2012        fn assert_pub_parts<K, T>(_: &K)
2013        where
2014            T: UnsignedModularInt,
2015            K: PublicKeyParts<T>,
2016        {
2017        }
2018        fn assert_priv_parts<K, T>(_: &K)
2019        where
2020            T: UnsignedModularInt,
2021            K: PrivateKeyParts<T>,
2022        {
2023        }
2024
2025        // Use the existing public-key constructor for the pubkey side,
2026        // then attach a dummy d.
2027        let public =
2028            crate::modmath_support::public_key_ct_from_be_bytes::<SmallUCt>(&[35u8], 5).unwrap();
2029        let key = GenericRsaPrivateKey::from_public_and_d(public, wrap_value(SmallUCt::from(29u8)));
2030
2031        assert_pub_parts::<_, ModMathValue<SmallUCt>>(&key);
2032        assert_priv_parts::<_, ModMathValue<SmallUCt>>(&key);
2033
2034        // Round-trip: accessors return the values we constructed it with.
2035        assert_eq!(PrivateKeyParts::d(&key), &wrap_value(SmallUCt::from(29u8)));
2036        assert_eq!(key.as_public().e(), &wrap_value(SmallUCt::from(5u8)));
2037    }
2038}