From 9dfe54d4377cfc710d70ed81922690055c2d0223 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 24 Jan 2025 12:02:42 -0800 Subject: [PATCH] Allow custom MSB setting in generated primes --- CHANGELOG.md | 3 + benches/bench.rs | 12 ++-- src/hazmat.rs | 2 +- src/hazmat/miller_rabin.rs | 4 +- src/hazmat/sieve.rs | 122 +++++++++++++++++++++++++++++-------- src/presets.rs | 30 +++++---- 6 files changed, 128 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32fe345..58c7ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed `Sieve` to `SmallPrimesSieve`. ([#64]) - Bumped `crypto-bigint` to 0.6.0-rc.7 and MSRV to 1.83. ([#67]) - Bumped `crypto-bigint` to 0.6. ([#68]) +- `random_odd_integer()` takes an additional `SetBits` argument. ([#69]) +- `random_odd_integer()` now returns a `Result` instead of panicking. ([#69]) ### Added @@ -23,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#64]: https://github.com/entropyxyz/crypto-primes/pull/64 [#67]: https://github.com/entropyxyz/crypto-primes/pull/67 [#68]: https://github.com/entropyxyz/crypto-primes/pull/68 +[#69]: https://github.com/entropyxyz/crypto-primes/pull/69 ## [0.6.0-pre.2] - 2024-10-18 diff --git a/benches/bench.rs b/benches/bench.rs index acbde65..b0f8d3b 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -17,7 +17,7 @@ use rand_core::RngCore; use crypto_primes::{ generate_prime_with_rng, generate_safe_prime_with_rng, hazmat::{ - lucas_test, random_odd_integer, AStarBase, BruteForceBase, LucasCheck, MillerRabin, SelfridgeBase, + lucas_test, random_odd_integer, AStarBase, BruteForceBase, LucasCheck, MillerRabin, SelfridgeBase, SetBits, SmallPrimesSieve, }, is_prime_with_rng, is_safe_prime_with_rng, @@ -37,7 +37,7 @@ fn make_random_rng() -> ChaCha8Rng { } fn random_odd_uint(rng: &mut impl CryptoRngCore, bit_length: u32) -> Odd { - random_odd_integer::(rng, NonZero::new(bit_length).unwrap()) + random_odd_integer::(rng, NonZero::new(bit_length).unwrap(), SetBits::Msb).unwrap() } fn make_sieve(rng: &mut impl CryptoRngCore) -> SmallPrimesSieve> { @@ -449,7 +449,9 @@ fn bench_glass_pumpkin(c: &mut Criterion) { // Mimics the sequence of checks `glass-pumpkin` does to find a prime. fn prime_like_gp(bit_length: u32, rng: &mut impl CryptoRngCore) -> BoxedUint { loop { - let start = random_odd_integer::(rng, NonZero::new(bit_length).unwrap()).get(); + let start = random_odd_integer::(rng, NonZero::new(bit_length).unwrap(), SetBits::Msb) + .unwrap() + .get(); let sieve = SmallPrimesSieve::new(start, NonZero::new(bit_length).unwrap(), false); for num in sieve { let odd_num = Odd::new(num.clone()).unwrap(); @@ -473,7 +475,9 @@ fn bench_glass_pumpkin(c: &mut Criterion) { // Mimics the sequence of checks `glass-pumpkin` does to find a safe prime. fn safe_prime_like_gp(bit_length: u32, rng: &mut impl CryptoRngCore) -> BoxedUint { loop { - let start = random_odd_integer::(rng, NonZero::new(bit_length).unwrap()).get(); + let start = random_odd_integer::(rng, NonZero::new(bit_length).unwrap(), SetBits::Msb) + .unwrap() + .get(); let sieve = SmallPrimesSieve::new(start, NonZero::new(bit_length).unwrap(), true); for num in sieve { let odd_num = Odd::new(num.clone()).unwrap(); diff --git a/src/hazmat.rs b/src/hazmat.rs index b668788..9e397d4 100644 --- a/src/hazmat.rs +++ b/src/hazmat.rs @@ -14,7 +14,7 @@ mod sieve; pub use lucas::{lucas_test, AStarBase, BruteForceBase, LucasBase, LucasCheck, SelfridgeBase}; pub use miller_rabin::MillerRabin; -pub use sieve::{random_odd_integer, SmallPrimesSieve, SmallPrimesSieveFactory}; +pub use sieve::{random_odd_integer, SetBits, SmallPrimesSieve, SmallPrimesSieveFactory}; /// Possible results of various primality tests. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/hazmat/miller_rabin.rs b/src/hazmat/miller_rabin.rs index 8e5ae55..11d64c2 100644 --- a/src/hazmat/miller_rabin.rs +++ b/src/hazmat/miller_rabin.rs @@ -147,7 +147,7 @@ mod tests { use num_prime::nt_funcs::is_prime64; use super::MillerRabin; - use crate::hazmat::{primes, pseudoprimes, random_odd_integer, SmallPrimesSieve}; + use crate::hazmat::{primes, pseudoprimes, random_odd_integer, SetBits, SmallPrimesSieve}; #[test] fn miller_rabin_derived_traits() { @@ -196,7 +196,7 @@ mod tests { #[test] fn trivial() { let mut rng = ChaCha8Rng::from_seed(*b"01234567890123456789012345678901"); - let start = random_odd_integer::(&mut rng, NonZero::new(1024).unwrap()); + let start = random_odd_integer::(&mut rng, NonZero::new(1024).unwrap(), SetBits::Msb).unwrap(); for num in SmallPrimesSieve::new(start.get(), NonZero::new(1024).unwrap(), false).take(10) { let mr = MillerRabin::new(Odd::new(num).unwrap()); diff --git a/src/hazmat/sieve.rs b/src/hazmat/sieve.rs index bceb0c9..cd0bb31 100644 --- a/src/hazmat/sieve.rs +++ b/src/hazmat/sieve.rs @@ -5,29 +5,64 @@ use alloc::{vec, vec::Vec}; use core::marker::PhantomData; use core::num::{NonZero, NonZeroU32}; -use crypto_bigint::{Integer, Odd, RandomBits}; +use crypto_bigint::{Integer, Odd, RandomBits, RandomBitsError}; use rand_core::CryptoRngCore; use crate::hazmat::precomputed::{SmallPrime, LAST_SMALL_PRIME, RECIPROCALS, SMALL_PRIMES}; use crate::traits::SieveFactory; -/// Returns a random odd integer with given bit length -/// (that is, with both `0` and `bit_length-1` bits set). +/// Decide how prime candidates are manipulated by setting certain bits before primality testing, +/// influencing the range of the prime. +#[derive(Debug, Clone, Copy)] +pub enum SetBits { + /// Set the most significant bit, thus limiting the range to `[MAX/2 + 1, MAX]`. + /// + /// In other words, all candidates will have the same bit size. + Msb, + /// Set two most significant bits, limiting the range to `[MAX - MAX/4 + 1, MAX]`. + /// + /// This is useful in the RSA case because a product of two such numbers will have a guaranteed bit size. + TwoMsb, + /// No additional bits set; uses the full range `[1, MAX]`. + None, +} + +/// Returns a random odd integer up to the given bit length. +/// +/// The `set_bits` parameter decides which extra bits are set, which decides the range of the number. /// -/// *Panics*: if the `bit_length` is bigger than the bits available in the `Integer`, e.g. 37 for a -/// `U32`. -pub fn random_odd_integer(rng: &mut impl CryptoRngCore, bit_length: NonZeroU32) -> Odd { +/// Returns an error variant if `bit_length` is greater than the maximum allowed for `T` +/// (applies to fixed-length types). +pub fn random_odd_integer( + rng: &mut impl CryptoRngCore, + bit_length: NonZeroU32, + set_bits: SetBits, +) -> Result, RandomBitsError> { let bit_length = bit_length.get(); - let mut random = T::random_bits(rng, bit_length); + let mut random = T::try_random_bits(rng, bit_length)?; + // Make it odd + // `bit_length` is non-zero, so the 0-th bit exists. random.set_bit_vartime(0, true); - // Make sure it's the correct bit size - // Will not overflow since `bit_length` is ensured to be within the size of the integer. - random.set_bit_vartime(bit_length - 1, true); + // Will not overflow since `bit_length` is ensured to be within the size of the integer + // (checked within the `T::try_random_bits()` call). + // `bit_length - 1`-th bit exists since `bit_length` is non-zero. + match set_bits { + SetBits::None => {} + SetBits::Msb => random.set_bit_vartime(bit_length - 1, true), + SetBits::TwoMsb => { + random.set_bit_vartime(bit_length - 1, true); + // We could panic here, but since the primary purpose of `TwoMsb` is to ensure the bit length + // of the product of two numbers, ignoring this for `bit_length = 1` leads to the desired result. + if bit_length > 1 { + random.set_bit_vartime(bit_length - 2, true); + } + } + } - Odd::new(random).expect("the number is odd by construction") + Ok(Odd::new(random).expect("the number is odd by construction")) } // The type we use to calculate incremental residues. @@ -251,11 +286,12 @@ impl Iterator for SmallPrimesSieve { pub struct SmallPrimesSieveFactory { max_bit_length: NonZeroU32, safe_primes: bool, + set_bits: SetBits, phantom: PhantomData, } impl SmallPrimesSieveFactory { - fn new_impl(max_bit_length: u32, safe_primes: bool) -> Self { + fn new_impl(max_bit_length: u32, set_bits: SetBits, safe_primes: bool) -> Self { if !safe_primes && max_bit_length < 2 { panic!("`bit_length` must be 2 or greater."); } @@ -266,20 +302,21 @@ impl SmallPrimesSieveFactory { Self { max_bit_length, safe_primes, + set_bits, phantom: PhantomData, } } /// Creates a factory that produces sieves returning numbers of `max_bit_length` bits (with the top bit set) /// that are not divisible by a number of small factors. - pub fn new(max_bit_length: u32) -> Self { - Self::new_impl(max_bit_length, false) + pub fn new(max_bit_length: u32, set_bits: SetBits) -> Self { + Self::new_impl(max_bit_length, set_bits, false) } /// Creates a factory that produces sieves returning numbers `n` of `max_bit_length` bits (with the top bit set) /// such that neither `n` nor `(n - 1) / 2` are divisible by a number of small factors. - pub fn new_safe_primes(max_bit_length: u32) -> Self { - Self::new_impl(max_bit_length, true) + pub fn new_safe_primes(max_bit_length: u32, set_bits: SetBits) -> Self { + Self::new_impl(max_bit_length, set_bits, true) } } @@ -291,7 +328,8 @@ impl SieveFactory for SmallPrimesSieveFactory { rng: &mut impl CryptoRngCore, _previous_sieve: Option<&Self::Sieve>, ) -> Option { - let start = random_odd_integer::(rng, self.max_bit_length); + let start = + random_odd_integer::(rng, self.max_bit_length, self.set_bits).expect("random_odd_integer() failed"); Some(SmallPrimesSieve::new( start.get(), self.max_bit_length, @@ -312,7 +350,7 @@ mod tests { use rand_chacha::ChaCha8Rng; use rand_core::{OsRng, SeedableRng}; - use super::{random_odd_integer, SmallPrimesSieve, SmallPrimesSieveFactory}; + use super::{random_odd_integer, SetBits, SmallPrimesSieve, SmallPrimesSieveFactory}; use crate::hazmat::precomputed::SMALL_PRIMES; #[test] @@ -320,7 +358,9 @@ mod tests { let max_prime = SMALL_PRIMES[SMALL_PRIMES.len() - 1]; let mut rng = ChaCha8Rng::from_seed(*b"01234567890123456789012345678901"); - let start = random_odd_integer::(&mut rng, NonZero::new(32).unwrap()).get(); + let start = random_odd_integer::(&mut rng, NonZero::new(32).unwrap(), SetBits::Msb) + .unwrap() + .get(); for num in SmallPrimesSieve::new(start, NonZero::new(32).unwrap(), false).take(100) { let num_u64 = u64::from(num); assert!(num_u64.leading_zeros() == 32); @@ -336,7 +376,9 @@ mod tests { let max_prime = SMALL_PRIMES[SMALL_PRIMES.len() - 1]; let mut rng = ChaCha8Rng::from_seed(*b"01234567890123456789012345678901"); - let start = random_odd_integer::(&mut rng, NonZero::new(32).unwrap()).get(); + let start = random_odd_integer::(&mut rng, NonZero::new(32).unwrap(), SetBits::Msb) + .unwrap() + .get(); for num in SmallPrimesSieve::new(start, NonZero::new(32).unwrap(), false).take(100) { // For 32-bit targets @@ -414,15 +456,16 @@ mod tests { #[test] fn random_below_max_length() { for _ in 0..10 { - let r = random_odd_integer::(&mut OsRng, NonZero::new(50).unwrap()).get(); + let r = random_odd_integer::(&mut OsRng, NonZero::new(50).unwrap(), SetBits::Msb) + .unwrap() + .get(); assert_eq!(r.bits(), 50); } } #[test] - #[should_panic(expected = "try_random_bits() failed: BitLengthTooLarge { bit_length: 65, bits_precision: 64 }")] fn random_odd_uint_too_many_bits() { - let _p = random_odd_integer::(&mut OsRng, NonZero::new(65).unwrap()); + assert!(random_odd_integer::(&mut OsRng, NonZero::new(65).unwrap(), SetBits::Msb).is_err()); } #[test] @@ -450,12 +493,41 @@ mod tests { #[test] #[should_panic(expected = "`bit_length` must be 2 or greater")] fn too_few_bits_regular_primes() { - let _fac = SmallPrimesSieveFactory::::new(1); + let _fac = SmallPrimesSieveFactory::::new(1, SetBits::Msb); } #[test] #[should_panic(expected = "`bit_length` must be 3 or greater")] fn too_few_bits_safe_primes() { - let _fac = SmallPrimesSieveFactory::::new_safe_primes(2); + let _fac = SmallPrimesSieveFactory::::new_safe_primes(2, SetBits::Msb); + } + + #[test] + fn set_bits() { + for _ in 0..10 { + let x = random_odd_integer::(&mut OsRng, NonZero::new(64).unwrap(), SetBits::Msb).unwrap(); + assert!(bool::from(x.bit(63))); + } + + for _ in 0..10 { + let x = random_odd_integer::(&mut OsRng, NonZero::new(64).unwrap(), SetBits::TwoMsb).unwrap(); + assert!(bool::from(x.bit(63))); + assert!(bool::from(x.bit(62))); + } + + // 1 in 2^30 chance of spurious failure... good enough? + assert!((0..30) + .map(|_| { random_odd_integer::(&mut OsRng, NonZero::new(64).unwrap(), SetBits::None).unwrap() }) + .any(|x| !bool::from(x.bit(63)))); + } + + #[test] + fn set_two_msb_small_bit_length() { + // Check that when technically there isn't a second most significant bit, + // `random_odd_integer()` still returns a number. + let x = random_odd_integer::(&mut OsRng, NonZero::new(1).unwrap(), SetBits::TwoMsb) + .unwrap() + .get(); + assert_eq!(x, U64::ONE); } } diff --git a/src/presets.rs b/src/presets.rs index 0fe44c1..f11830e 100644 --- a/src/presets.rs +++ b/src/presets.rs @@ -6,7 +6,7 @@ use rand_core::OsRng; use crate::{ generic::sieve_and_find, - hazmat::{lucas_test, AStarBase, LucasCheck, MillerRabin, Primality, SmallPrimesSieveFactory}, + hazmat::{lucas_test, AStarBase, LucasCheck, MillerRabin, Primality, SetBits, SmallPrimesSieveFactory}, }; #[cfg(feature = "multicore")] @@ -84,8 +84,12 @@ pub fn generate_prime_with_rng( rng: &mut impl CryptoRngCore, bit_length: u32, ) -> T { - sieve_and_find(rng, SmallPrimesSieveFactory::new(bit_length), is_prime_with_rng) - .expect("will produce a result eventually") + sieve_and_find( + rng, + SmallPrimesSieveFactory::new(bit_length, SetBits::Msb), + is_prime_with_rng, + ) + .expect("will produce a result eventually") } /// Returns a random safe prime (that is, such that `(n - 1) / 2` is also prime) @@ -100,7 +104,7 @@ pub fn generate_safe_prime_with_rng( ) -> T { sieve_and_find( rng, - SmallPrimesSieveFactory::new_safe_primes(bit_length), + SmallPrimesSieveFactory::new_safe_primes(bit_length, SetBits::Msb), is_safe_prime_with_rng, ) .expect("will produce a result eventually") @@ -124,7 +128,7 @@ where { par_sieve_and_find( rng, - SmallPrimesSieveFactory::new(bit_length), + SmallPrimesSieveFactory::new(bit_length, SetBits::Msb), is_prime_with_rng, threadcount, ) @@ -151,7 +155,7 @@ where { par_sieve_and_find( rng, - SmallPrimesSieveFactory::new_safe_primes(bit_length), + SmallPrimesSieveFactory::new_safe_primes(bit_length, SetBits::Msb), is_safe_prime_with_rng, threadcount, ) @@ -385,13 +389,13 @@ mod tests { } #[test] - #[should_panic(expected = "try_random_bits() failed: BitLengthTooLarge { bit_length: 65, bits_precision: 64 }")] + #[should_panic(expected = "random_odd_integer() failed: BitLengthTooLarge { bit_length: 65, bits_precision: 64 }")] fn generate_prime_too_many_bits() { let _p: U64 = generate_prime_with_rng(&mut OsRng, 65); } #[test] - #[should_panic(expected = "try_random_bits() failed: BitLengthTooLarge { bit_length: 65, bits_precision: 64 }")] + #[should_panic(expected = "random_odd_integer() failed: BitLengthTooLarge { bit_length: 65, bits_precision: 64 }")] fn generate_safe_prime_too_many_bits() { let _p: U64 = generate_safe_prime_with_rng(&mut OsRng, 65); } @@ -478,7 +482,7 @@ mod tests_openssl { use rand_core::OsRng; use super::{generate_prime, is_prime}; - use crate::hazmat::random_odd_integer; + use crate::hazmat::{random_odd_integer, SetBits}; fn openssl_is_prime(num: &BigNum, ctx: &mut BigNumContext) -> bool { num.is_prime(64, ctx).unwrap() @@ -513,7 +517,7 @@ mod tests_openssl { // Generate random numbers, check if our test agrees with OpenSSL for _ in 0..100 { - let p = random_odd_integer::(&mut OsRng, NonZero::new(128).unwrap()); + let p = random_odd_integer::(&mut OsRng, NonZero::new(128).unwrap(), SetBits::Msb).unwrap(); let actual = is_prime(p.as_ref()); let p_bn = to_openssl(&p); let expected = openssl_is_prime(&p_bn, &mut ctx); @@ -538,7 +542,7 @@ mod tests_gmp { }; use super::{generate_prime, is_prime}; - use crate::hazmat::random_odd_integer; + use crate::hazmat::{random_odd_integer, SetBits}; fn gmp_is_prime(num: &Integer) -> bool { matches!(num.is_probably_prime(25), IsPrime::Yes | IsPrime::Probably) @@ -563,7 +567,7 @@ mod tests_gmp { // Generate primes with GMP, check them for _ in 0..100 { - let start = random_odd_integer::(&mut OsRng, NonZero::new(128).unwrap()); + let start = random_odd_integer::(&mut OsRng, NonZero::new(128).unwrap(), SetBits::Msb).unwrap(); let start_bn = to_gmp(&start); let p_bn = start_bn.next_prime(); let p = from_gmp(&p_bn); @@ -572,7 +576,7 @@ mod tests_gmp { // Generate random numbers, check if our test agrees with GMP for _ in 0..100 { - let p = random_odd_integer::(&mut OsRng, NonZero::new(128).unwrap()); + let p = random_odd_integer::(&mut OsRng, NonZero::new(128).unwrap(), SetBits::Msb).unwrap(); let actual = is_prime(p.as_ref()); let p_bn = to_gmp(&p); let expected = gmp_is_prime(&p_bn);