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