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