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 fixed_bigint::{Ct, Nct, Personality};
12use modmath::{CiosMontMul, CiosMontMulCt, Field as ModmathField, Parity, WideMul};
13use num_traits::ops::overflowing::OverflowingAdd;
14use num_traits::ops::wrapping::{WrappingAdd, WrappingMul, WrappingSub};
15use num_traits::{One, Zero};
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{
45}
46
47impl<T> ModMathInt for T where
48    T: FixedWidthUnsignedInt
49        + From<u8>
50        + PartialEq
51        + PartialOrd
52        + One
53        + Zero
54        + Parity
55        + OverflowingAdd
56        + WideMul
57        + CiosMontMul
58        + WrappingAdd
59        + WrappingMul
60        + WrappingSub
61        + Shr<usize, Output = Self>
62        + ShrAssign<usize>
63{
64}
65
66pub trait ModMathIntCt:
67    FixedWidthUnsignedInt
68    + From<u8>
69    + PartialEq
70    + PartialOrd
71    + One
72    + Zero
73    + Parity
74    + OverflowingAdd
75    + WideMul
76    + CiosMontMulCt
77    + WrappingAdd
78    + WrappingMul
79    + WrappingSub
80    + Shr<usize, Output = Self>
81    + ShrAssign<usize>
82    + subtle::ConditionallySelectable
83    + subtle::ConstantTimeLess
84    + core::ops::BitAnd<Output = Self>
85{
86}
87
88impl<T> ModMathIntCt for T where
89    T: FixedWidthUnsignedInt
90        + From<u8>
91        + PartialEq
92        + PartialOrd
93        + One
94        + Zero
95        + Parity
96        + OverflowingAdd
97        + WideMul
98        + CiosMontMulCt
99        + WrappingAdd
100        + WrappingMul
101        + WrappingSub
102        + Shr<usize, Output = Self>
103        + ShrAssign<usize>
104        + subtle::ConditionallySelectable
105        + subtle::ConstantTimeLess
106        + core::ops::BitAnd<Output = Self>
107{
108}
109
110#[cfg(feature = "alloc")]
111fn wrap_value<T>(value: T) -> ModMathValue<T> {
112    ModMathValue(value)
113}
114
115#[cfg(not(feature = "alloc"))]
116fn wrap_value<T>(value: T) -> ModMathValue<T> {
117    value
118}
119
120#[cfg(feature = "alloc")]
121fn unwrap_value<T: Copy>(value: &ModMathValue<T>) -> T {
122    value.0
123}
124
125#[cfg(feature = "alloc")]
126fn unwrap_value_ref<T>(value: &ModMathValue<T>) -> &T {
127    &value.0
128}
129
130#[cfg(not(feature = "alloc"))]
131fn unwrap_value_ref<T>(value: &ModMathValue<T>) -> &T {
132    value
133}
134
135#[cfg(not(feature = "alloc"))]
136fn unwrap_value<T: Copy>(value: &ModMathValue<T>) -> T {
137    *value
138}
139
140#[cfg(feature = "alloc")]
141#[repr(transparent)]
142#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
143pub struct ModMathValue<T>(pub T);
144
145#[cfg(feature = "alloc")]
146impl<T> ModMathValue<T> {
147    pub fn from_inner(inner: T) -> Self {
148        Self(inner)
149    }
150
151    pub fn inner(&self) -> &T {
152        &self.0
153    }
154}
155
156#[cfg(feature = "alloc")]
157impl<T> Zeroize for ModMathValue<T>
158where
159    T: Zeroize,
160{
161    fn zeroize(&mut self) {
162        self.0.zeroize();
163    }
164}
165
166#[cfg(feature = "alloc")]
167impl<T> From<u8> for ModMathValue<T>
168where
169    T: From<u8>,
170{
171    fn from(value: u8) -> Self {
172        Self(<T as From<u8>>::from(value))
173    }
174}
175
176#[cfg(feature = "alloc")]
177impl<T> IntegerResize for ModMathValue<T>
178where
179    T: FixedWidthUnsignedInt + PartialOrd,
180{
181    type Output = Self;
182
183    fn resize_unchecked(self, _at_least_bits_precision: u32) -> Self::Output {
184        self
185    }
186
187    fn try_resize(self, at_least_bits_precision: u32) -> Option<Self::Output> {
188        // Mirrors `crypto_bigint::Resize::try_resize`: returns `Some` iff
189        // the actual value fits in `at_least_bits_precision` bits. Our
190        // type is fixed-width and `resize_unchecked` is a no-op, but the
191        // check still needs to reject values that wouldn't survive a
192        // narrower precision.
193        let value_bits = self.bits_precision() - self.leading_zeros();
194        if value_bits <= at_least_bits_precision {
195            Some(self)
196        } else {
197            None
198        }
199    }
200}
201
202#[cfg(feature = "alloc")]
203impl<T> UnsignedModularInt for ModMathValue<T>
204where
205    T: FixedWidthUnsignedInt + PartialOrd,
206{
207    type Bytes = <T as FixedWidthUnsignedInt>::Bytes;
208
209    fn leading_zeros(&self) -> u32 {
210        FixedWidthUnsignedInt::leading_zeros(&self.0)
211    }
212
213    fn to_be_bytes(&self) -> Self::Bytes {
214        FixedWidthUnsignedInt::to_be_bytes(&self.0)
215    }
216
217    #[cfg(feature = "alloc")]
218    fn to_be_bytes_trimmed_vartime(&self) -> Box<[u8]> {
219        let bytes = self.to_be_bytes();
220        let bytes = bytes.as_ref();
221        let first_non_zero = bytes
222            .iter()
223            .position(|b| *b != 0)
224            .unwrap_or(bytes.len().saturating_sub(1));
225        bytes[first_non_zero..].to_vec().into_boxed_slice()
226    }
227
228    fn as_nz_ref(&self) -> NonZero<Self> {
229        NonZero::new(*self).expect("value is non-zero")
230    }
231
232    fn bits(&self) -> u32 {
233        self.bits_precision() - self.leading_zeros()
234    }
235
236    fn bits_precision(&self) -> u32 {
237        FixedWidthUnsignedInt::bits_precision(&self.0)
238    }
239}
240
241#[cfg(feature = "alloc")]
242impl<T> TryFromBeBytes for ModMathValue<T>
243where
244    T: FixedWidthUnsignedInt,
245{
246    fn try_from_be_bytes_vartime(bytes: &[u8]) -> Result<Self> {
247        Ok(Self(
248            <T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(bytes)?,
249        ))
250    }
251}
252
253#[cfg(not(feature = "alloc"))]
254pub type ModMathValue<T> = T;
255
256#[derive(Clone, Debug)]
257pub struct ModMathParams<T, P: Personality = Nct> {
258    // Owns the modulus + precomputed Montgomery constants. `Clone` is a
259    // trivial 4×T memcpy per modmath::Field's documented guarantee — does
260    // NOT re-run `compute_r_mod_n` / `compute_r2_mod_n`.
261    field: ModmathField<T, P>,
262    // Parallel copy of the modulus, wrapped in `Odd` for the
263    // `ModulusParams::modulus() -> &Odd<...>` trait interface. Duplicates
264    // `field.modulus()` (one extra T per params, one extra T-sized memcpy
265    // per clone) — cheap, and lets `modulus()` return a real reference
266    // instead of transmuting through `repr(transparent)`.
267    modulus_odd: Odd<ModMathValue<T>>,
268}
269
270impl<T: ModMathInt> ModMathParams<T, Nct> {
271    pub fn new(modulus: T) -> Result<Self> {
272        let field = ModmathField::<T, Nct>::new(modulus).ok_or(Error::InvalidModulus)?;
273        let modulus_odd = Odd::new(wrap_value(modulus)).ok_or(Error::InvalidModulus)?;
274        Ok(Self { field, modulus_odd })
275    }
276}
277
278impl<T: ModMathIntCt> ModMathParams<T, Ct> {
279    /// Create CT (encrypt) Montgomery parameters for an odd, non-zero
280    /// modulus.
281    pub fn new(modulus: T) -> Result<Self> {
282        let field = ModmathField::<T, Ct>::new(modulus).ok_or(Error::InvalidModulus)?;
283        let modulus_odd = Odd::new(wrap_value(modulus)).ok_or(Error::InvalidModulus)?;
284        Ok(Self { field, modulus_odd })
285    }
286}
287
288impl<T, P: Personality> ModMathParams<T, P> {
289    pub(crate) fn field(&self) -> &ModmathField<T, P> {
290        &self.field
291    }
292}
293
294/// Construct an **NCT** public key from big-endian modulus bytes and a public
295/// exponent. Use this for signature verification.
296pub fn public_key_from_be_bytes<T>(
297    modulus: &[u8],
298    exponent: u32,
299) -> Result<GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Nct>>>
300where
301    T: ModMathInt,
302{
303    let n = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
304        modulus,
305    )?);
306    let exponent = exponent.to_be_bytes();
307    let e = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
308        &exponent,
309    )?);
310    GenericRsaPublicKey::from_components(n, e, ModMathParams::<T, Nct>::new(unwrap_value(&n))?)
311}
312
313/// Apply the raw RSA public operation to a fixed-width block using the **NCT**
314/// (vartime) Montgomery path. Intended for signature verification.
315pub fn rsa_public_op<T>(
316    key: &GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Nct>>,
317    input: &[u8],
318) -> Result<<ModMathValue<T> as UnsignedModularInt>::Bytes>
319where
320    T: ModMathInt,
321{
322    let input = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
323        input,
324    )?);
325    Ok(rsa_encrypt(key, &input)?.to_be_bytes())
326}
327
328/// Construct a **CT** public key. Use this when the resulting key will feed
329/// PKCS#1 v1.5 / OAEP encryption (or any other path where the plaintext is
330/// secret). `T` must be a Ct-typed FixedUInt; the bound is enforced by the
331/// `CiosMontMulCt` requirement inside [`ModMathIntCt`].
332pub fn public_key_ct_from_be_bytes<T>(
333    modulus: &[u8],
334    exponent: u32,
335) -> Result<GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Ct>>>
336where
337    T: ModMathIntCt,
338{
339    let n = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
340        modulus,
341    )?);
342    let exponent = exponent.to_be_bytes();
343    let e = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
344        &exponent,
345    )?);
346    GenericRsaPublicKey::from_components(n, e, ModMathParams::<T, Ct>::new(unwrap_value(&n))?)
347}
348
349pub fn rsa_public_op_ct<T>(
350    key: &GenericRsaPublicKey<ModMathValue<T>, ModMathParams<T, Ct>>,
351    input: &[u8],
352) -> Result<<ModMathValue<T> as UnsignedModularInt>::Bytes>
353where
354    T: ModMathIntCt,
355{
356    let input = wrap_value(<T as FixedWidthUnsignedInt>::try_from_be_bytes_vartime(
357        input,
358    )?);
359    Ok(rsa_encrypt(key, &input)?.to_be_bytes())
360}
361
362#[derive(Clone, Debug)]
363pub struct ModMathForm<T, P: Personality = Nct>
364where
365    T: Clone,
366{
367    integer_mont: ModMathValue<T>,
368    params: ModMathParams<T, P>,
369}
370
371impl<T: ModMathInt> IntoMontyForm<ModMathParams<T, Nct>> for ModMathForm<T, Nct> {
372    fn from_reduced(integer: ModMathValue<T>, params: &ModMathParams<T, Nct>) -> Self {
373        let field = params.field();
374        let r = field.reduce(unwrap_value_ref(&integer));
375        Self {
376            integer_mont: wrap_value(r.mont_value()),
377            params: params.clone(),
378        }
379    }
380
381    /// `Field::reduce` is `raw * R² mod modulus` via CIOS — well-defined for
382    /// any `raw < R = 2^W`. Same body as `from_reduced` because the
383    /// underlying primitive already handles unreduced input.
384    fn from_value(integer: ModMathValue<T>, params: &ModMathParams<T, Nct>) -> Self {
385        Self::from_reduced(integer, params)
386    }
387}
388
389impl<T: ModMathInt> ModMathForm<T, Nct> {
390    fn pow_loop(&self, exp_raw: T) -> T {
391        let field = self.params.field();
392        let base = field.residue_from_mont(unwrap_value(&self.integer_mont));
393        field.exp(&base, &exp_raw).mont_value()
394    }
395
396    fn to_reduced(&self) -> T {
397        let field = self.params.field();
398        let r = field.residue_from_mont(unwrap_value(&self.integer_mont));
399        field.into_raw(&r)
400    }
401}
402
403impl<T: ModMathInt> Pow<ModMathParams<T, Nct>> for ModMathForm<T, Nct> {
404    fn pow(&self, exp: &ModMathValue<T>) -> Self {
405        let result_mont = self.pow_loop(unwrap_value(exp));
406        Self {
407            integer_mont: wrap_value(result_mont),
408            params: self.params.clone(),
409        }
410    }
411}
412
413impl<T: ModMathInt> PowBoundedExp<ModMathParams<T, Nct>> for ModMathForm<T, Nct> {
414    fn pow_bounded_exp(&self, exp: &ModMathValue<T>, _exp_bits: u32) -> Self {
415        // The LSB-first loop exits naturally when the exponent reaches zero,
416        // so the `_exp_bits` hint is unused here.
417        let result_mont = self.pow_loop(unwrap_value(exp));
418        Self {
419            integer_mont: wrap_value(result_mont),
420            params: self.params.clone(),
421        }
422    }
423
424    fn retrieve(&self) -> ModMathValue<T> {
425        wrap_value(self.to_reduced())
426    }
427}
428
429impl<T: ModMathInt> ModulusParams for ModMathParams<T, Nct> {
430    type Modulus = ModMathValue<T>;
431    type MontgomeryForm = ModMathForm<T, Nct>;
432
433    fn modulus(&self) -> &Odd<Self::Modulus> {
434        &self.modulus_odd
435    }
436
437    fn bits_precision(&self) -> u32 {
438        FixedWidthUnsignedInt::bits_precision(self.field.modulus())
439    }
440}
441
442impl<T: ModMathIntCt> IntoMontyForm<ModMathParams<T, Ct>> for ModMathForm<T, Ct> {
443    fn from_reduced(integer: ModMathValue<T>, params: &ModMathParams<T, Ct>) -> Self {
444        let field = params.field();
445        let r = field.reduce(unwrap_value_ref(&integer));
446        Self {
447            integer_mont: wrap_value(r.mont_value()),
448            params: params.clone(),
449        }
450    }
451
452    /// Same as the Nct variant: `FieldCt::reduce` uses `wide_montgomery_mul_ct`
453    /// with `R² mod modulus`, which handles arbitrary `raw < R = 2^W`.
454    fn from_value(integer: ModMathValue<T>, params: &ModMathParams<T, Ct>) -> Self {
455        Self::from_reduced(integer, params)
456    }
457}
458
459impl<T: ModMathIntCt> ModMathForm<T, Ct> {
460    fn pow_loop(&self, exp_raw: T) -> T {
461        let field = self.params.field();
462        let base = field.residue_from_mont(unwrap_value(&self.integer_mont));
463        field.exp_public_exp(&base, &exp_raw).mont_value()
464    }
465
466    fn to_reduced(&self) -> T {
467        let field = self.params.field();
468        let r = field.residue_from_mont(unwrap_value(&self.integer_mont));
469        field.into_raw(&r)
470    }
471}
472
473impl<T: ModMathIntCt> Pow<ModMathParams<T, Ct>> for ModMathForm<T, Ct> {
474    fn pow(&self, exp: &ModMathValue<T>) -> Self {
475        let result_mont = self.pow_loop(unwrap_value(exp));
476        Self {
477            integer_mont: wrap_value(result_mont),
478            params: self.params.clone(),
479        }
480    }
481}
482
483impl<T: ModMathIntCt> PowBoundedExp<ModMathParams<T, Ct>> for ModMathForm<T, Ct> {
484    fn pow_bounded_exp(&self, exp: &ModMathValue<T>, _exp_bits: u32) -> Self {
485        let result_mont = self.pow_loop(unwrap_value(exp));
486        Self {
487            integer_mont: wrap_value(result_mont),
488            params: self.params.clone(),
489        }
490    }
491
492    fn retrieve(&self) -> ModMathValue<T> {
493        wrap_value(self.to_reduced())
494    }
495}
496
497impl<T: ModMathIntCt> ModulusParams for ModMathParams<T, Ct> {
498    type Modulus = ModMathValue<T>;
499    type MontgomeryForm = ModMathForm<T, Ct>;
500
501    fn modulus(&self) -> &Odd<Self::Modulus> {
502        &self.modulus_odd
503    }
504
505    fn bits_precision(&self) -> u32 {
506        FixedWidthUnsignedInt::bits_precision(self.field.modulus())
507    }
508}
509
510#[cfg(test)]
511#[cfg(all(feature = "alloc", feature = "private-key"))]
512mod tests {
513    use fixed_bigint::{Ct, FixedUInt};
514    use rand::rngs::ChaCha8Rng;
515    use rand_core::SeedableRng;
516    use sha1::Sha1;
517    use signature::hazmat::PrehashVerifier;
518
519    use super::{
520        public_key_ct_from_be_bytes, public_key_from_be_bytes, ModMathParams, ModMathValue,
521    };
522    use crate::key::GenericRsaPublicKey;
523    use crate::pkcs1v15::{GenericEncryptingKey, GenericSignature, GenericVerifyingKey};
524    use crate::{traits::RandomizedEncryptor, BoxedUint, Pkcs1v15Encrypt, RsaPublicKey};
525
526    type SmallU = FixedUInt<u8, 64>;
527    type SmallUCt = FixedUInt<u8, 64, Ct>;
528
529    #[test]
530    fn brand_round_trip() {
531        let params = ModMathParams::<SmallU>::new(SmallU::from(13u8)).unwrap();
532        let f = params.field();
533        let r = f.reduce(&SmallU::from(7u8));
534        assert_eq!(f.into_raw(&r), SmallU::from(7u8));
535    }
536
537    #[test]
538    fn brand_mul_exp() {
539        let params = ModMathParams::<SmallU>::new(SmallU::from(13u8)).unwrap();
540        let f = params.field();
541        // 7 * 11 = 77 ≡ 12 (mod 13)
542        let a = f.reduce(&SmallU::from(7u8));
543        let b = f.reduce(&SmallU::from(11u8));
544        assert_eq!(f.into_raw(&f.mul(&a, &b)), SmallU::from(12u8));
545        // 2^10 = 1024 ≡ 10 (mod 13)
546        let base = f.reduce(&SmallU::from(2u8));
547        assert_eq!(
548            f.into_raw(&f.exp(&base, &SmallU::from(10u8))),
549            SmallU::from(10u8)
550        );
551    }
552
553    #[test]
554    fn brand_ct_matches_nct() {
555        let p_nct = ModMathParams::<SmallU>::new(SmallU::from(13u8)).unwrap();
556        let p_ct = ModMathParams::<SmallUCt, Ct>::new(SmallUCt::from(13u8)).unwrap();
557        let f_nct = p_nct.field();
558        let f_ct = p_ct.field();
559        let nct = f_nct.into_raw(&f_nct.mul(
560            &f_nct.reduce(&SmallU::from(7u8)),
561            &f_nct.reduce(&SmallU::from(11u8)),
562        ));
563        let ct = f_ct.into_raw(&f_ct.mul(
564            &f_ct.reduce(&SmallUCt::from(7u8)),
565            &f_ct.reduce(&SmallUCt::from(11u8)),
566        ));
567        // Distinct types — compare via underlying byte representation.
568        let mut nct_bytes = [0u8; 64];
569        let mut ct_bytes = [0u8; 64];
570        let _ = nct.to_be_bytes(&mut nct_bytes);
571        let _ = ct.to_be_bytes(&mut ct_bytes);
572        assert_eq!(nct_bytes, ct_bytes);
573    }
574
575    #[test]
576    fn verify_pkcs1v15_signature_with_modmath_fixed_uint() {
577        type U512 = FixedUInt<u8, 64>;
578
579        let digest: [u8; 20] = [
580            0x43, 0x0c, 0xe3, 0x4d, 0x02, 0x07, 0x24, 0xed, 0x75, 0xa1, 0x96, 0xdf, 0xc2, 0xad,
581            0x67, 0xc7, 0x77, 0x72, 0xd1, 0x69,
582        ];
583        let modulus: [u8; 64] = [
584            0x96, 0x9D, 0x03, 0xFF, 0xA9, 0x8D, 0x88, 0x8F, 0x3A, 0xA4, 0xF2, 0xFE, 0xD2, 0x32,
585            0xE6, 0x1C, 0x4A, 0xCF, 0x06, 0x63, 0xA9, 0x2F, 0x99, 0x03, 0x4C, 0xF7, 0xB7, 0x24,
586            0x5A, 0x1A, 0x1E, 0x5E, 0xAF, 0xA5, 0x65, 0xAF, 0xB9, 0x0B, 0xAB, 0x22, 0x85, 0x71,
587            0x2F, 0xAA, 0x50, 0x39, 0x39, 0xA0, 0x65, 0xFB, 0x60, 0xDD, 0x08, 0x28, 0xA3, 0x84,
588            0xF2, 0x6D, 0x8A, 0xFC, 0x28, 0x6D, 0xF6, 0xCF,
589        ];
590        let signature: [u8; 64] = [
591            0x45, 0x53, 0xF3, 0xAF, 0x16, 0xAF, 0x63, 0x97, 0xB0, 0xD3, 0x2F, 0x8A, 0xEC, 0xD5,
592            0x4C, 0xF1, 0xF3, 0xD0, 0x0C, 0x9F, 0x42, 0xDC, 0x68, 0xCB, 0xD7, 0x05, 0xCE, 0xA5,
593            0xA9, 0x70, 0x95, 0x3E, 0xC0, 0xBC, 0x4A, 0x18, 0xED, 0x91, 0xA3, 0x5D, 0x66, 0xEC,
594            0xDA, 0x4A, 0x83, 0x32, 0xCF, 0xC3, 0xA3, 0xAB, 0x21, 0xAD, 0x59, 0xB2, 0x2E, 0x87,
595            0xC2, 0x73, 0xFF, 0x08, 0x88, 0xDD, 0x4D, 0xE0,
596        ];
597
598        let key = public_key_from_be_bytes::<U512>(&modulus, 3).unwrap();
599        let verifying_key = GenericVerifyingKey::<Sha1, _, _>::new(key);
600        let signature =
601            GenericSignature::from(ModMathValue::from_inner(U512::from_be_bytes(&signature)));
602        verifying_key.verify_prehash(&digest, &signature).unwrap();
603    }
604
605    #[test]
606    fn verify_pkcs1v15_signature_with_modmath_fixed_uint32() {
607        type U512 = FixedUInt<u32, 16>;
608
609        let digest: [u8; 20] = [
610            0x43, 0x0c, 0xe3, 0x4d, 0x02, 0x07, 0x24, 0xed, 0x75, 0xa1, 0x96, 0xdf, 0xc2, 0xad,
611            0x67, 0xc7, 0x77, 0x72, 0xd1, 0x69,
612        ];
613        let modulus: [u8; 64] = [
614            0x96, 0x9D, 0x03, 0xFF, 0xA9, 0x8D, 0x88, 0x8F, 0x3A, 0xA4, 0xF2, 0xFE, 0xD2, 0x32,
615            0xE6, 0x1C, 0x4A, 0xCF, 0x06, 0x63, 0xA9, 0x2F, 0x99, 0x03, 0x4C, 0xF7, 0xB7, 0x24,
616            0x5A, 0x1A, 0x1E, 0x5E, 0xAF, 0xA5, 0x65, 0xAF, 0xB9, 0x0B, 0xAB, 0x22, 0x85, 0x71,
617            0x2F, 0xAA, 0x50, 0x39, 0x39, 0xA0, 0x65, 0xFB, 0x60, 0xDD, 0x08, 0x28, 0xA3, 0x84,
618            0xF2, 0x6D, 0x8A, 0xFC, 0x28, 0x6D, 0xF6, 0xCF,
619        ];
620        let signature: [u8; 64] = [
621            0x45, 0x53, 0xF3, 0xAF, 0x16, 0xAF, 0x63, 0x97, 0xB0, 0xD3, 0x2F, 0x8A, 0xEC, 0xD5,
622            0x4C, 0xF1, 0xF3, 0xD0, 0x0C, 0x9F, 0x42, 0xDC, 0x68, 0xCB, 0xD7, 0x05, 0xCE, 0xA5,
623            0xA9, 0x70, 0x95, 0x3E, 0xC0, 0xBC, 0x4A, 0x18, 0xED, 0x91, 0xA3, 0x5D, 0x66, 0xEC,
624            0xDA, 0x4A, 0x83, 0x32, 0xCF, 0xC3, 0xA3, 0xAB, 0x21, 0xAD, 0x59, 0xB2, 0x2E, 0x87,
625            0xC2, 0x73, 0xFF, 0x08, 0x88, 0xDD, 0x4D, 0xE0,
626        ];
627
628        let n = U512::from_be_bytes(&modulus);
629        let e = U512::from(3u8);
630        // Turbofish the personality: `ModMathParams::new` is ambiguous
631        // between the Nct and Ct impl blocks (the `P = Nct` default doesn't
632        // fire in inference contexts). Pin Nct explicitly.
633        let key = GenericRsaPublicKey::from_components(
634            ModMathValue::from_inner(n),
635            ModMathValue::from_inner(e),
636            ModMathParams::<U512, fixed_bigint::Nct>::new(n).unwrap(),
637        )
638        .unwrap();
639        let verifying_key = GenericVerifyingKey::<Sha1, _, _>::new(key);
640        let signature =
641            GenericSignature::from(ModMathValue::from_inner(U512::from_be_bytes(&signature)));
642        verifying_key.verify_prehash(&digest, &signature).unwrap();
643    }
644
645    #[test]
646    fn encrypt_pkcs1v15_with_modmath_fixed_uint_matches_boxeduint() {
647        // Encrypt path takes a secret plaintext, so type the modulus as
648        // Ct-personality — `CiosMontMulCt` only resolves for Ct-typed
649        // FixedUInts under the personality typestate.
650        type U512 = FixedUInt<u8, 64, Ct>;
651
652        let modulus: [u8; 64] = [
653            0x96, 0x9D, 0x03, 0xFF, 0xA9, 0x8D, 0x88, 0x8F, 0x3A, 0xA4, 0xF2, 0xFE, 0xD2, 0x32,
654            0xE6, 0x1C, 0x4A, 0xCF, 0x06, 0x63, 0xA9, 0x2F, 0x99, 0x03, 0x4C, 0xF7, 0xB7, 0x24,
655            0x5A, 0x1A, 0x1E, 0x5E, 0xAF, 0xA5, 0x65, 0xAF, 0xB9, 0x0B, 0xAB, 0x22, 0x85, 0x71,
656            0x2F, 0xAA, 0x50, 0x39, 0x39, 0xA0, 0x65, 0xFB, 0x60, 0xDD, 0x08, 0x28, 0xA3, 0x84,
657            0xF2, 0x6D, 0x8A, 0xFC, 0x28, 0x6D, 0xF6, 0xCF,
658        ];
659        let msg = b"hello world!";
660
661        let modmath_key = public_key_ct_from_be_bytes::<U512>(&modulus, 3).unwrap();
662        let boxed_key = RsaPublicKey::new(
663            BoxedUint::from_be_slice(&modulus, 512).unwrap(),
664            3u64.into(),
665        )
666        .unwrap();
667
668        let mut modmath_rng = ChaCha8Rng::from_seed([42; 32]);
669        let mut boxed_rng = ChaCha8Rng::from_seed([42; 32]);
670        let mut storage = [0u8; 64];
671
672        let modmath_ciphertext = GenericEncryptingKey::new(modmath_key)
673            .encrypt_with_rng_into(&mut modmath_rng, msg, &mut storage)
674            .unwrap();
675        let boxed_ciphertext = boxed_key
676            .encrypt(&mut boxed_rng, Pkcs1v15Encrypt, msg)
677            .unwrap();
678
679        assert_eq!(modmath_ciphertext, boxed_ciphertext.as_slice());
680    }
681}