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