Skip to main content

tecdsa_core/rsa/
tough.rs

1//! Algorithm 3: SampleRSA / SampleDF.
2//!
3//! A *tough prime* is a prime `p = 2·q_1⋯q_k + 1` where `q_1..q_k` are distinct primes of
4//! about `tough_bits` bits each (`k = tough_count`; the last factor is drawn from the
5//! interval that makes `p` exactly the requested length, so it may deviate slightly in size).
6//! With `k = 1` this is a safe prime.
7//!
8//! * `SampleRSA`: `N = p1·p2`, `p1 ≡ 3`, `p2 ≡ 7 (mod 8)`, `gcd(p1−1, p2−1) = 2`
9//!   (the prime sets of `(p1−1)/2` and `(p2−1)/2` are disjoint).
10//! * `SampleDF`: `N̂` as above, `t` a random square in `Z*_N̂`, `s_i = t^{λ_i}`,
11//!   `λ_i ← [2^{2ℓ}]`. The factorization and `λ_i` are returned in [`DfSecret`] so that the
12//!   caller can drop (and thereby zeroize) them after the proof (FR-S4).
13//!
14//! All sizes are in bits. Sampling is variable-time: it only handles values that are
15//! rejected or published (candidates), and the final primes stay inside zeroizing structs.
16
17use alloc::vec::Vec;
18use core::fmt;
19
20use crypto_bigint::modular::{BoxedMontyForm, BoxedMontyParams};
21use crypto_bigint::{
22    BoxedUint, ConcatenatingMul, Gcd, Limb, NonZero, RandomBits, RandomMod, Resize,
23};
24use crypto_primes::{Flavor, is_prime, random_prime};
25use rand_core::CryptoRng;
26use zeroize::{Zeroize, ZeroizeOnDrop};
27
28use crate::params::Params;
29
30/// Smallest accepted size of a prime factor.
31const MIN_PRIME_BITS: u32 = 32;
32/// Number of small primes used for trial-division pre-sieving.
33const SIEVE_LIMIT: u32 = 16384;
34/// Retries for the first `k−1` factors before giving up.
35const MAX_PREFIX_ATTEMPTS: u32 = 100_000;
36
37/// Errors of the tough-prime samplers.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum ToughError {
40    /// The requested sizes are inconsistent (no room for `p'`, bad residue, too small).
41    BadSize,
42}
43
44impl fmt::Display for ToughError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            ToughError::BadSize => f.write_str("inconsistent tough-prime sizes"),
48        }
49    }
50}
51
52/// Shape of tough primes: `p = 2·∏ q_i + 1`.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct ToughSpec {
55    /// Number of primes `q_i` in `(p−1)/2`.
56    pub count: u32,
57    /// Bit size of each `q_i` (`0`: unconstrained, only allowed with `count == 1`).
58    pub bits: u32,
59}
60
61impl ToughSpec {
62    /// Spec from a parameter profile.
63    pub const fn from_params(params: &Params) -> Self {
64        Self {
65            count: params.tough_count,
66            bits: params.tough_bits,
67        }
68    }
69    /// Safe primes: `(p−1)/2` is a single prime.
70    pub const SAFE: Self = Self { count: 1, bits: 0 };
71}
72
73/// A tough prime together with its factorization of `(p−1)/2`. Zeroized on drop.
74#[derive(ZeroizeOnDrop)]
75pub struct ToughPrime {
76    p: BoxedUint,
77    factors: Vec<BoxedUint>,
78}
79
80impl ToughPrime {
81    /// The prime `p`.
82    pub fn p(&self) -> &BoxedUint {
83        &self.p
84    }
85    /// The primes `q_i` with `(p−1)/2 = ∏ q_i`.
86    pub fn factors(&self) -> &[BoxedUint] {
87        &self.factors
88    }
89    /// `(p−1)/2`.
90    pub fn half(&self) -> BoxedUint {
91        self.factors
92            .iter()
93            .fold(BoxedUint::one(), |a, q| a.concatenating_mul(q))
94    }
95}
96
97impl fmt::Debug for ToughPrime {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.write_str("ToughPrime(***)")
100    }
101}
102
103/// Secret part of an RSA modulus. Zeroized on drop; deliberately not `Clone`.
104#[derive(ZeroizeOnDrop)]
105pub struct RsaSecret {
106    pub p1: ToughPrime,
107    pub p2: ToughPrime,
108}
109
110impl fmt::Debug for RsaSecret {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.write_str("RsaSecret(***)")
113    }
114}
115
116/// Output of `SampleRSA`.
117pub struct RsaModulus {
118    /// Public modulus `N = p1·p2`.
119    pub n: BoxedUint,
120    /// Factorization.
121    pub secret: RsaSecret,
122}
123
124impl fmt::Debug for RsaModulus {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "RsaModulus {{ n: {:?}, secret: *** }}", self.n.bits())
127    }
128}
129
130/// Secret part of a DF tuple: factorization of `N̂` and the exponents. Zeroized on drop.
131#[derive(ZeroizeOnDrop)]
132pub struct DfSecret {
133    pub rsa: RsaSecret,
134    pub lambda1: BoxedUint,
135    pub lambda2: BoxedUint,
136}
137
138impl fmt::Debug for DfSecret {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        f.write_str("DfSecret(***)")
141    }
142}
143
144/// Output of `SampleDF`: public tuple `(N̂, t, s1, s2)` and the secret witness.
145pub struct DfTuple {
146    pub n_hat: BoxedUint,
147    pub t: BoxedUint,
148    pub s1: BoxedUint,
149    pub s2: BoxedUint,
150    pub secret: DfSecret,
151}
152
153impl fmt::Debug for DfTuple {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(
156            f,
157            "DfTuple {{ n_hat: {:?} bits, secret: *** }}",
158            self.n_hat.bits()
159        )
160    }
161}
162
163fn small_primes() -> Vec<NonZero<Limb>> {
164    let mut sieve = alloc::vec![true; SIEVE_LIMIT as usize + 1];
165    let mut out = Vec::new();
166    for i in 2..=SIEVE_LIMIT {
167        if sieve[i as usize] {
168            if let Some(nz) = Option::from(NonZero::new(Limb::from_u32(i))) {
169                out.push(nz);
170            }
171            let mut j = i * i;
172            while j <= SIEVE_LIMIT {
173                sieve[j as usize] = false;
174                j += i;
175            }
176        }
177    }
178    out
179}
180
181const _: () = assert!(Limb::BITS == 64);
182
183/// `x mod s` for a small `s < 2^20` using word-wise Horner (variable time, public/candidate data).
184fn mod_small(x: &BoxedUint, s: u64) -> u64 {
185    let a = (1u64 << 32) % s;
186    let r_word = a * a % s; // 2^64 mod s
187    x.as_words()
188        .iter()
189        .rev()
190        .fold(0u64, |r, &w| (r * r_word % s + w % s) % s)
191}
192
193fn divisible_by_small(x: &BoxedUint, primes: &[NonZero<Limb>]) -> bool {
194    primes.iter().any(|q| mod_small(x, q.get().0) == 0)
195}
196
197/// Temporary pool of secret primes.
198#[derive(ZeroizeOnDrop)]
199struct Pool(Vec<BoxedUint>);
200
201/// Binomial coefficient C(n, k), saturating.
202fn binom_sat(n: u32, k: u32) -> u128 {
203    let mut r: u128 = 1;
204    for i in 0..k {
205        r = r.saturating_mul(u128::from(n - i)) / u128::from(i + 1);
206    }
207    r
208}
209
210/// Random prime in `[qlo, qhi]` with `q ≡ low2 (mod 4)`: pick a random start and search
211/// the arithmetic progression `q0 + 4i` with an incremental small-prime sieve window.
212fn random_prime_in<R: CryptoRng + ?Sized>(
213    rng: &mut R,
214    qlo: &BoxedUint,
215    qhi: &BoxedUint,
216    low2: u64,
217    sieve: &[NonZero<Limb>],
218) -> Option<BoxedUint> {
219    const W: usize = 2048;
220    let one = BoxedUint::one();
221    let span = BoxedUint::from(4 * W as u64);
222    let width = qhi.wrapping_sub(qlo).wrapping_sub(&span).wrapping_add(&one);
223    let width_nz = Option::<NonZero<BoxedUint>>::from(width.to_nz())?;
224    let mut marks = alloc::vec![false; W];
225    loop {
226        let mut q0 = BoxedUint::random_mod_vartime(rng, &width_nz).wrapping_add(qlo);
227        let cur = q0.as_words()[0] & 3;
228        q0 = q0
229            .wrapping_sub(BoxedUint::from(cur))
230            .wrapping_add(BoxedUint::from(low2));
231        if q0 < *qlo {
232            continue;
233        }
234        marks.iter_mut().for_each(|m| *m = false);
235        for sp in sieve.iter().skip(1) {
236            let s = sp.get().0;
237            let r = mod_small(&q0, s);
238            let inv2 = s.div_ceil(2);
239            let inv4 = inv2 * inv2 % s;
240            let mut i = ((s - r) % s) * inv4 % s;
241            while (i as usize) < W {
242                marks[i as usize] = true;
243                i += s;
244            }
245        }
246        for (i, m) in marks.iter().enumerate() {
247            if *m {
248                continue;
249            }
250            let q = q0.wrapping_add(BoxedUint::from(4 * i as u64));
251            if q > *qhi {
252                break;
253            }
254            if is_prime(Flavor::Any, &q) {
255                return Some(q);
256            }
257        }
258    }
259}
260
261/// Algorithm 1/3 of the paper: a pool `B` of ~`2ℓ`-bit primes, then random size-`k` subsets
262/// `S` until `2·∏_{S} q + 1` is prime. Candidates are screened by residues mod small primes
263/// computed from per-pool-prime residues (no big multiplication), and only survivors get a
264/// product and a Miller–Rabin/Lucas test. Pool primes are drawn from a narrow band so that
265/// the product lands in `[3·2^(bits−2), 2^bits)` with high probability.
266fn sample_from_pool<R: CryptoRng + ?Sized>(
267    rng: &mut R,
268    bits: u32,
269    t: u64,
270    spec: ToughSpec,
271    sieve: &[NonZero<Limb>],
272) -> Result<ToughPrime, ToughError> {
273    let k = spec.count;
274    let b = spec.bits;
275    let ku = k as usize;
276    let prec = bits.div_ceil(64) * 64;
277    let wide = prec + 64;
278    let one = BoxedUint::one_with_precision(wide);
279    let lo = BoxedUint::from(3u8).resize(wide).shl(bits - 2);
280    let hi = one.shl(bits);
281    // Target of ∏q: the middle of (lo/2, hi/2).
282    let mid = lo.wrapping_add(&hi).shr(2);
283
284    // Pool size: C(B,k) ≥ 6·bits subsets.
285    let mut pool_n = k + 1;
286    while binom_sat(pool_n, k) < 6 * u128::from(bits) {
287        pool_n += 1;
288    }
289    let pn = pool_n as usize;
290
291    // Largest j ∈ [0,255] with ((256+j)·2^(b−9))^k ≤ mid, fixing the band of the pool.
292    let pow_k = |j: u32| -> BoxedUint {
293        let q0 = BoxedUint::from(256 + j).resize(wide).shl(b - 9);
294        let mut acc = one.clone();
295        for _ in 0..k {
296            acc = acc.concatenating_mul(&q0).resize(wide + 64 * k);
297        }
298        acc
299    };
300    let mid_big = mid.resize(wide + 64 * k);
301    let (mut jl, mut jh) = (0u32, 255u32);
302    while jl < jh {
303        let m = (jl + jh).div_ceil(2);
304        if pow_k(m) <= mid_big {
305            jl = m;
306        } else {
307            jh = m - 1;
308        }
309    }
310    let qlo = BoxedUint::from(256 + jl).resize(wide).shl(b - 9);
311    let qhi = qlo.wrapping_add(qlo.shr(6));
312    let q_max = one.shl(b).wrapping_sub(&one);
313    let qhi = if qhi > q_max { q_max } else { qhi };
314
315    let primes32: Vec<u32> = sieve.iter().map(|x| x.get().0 as u32).collect();
316    let tries_per_pool = 16 * bits;
317    let mut resid = alloc::vec![0u32; primes32.len() * pn];
318
319    for _ in 0..MAX_PREFIX_ATTEMPTS.min(64) {
320        // Pool: for p ≡ 3 (8) all q ≡ 1 (4); for p ≡ 7 (8) half of them are ≡ 3 (4).
321        let mut pool = Pool(Vec::with_capacity(pn));
322        while pool.0.len() < pn {
323            let low2 = if t == 1 || pool.0.len() % 2 == 0 {
324                1
325            } else {
326                3
327            };
328            let q = random_prime_in(rng, &qlo, &qhi, low2, sieve).ok_or(ToughError::BadSize)?;
329            if !pool.0.contains(&q) {
330                pool.0.push(q);
331            }
332        }
333        let mod4: Vec<u64> = pool.0.iter().map(|q| q.as_words()[0] & 3).collect();
334        for (i, sp) in sieve.iter().enumerate() {
335            for (j, q) in pool.0.iter().enumerate() {
336                resid[i * pn + j] = mod_small(q, sp.get().0) as u32;
337            }
338        }
339        let mut idx: Vec<usize> = (0..pn).collect();
340        let mut seen: Vec<u64> = Vec::new();
341        for _ in 0..tries_per_pool {
342            // Random k-subset (partial Fisher–Yates).
343            for i in 0..ku {
344                let r = rng.next_u32() as usize % (pn - i);
345                idx.swap(i, i + r);
346            }
347            let sub = &idx[..ku];
348            let prod4 = sub.iter().fold(1u64, |a, &j| (a * mod4[j]) & 3);
349            if prod4 != t {
350                continue;
351            }
352            let mut ok = true;
353            for (i, &sp) in primes32.iter().enumerate() {
354                let sp = u64::from(sp);
355                let mut r = 2u64;
356                for &j in sub {
357                    r = r * u64::from(resid[i * pn + j]) % sp;
358                }
359                if r + 1 == sp {
360                    ok = false;
361                    break;
362                }
363            }
364            if !ok {
365                continue;
366            }
367            // Subset id (bitmask) to avoid retesting; pool sizes are < 64.
368            let mask = sub.iter().fold(0u64, |a, &j| a | (1u64 << (j % 64)));
369            if pn <= 64 {
370                if seen.contains(&mask) {
371                    continue;
372                }
373                seen.push(mask);
374            }
375            let m = sub.iter().fold(one.clone(), |a, &j| {
376                a.concatenating_mul(&pool.0[j]).resize(wide)
377            });
378            let p = m.shl(1).wrapping_add(&one);
379            if p < lo || p >= hi {
380                continue;
381            }
382            let p = p.resize(prec);
383            if !is_prime(Flavor::Any, &p) {
384                continue;
385            }
386            let factors = sub
387                .iter()
388                .map(|&j| pool.0[j].clone().resize(prec))
389                .collect();
390            return Ok(ToughPrime { p, factors });
391        }
392    }
393    Err(ToughError::BadSize)
394}
395
396/// Samples a tough prime `p = 2·∏q_i + 1` with `p ∈ [3·2^(bits−2), 2^bits)` (exactly
397/// `bits` bits, top two bits set so that products of two such primes have full length)
398/// and `p ≡ residue_mod8 (mod 8)`.
399fn sample_tough_prime_inner<R: CryptoRng + ?Sized>(
400    rng: &mut R,
401    bits: u32,
402    residue_mod8: u8,
403    spec: ToughSpec,
404    sieve: &[NonZero<Limb>],
405) -> Result<ToughPrime, ToughError> {
406    if residue_mod8 != 3 && residue_mod8 != 7 {
407        return Err(ToughError::BadSize);
408    }
409    let k = spec.count;
410    let b = spec.bits;
411    if k == 0 || bits < 2 * MIN_PRIME_BITS || (b == 0 && k != 1) || (b != 0 && b < MIN_PRIME_BITS) {
412        return Err(ToughError::BadSize);
413    }
414    // Feasibility: 2·∏q_i + 1 must be able to land in [3·2^(bits−2), 2^bits).
415    if b != 0 && (k * (b - 1) + 1 >= bits || k * b + 1 < bits) {
416        return Err(ToughError::BadSize);
417    }
418    // (p−1)/2 mod 4: p ≡ 3 (8) -> 1; p ≡ 7 (8) -> 3.
419    let t: u64 = if residue_mod8 == 3 { 1 } else { 3 };
420    if k >= 2 {
421        return sample_from_pool(rng, bits, t, spec, sieve);
422    }
423
424    let prec = bits.div_ceil(64) * 64;
425    let wide = prec + 64;
426    let one = BoxedUint::one_with_precision(wide);
427    let lo = BoxedUint::from(3u8).resize(wide).shl(bits - 2);
428    let hi = one.shl(bits);
429    let lo_m1 = lo.wrapping_sub(&one);
430    let hi_m2 = hi.wrapping_sub(BoxedUint::from(2u8));
431    let q_floor = if b == 0 {
432        BoxedUint::zero_with_precision(wide)
433    } else {
434        one.shl(b - 1)
435    };
436    let q_ceil = if b == 0 {
437        None
438    } else {
439        Some(one.shl(b).wrapping_sub(&one))
440    };
441
442    'outer: for _ in 0..MAX_PREFIX_ATTEMPTS {
443        let mut factors: Vec<BoxedUint> = Vec::new();
444        for _ in 0..k - 1 {
445            let q: BoxedUint = random_prime(rng, Flavor::Any, b);
446            if factors.contains(&q) {
447                continue 'outer;
448            }
449            factors.push(q);
450        }
451        let m = factors
452            .iter()
453            .fold(one.clone(), |a, q| a.concatenating_mul(q).resize(wide));
454        let two_m = m.shl(1);
455        let Some(two_m_nz) = Option::<NonZero<BoxedUint>>::from(two_m.to_nz()) else {
456            return Err(ToughError::BadSize);
457        };
458        // Range of the last factor q: 2·m·q + 1 ∈ [lo, hi).
459        let (qlo, rem) = lo_m1.div_rem_vartime(&two_m_nz);
460        let qlo = if bool::from(rem.is_zero()) {
461            qlo
462        } else {
463            qlo.wrapping_add(&one)
464        };
465        let qhi = hi_m2.div_rem_vartime(&two_m_nz).0;
466        let (qlo, qhi) = match &q_ceil {
467            Some(c) => (
468                if qlo < q_floor { q_floor.clone() } else { qlo },
469                if qhi > *c { c.clone() } else { qhi },
470            ),
471            None => (qlo, qhi),
472        };
473        if qlo > qhi {
474            continue;
475        }
476        let width = qhi.wrapping_sub(&qlo).wrapping_add(&one);
477        let Some(width_nz) = Option::<NonZero<BoxedUint>>::from(width.to_nz()) else {
478            continue;
479        };
480        let m_mod4 = m.as_words()[0] & 3;
481        let low2 = (t * m_mod4) & 3; // q ≡ t·m (mod 4) gives q·m ≡ t since m² ≡ 1
482        loop {
483            let r = BoxedUint::random_mod_vartime(rng, &width_nz);
484            let mut q = r.wrapping_add(&qlo);
485            // Adjust the two low bits.
486            let cur = q.as_words()[0] & 3;
487            q = q
488                .wrapping_sub(BoxedUint::from(cur))
489                .wrapping_add(BoxedUint::from(low2));
490            if q < qlo || q > qhi {
491                // Possible only at interval edges; draw again.
492                continue;
493            }
494            if divisible_by_small(&q, sieve) {
495                continue;
496            }
497            let p = two_m
498                .concatenating_mul(&q)
499                .wrapping_add(BoxedUint::one())
500                .resize(prec);
501            if divisible_by_small(&p, sieve) || factors.contains(&q) {
502                continue;
503            }
504            if !is_prime(Flavor::Any, &p) || !is_prime(Flavor::Any, &q) {
505                continue;
506            }
507            factors.push(q.resize(prec));
508            return Ok(ToughPrime { p, factors });
509        }
510    }
511    Err(ToughError::BadSize)
512}
513
514/// Samples a tough prime of exactly `bits` bits, `p = 2·∏q_i + 1`, with
515/// `p ≡ residue_mod8 (mod 8)`, `residue_mod8 ∈ {3, 7}`. The top two bits of `p` are set.
516pub fn sample_tough_prime<R: CryptoRng + ?Sized>(
517    rng: &mut R,
518    bits: u32,
519    residue_mod8: u8,
520    spec: ToughSpec,
521) -> Result<ToughPrime, ToughError> {
522    sample_tough_prime_inner(rng, bits, residue_mod8, spec, &small_primes())
523}
524
525/// `SampleRSA` with explicit sizes: `N` has exactly `n_bits` bits (`n_bits` even).
526pub fn sample_rsa_with<R: CryptoRng + ?Sized>(
527    rng: &mut R,
528    n_bits: u32,
529    spec: ToughSpec,
530) -> Result<RsaModulus, ToughError> {
531    if n_bits % 2 != 0 || n_bits < 2 * MIN_PRIME_BITS + 8 {
532        return Err(ToughError::BadSize);
533    }
534    let bits = n_bits / 2;
535    let sieve = small_primes();
536    let p1 = sample_tough_prime_inner(rng, bits, 3, spec, &sieve)?;
537    let h1 = p1.half();
538    let p2 = loop {
539        let cand = sample_tough_prime_inner(rng, bits, 7, spec, &sieve)?;
540        let g = h1.gcd(&cand.half());
541        if g.is_one().into() {
542            break cand;
543        }
544    };
545    // Both primes are in [3·2^(bits−2), 2^bits), so N has exactly n_bits bits.
546    let n = p1.p().concatenating_mul(p2.p());
547    if n.bits() != n_bits {
548        return Err(ToughError::BadSize);
549    }
550    Ok(RsaModulus {
551        n,
552        secret: RsaSecret { p1, p2 },
553    })
554}
555
556/// `SampleRSA` for the server Paillier modulus (`params.n_rsa` bits, tough primes).
557pub fn sample_rsa<R: CryptoRng + ?Sized>(
558    rng: &mut R,
559    params: &Params,
560) -> Result<RsaModulus, ToughError> {
561    sample_rsa_with(rng, params.n_rsa, ToughSpec::from_params(params))
562}
563
564/// `SampleDF` with explicit sizes: modulus of `n_bits` bits, exponents `λ_i ← [2^{2ℓ}]`.
565pub fn sample_df_with<R: CryptoRng + ?Sized>(
566    rng: &mut R,
567    n_bits: u32,
568    spec: ToughSpec,
569    ell: u32,
570) -> Result<DfTuple, ToughError> {
571    if ell == 0 {
572        return Err(ToughError::BadSize);
573    }
574    let rsa = sample_rsa_with(rng, n_bits, spec)?;
575    let RsaModulus { n, secret } = rsa;
576    let odd = Option::from(n.to_odd()).ok_or(ToughError::BadSize)?;
577    let mparams = BoxedMontyParams::new(odd);
578    let nz = Option::<NonZero<BoxedUint>>::from(n.to_nz()).ok_or(ToughError::BadSize)?;
579
580    // t = r² for a random unit r.
581    let t_form = loop {
582        let mut r = BoxedUint::random_mod_vartime(rng, &nz);
583        let unit: bool = r.gcd(&n).is_one().into();
584        if unit {
585            let f = BoxedMontyForm::new(r.clone(), &mparams).square();
586            r.zeroize();
587            break f;
588        }
589        r.zeroize();
590    };
591    let t = t_form.retrieve();
592
593    let exp_bits = 2 * ell;
594    let lambda1 = BoxedUint::random_bits(rng, exp_bits);
595    let lambda2 = BoxedUint::random_bits(rng, exp_bits);
596    let s1 = t_form.pow_bounded_exp(&lambda1, exp_bits).retrieve();
597    let s2 = t_form.pow_bounded_exp(&lambda2, exp_bits).retrieve();
598
599    Ok(DfTuple {
600        n_hat: n,
601        t,
602        s1,
603        s2,
604        secret: DfSecret {
605            rsa: secret,
606            lambda1,
607            lambda2,
608        },
609    })
610}
611
612/// `SampleDF` for the server (`params.n_rsa` bits, tough primes, `ℓ = params.ell`).
613pub fn sample_df<R: CryptoRng + ?Sized>(
614    rng: &mut R,
615    params: &Params,
616) -> Result<DfTuple, ToughError> {
617    sample_df_with(
618        rng,
619        params.n_rsa,
620        ToughSpec::from_params(params),
621        params.ell,
622    )
623}
624
625/// `SampleDF` for the client's ephemeral tuple (`params.n_client` bits). Tough primes of
626/// the server profile do not fit into 1024-bit primes, so safe primes are used.
627// TODO(spec §5/§6.2): confirm the client's prime structure with the paper.
628pub fn sample_df_client<R: CryptoRng + ?Sized>(
629    rng: &mut R,
630    params: &Params,
631) -> Result<DfTuple, ToughError> {
632    // Paper (Algorithm 3): T = ⌈n/4ℓ⌉ primes of 2ℓ bits, same as the server.
633    let spec = ToughSpec {
634        count: params.n_client.div_ceil(4 * params.ell),
635        bits: 2 * params.ell,
636    };
637    sample_df_with(rng, params.n_client, spec, params.ell)
638}
639
640#[cfg(test)]
641mod tests {
642    #![allow(clippy::unwrap_used)]
643    extern crate std;
644    use super::*;
645    use crate::params::PARAMS_128;
646    use rand_chacha::ChaCha8Rng;
647    use rand_core::SeedableRng;
648
649    const SPEC: ToughSpec = ToughSpec { count: 4, bits: 64 };
650
651    fn bit_is_set(x: &BoxedUint, i: u32) -> bool {
652        bool::from(x.bit(i))
653    }
654
655    fn prime(x: &BoxedUint) -> bool {
656        is_prime(Flavor::Any, x)
657    }
658
659    fn mod8(x: &BoxedUint) -> u64 {
660        x.as_words()[0] & 7
661    }
662
663    fn check_prime_structure(tp: &ToughPrime, bits: u32, spec: ToughSpec, res: u64) {
664        let p = tp.p();
665        assert_eq!(p.bits(), bits);
666        assert!(bit_is_set(p, bits - 1) && bit_is_set(p, bits - 2));
667        assert!(prime(p));
668        assert_eq!(mod8(p), res);
669        assert_eq!(tp.factors().len(), spec.count as usize);
670        for q in tp.factors() {
671            if spec.bits != 0 {
672                // all but the last factor have exactly `bits` bits; the last one is close
673                assert!(q.bits() + 8 >= spec.bits && q.bits() <= spec.bits);
674            }
675            assert!(prime(q));
676        }
677        let expect = tp.half().shl(1).wrapping_add(BoxedUint::one());
678        assert_eq!(&expect, p);
679    }
680
681    #[test]
682    fn tough_prime_structure_both_residues() {
683        let mut rng = ChaCha8Rng::seed_from_u64(1);
684        for res in [3u8, 7u8] {
685            let tp = sample_tough_prime(&mut rng, 256, res, SPEC).unwrap();
686            check_prime_structure(&tp, 256, SPEC, res as u64);
687        }
688    }
689
690    #[test]
691    fn safe_prime_when_count_one() {
692        let mut rng = ChaCha8Rng::seed_from_u64(2);
693        let tp = sample_tough_prime(&mut rng, 128, 7, ToughSpec::SAFE).unwrap();
694        check_prime_structure(&tp, 128, ToughSpec::SAFE, 7);
695    }
696
697    #[test]
698    fn rejects_bad_sizes() {
699        let mut rng = ChaCha8Rng::seed_from_u64(3);
700        assert!(sample_tough_prime(&mut rng, 256, 5, SPEC).is_err());
701        let big = ToughSpec {
702            count: 6,
703            bits: 256,
704        };
705        assert!(sample_tough_prime(&mut rng, 1024, 3, big).is_err());
706        assert!(sample_rsa_with(&mut rng, 513, SPEC).is_err());
707    }
708
709    #[test]
710    fn rsa_small() {
711        let mut rng = ChaCha8Rng::seed_from_u64(4);
712        let bits = 512;
713        let m = sample_rsa_with(&mut rng, bits, SPEC).unwrap();
714        assert_eq!(m.n.bits(), bits);
715        check_prime_structure(&m.secret.p1, bits / 2, SPEC, 3);
716        check_prime_structure(&m.secret.p2, bits / 2, SPEC, 7);
717        assert_eq!(m.secret.p1.p().concatenating_mul(m.secret.p2.p()), m.n);
718        // gcd(p1-1, p2-1) = 2
719        let one = BoxedUint::one();
720        let a = m.secret.p1.p().wrapping_sub(&one);
721        let b = m.secret.p2.p().wrapping_sub(&one);
722        assert_eq!(a.gcd(&b), BoxedUint::from(2u8));
723        // small-prime sets disjoint
724        for q in m.secret.p1.factors() {
725            assert!(!m.secret.p2.factors().contains(q));
726        }
727        assert!(!alloc::format!("{:?}", m).contains(&alloc::format!("{:x}", m.secret.p1.p())));
728        assert_eq!(alloc::format!("{:?}", m.secret), "RsaSecret(***)");
729    }
730
731    #[test]
732    fn df_small() {
733        let mut rng = ChaCha8Rng::seed_from_u64(5);
734        let ell = 16;
735        let d = sample_df_with(&mut rng, 512, SPEC, ell).unwrap();
736        assert_eq!(d.n_hat.bits(), 512);
737        assert!(d.secret.lambda1.bits() <= 2 * ell && d.secret.lambda2.bits() <= 2 * ell);
738        let odd = Option::from(d.n_hat.to_odd()).unwrap();
739        let mp = BoxedMontyParams::new(odd);
740        let t = BoxedMontyForm::new(d.t.clone(), &mp);
741        assert_eq!(t.pow(&d.secret.lambda1).retrieve(), d.s1);
742        assert_eq!(t.pow(&d.secret.lambda2).retrieve(), d.s2);
743        // t is a square: t^((p-1)/2) = 1 mod each prime, check via QR mod p1 and p2.
744        for p in [d.secret.rsa.p1.p(), d.secret.rsa.p2.p()] {
745            let pm = Option::from(p.to_odd()).unwrap();
746            let pp = BoxedMontyParams::new(pm);
747            let tp = BoxedMontyForm::new(d.t.rem(&Option::from(p.to_nz()).unwrap()), &pp);
748            let e = p.wrapping_sub(BoxedUint::one()).shr(1);
749            assert!(bool::from(tp.pow(&e).retrieve().is_one()));
750        }
751    }
752
753    #[test]
754    #[ignore = "slow in debug: full-size 3072-bit SampleRSA, run with --release"]
755    fn rsa_full_size() {
756        let mut rng = ChaCha8Rng::seed_from_u64(6);
757        let start = std::time::Instant::now();
758        let m = sample_rsa(&mut rng, &PARAMS_128).unwrap();
759        std::println!("SampleRSA 3072: {:?}", start.elapsed());
760        assert_eq!(m.n.bits(), 3072);
761        let spec = ToughSpec::from_params(&PARAMS_128);
762        check_prime_structure(&m.secret.p1, 1536, spec, 3);
763        check_prime_structure(&m.secret.p2, 1536, spec, 7);
764        let one = BoxedUint::one();
765        let g = m
766            .secret
767            .p1
768            .p()
769            .wrapping_sub(&one)
770            .gcd(&m.secret.p2.p().wrapping_sub(&one));
771        assert_eq!(g, BoxedUint::from(2u8));
772    }
773}
774
775#[cfg(test)]
776mod client_df_tests {
777    #![allow(clippy::unwrap_used)]
778    extern crate std;
779    use super::*;
780    use crate::params::PARAMS_128;
781    use rand_chacha::ChaCha20Rng;
782    use rand_core::SeedableRng;
783
784    #[test]
785    #[ignore = "full-size: run with --release"]
786    fn client_df_full_size() {
787        let mut rng = ChaCha20Rng::seed_from_u64(5);
788        let t = std::time::Instant::now();
789        let df = sample_df_client(&mut rng, &PARAMS_128).unwrap();
790        std::eprintln!("client DF (2048-bit tough): {:?}", t.elapsed());
791        assert_eq!(df.n_hat.bits(), PARAMS_128.n_client);
792    }
793
794    #[test]
795    #[ignore = "benchmark: run with --release --nocapture"]
796    fn bench_median_of_10() {
797        let mut c = std::vec::Vec::new();
798        let mut r = std::vec::Vec::new();
799        for seed in 0..10u64 {
800            let mut rng = ChaCha20Rng::seed_from_u64(100 + seed);
801            let t = std::time::Instant::now();
802            let _ = sample_df_client(&mut rng, &PARAMS_128).unwrap();
803            c.push(t.elapsed());
804            let t = std::time::Instant::now();
805            let _ = sample_rsa(&mut rng, &PARAMS_128).unwrap();
806            r.push(t.elapsed());
807        }
808        c.sort();
809        r.sort();
810        std::eprintln!(
811            "client2048 median {:?} max {:?}; server3072 median {:?} max {:?}",
812            c[5],
813            c[9],
814            r[5],
815            r[9]
816        );
817    }
818}