From 7e0166b85dca252c81301269f1d7e14a60178255 Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Thu, 27 Nov 2025 08:12:47 -0800 Subject: [PATCH 1/7] New random_mod_core --- src/uint/rand.rs | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index d36dc26c..d9d44dc6 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -121,43 +121,35 @@ impl RandomMod for Uint { // TODO(tarcieri): obtain `n_bits` via a trait like `Integer` pub(super) fn random_mod_core( rng: &mut R, - n: &mut T, - modulus: &NonZero, + x: &mut T, + n: &NonZero, n_bits: u32, ) -> Result<(), R::Error> where T: AsMut<[Limb]> + AsRef<[Limb]> + ConstantTimeLess + Zero, { - #[cfg(target_pointer_width = "64")] - let mut next_word = || rng.try_next_u64(); - #[cfg(target_pointer_width = "32")] - let mut next_word = || rng.try_next_u32(); - let n_limbs = n_bits.div_ceil(Limb::BITS) as usize; + let mask = !0 >> n.as_ref().as_ref()[n_limbs - 1].0.leading_zeros(); - let hi_word_modulus = modulus.as_ref().as_ref()[n_limbs - 1].0; - let mask = !0 >> hi_word_modulus.leading_zeros(); - let mut hi_word = next_word()? & mask; + let buffer: Word = 0; + let mut buffer = buffer.to_le_bytes(); loop { - while hi_word > hi_word_modulus { - hi_word = next_word()? & mask; + for limb in &mut x.as_mut()[..n_limbs - 1] { + rng.try_fill_bytes(&mut buffer)?; + *limb = Limb::from_le_bytes(buffer); } - // Set high limb - n.as_mut()[n_limbs - 1] = Limb::from_le_bytes(hi_word.to_le_bytes()); - // Set low limbs - for i in 0..n_limbs - 1 { - // Need to deserialize from little-endian to make sure that two 32-bit limbs - // deserialized sequentially are equal to one 64-bit limb produced from the same - // byte stream. - n.as_mut()[i] = Limb::from_le_bytes(next_word()?.to_le_bytes()); + + rng.try_fill_bytes(&mut buffer)?; + x.as_mut()[n_limbs - 1] = Limb::from(Word::from_le_bytes(buffer) & mask); + if cfg!(target_pointer_width = "32") && n_limbs & 1 == 1 { + // Read entropy in 64-bit blocks, even on 32-bit platforms. + let _ = rng.try_next_u32()?; } - // If the high limb is equal to the modulus' high limb, it's still possible - // that the full uint is too big so we check and repeat if it is. - if n.ct_lt(modulus).into() { + + if x.ct_lt(n).into() { break; } - hi_word = next_word()? & mask; } Ok(()) } From 5b4509225565b9ebbcc60eb3aa3b8684ef74a568 Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Thu, 27 Nov 2025 08:26:46 -0800 Subject: [PATCH 2/7] Add random_mod platform independence test --- src/uint/rand.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index d36dc26c..2d35234c 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -288,6 +288,41 @@ mod tests { ); } + /// Make sure random_mod output is consistent across platforms + #[test] + fn random_mod_platform_independence() { + let mut rng = get_four_sequential_rng(); + + let modulus = NonZero::new(U256::from_u32(8192)).unwrap(); + let mut vals = [U256::ZERO; 5]; + for val in &mut vals { + *val = U256::random_mod(&mut rng, &modulus); + } + let expected = [55, 2172, 1657, 4668, 7688]; + for (want, got) in expected.into_iter().zip(vals.into_iter()) { + // assert_eq!(got.as_words()[0], want); + assert_eq!(got, U256::from_u32(want)); + } + + let modulus = + NonZero::new(U256::ZERO.wrapping_sub(&U256::from_u64(rng.next_u64()))).unwrap(); + let val = U256::random_mod(&mut rng, &modulus); + assert_eq!( + val, + U256::from_be_hex("C54302F2EB1E2F69C3B919AE0D16DF2259CD1A8A9B8EA8E0862878227D4B40A3") + ); + + let mut state = [0u8; 16]; + rng.fill_bytes(&mut state); + + assert_eq!( + state, + [ + 71, 204, 238, 147, 198, 196, 132, 164, 240, 211, 223, 12, 36, 189, 139, 48, + ], + ); + } + /// Test that random bytes are sampled consecutively. #[test] fn random_bits_4_bytes_sequential() { From d2f4b890a2e2aa0f45d7635c0da662bf39f3758c Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Thu, 27 Nov 2025 08:29:07 -0800 Subject: [PATCH 3/7] Apply the diff from this change --- src/uint/rand.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index ef7df16e..5c9708ac 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -301,7 +301,7 @@ mod tests { let val = U256::random_mod(&mut rng, &modulus); assert_eq!( val, - U256::from_be_hex("C54302F2EB1E2F69C3B919AE0D16DF2259CD1A8A9B8EA8E0862878227D4B40A3") + U256::from_be_hex("C3B919AE0D16DF2259CD1A8A9B8EA8E0862878227D4B40A3C54302F2EB1E2F69") ); let mut state = [0u8; 16]; From d0da67040e44e1a03f54c401e82aba7ba6ce1841 Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Thu, 27 Nov 2025 08:47:22 -0800 Subject: [PATCH 4/7] Optimize performance of random_mod_core --- src/uint/rand.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index 5c9708ac..0ab7392d 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -1,7 +1,7 @@ //! Random number generator support use super::{Uint, Word}; -use crate::{Encoding, Limb, NonZero, Random, RandomBits, RandomBitsError, RandomMod, Zero}; +use crate::{Limb, NonZero, Random, RandomBits, RandomBitsError, RandomMod, Zero}; use rand_core::{RngCore, TryRngCore}; use subtle::ConstantTimeLess; @@ -128,20 +128,19 @@ pub(super) fn random_mod_core( where T: AsMut<[Limb]> + AsRef<[Limb]> + ConstantTimeLess + Zero, { + #[cfg(target_pointer_width = "64")] + let next_word = |rng: &mut R| rng.try_next_u64(); + #[cfg(target_pointer_width = "32")] + let next_word = |rng: &mut R| rng.try_next_u32(); + let n_limbs = n_bits.div_ceil(Limb::BITS) as usize; let mask = !0 >> n.as_ref().as_ref()[n_limbs - 1].0.leading_zeros(); - let buffer: Word = 0; - let mut buffer = buffer.to_le_bytes(); - loop { for limb in &mut x.as_mut()[..n_limbs - 1] { - rng.try_fill_bytes(&mut buffer)?; - *limb = Limb::from_le_bytes(buffer); + limb.0 = next_word(rng)?; } - - rng.try_fill_bytes(&mut buffer)?; - x.as_mut()[n_limbs - 1] = Limb::from(Word::from_le_bytes(buffer) & mask); + x.as_mut()[n_limbs - 1].0 = next_word(rng)? & mask; if cfg!(target_pointer_width = "32") && n_limbs & 1 == 1 { // Read entropy in 64-bit blocks, even on 32-bit platforms. let _ = rng.try_next_u32()?; From 5be8ce3527451861f0385ae82f02a9e45f5383fc Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Thu, 27 Nov 2025 09:11:10 -0800 Subject: [PATCH 5/7] Revert "Optimize performance of random_mod_core" This appears to have fallen afoul of the compiler bug. This reverts commit d0da67040e44e1a03f54c401e82aba7ba6ce1841. --- src/uint/rand.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index 0ab7392d..5c9708ac 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -1,7 +1,7 @@ //! Random number generator support use super::{Uint, Word}; -use crate::{Limb, NonZero, Random, RandomBits, RandomBitsError, RandomMod, Zero}; +use crate::{Encoding, Limb, NonZero, Random, RandomBits, RandomBitsError, RandomMod, Zero}; use rand_core::{RngCore, TryRngCore}; use subtle::ConstantTimeLess; @@ -128,19 +128,20 @@ pub(super) fn random_mod_core( where T: AsMut<[Limb]> + AsRef<[Limb]> + ConstantTimeLess + Zero, { - #[cfg(target_pointer_width = "64")] - let next_word = |rng: &mut R| rng.try_next_u64(); - #[cfg(target_pointer_width = "32")] - let next_word = |rng: &mut R| rng.try_next_u32(); - let n_limbs = n_bits.div_ceil(Limb::BITS) as usize; let mask = !0 >> n.as_ref().as_ref()[n_limbs - 1].0.leading_zeros(); + let buffer: Word = 0; + let mut buffer = buffer.to_le_bytes(); + loop { for limb in &mut x.as_mut()[..n_limbs - 1] { - limb.0 = next_word(rng)?; + rng.try_fill_bytes(&mut buffer)?; + *limb = Limb::from_le_bytes(buffer); } - x.as_mut()[n_limbs - 1].0 = next_word(rng)? & mask; + + rng.try_fill_bytes(&mut buffer)?; + x.as_mut()[n_limbs - 1] = Limb::from(Word::from_le_bytes(buffer) & mask); if cfg!(target_pointer_width = "32") && n_limbs & 1 == 1 { // Read entropy in 64-bit blocks, even on 32-bit platforms. let _ = rng.try_next_u32()?; From 0f117fa6b2feb4bbc9db57d8de3dbb62f6326126 Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Thu, 27 Nov 2025 19:19:04 -0800 Subject: [PATCH 6/7] Use a variable-time comparison loop in random_mod Justification: `random_mod` is already variable-time due to the rejection sampling. This allows us to optimize the loop without running afoul of the compiler bug. --- src/uint/rand.rs | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index 5c9708ac..afa4b5a7 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -1,7 +1,9 @@ //! Random number generator support +use core::cmp::Ordering; + use super::{Uint, Word}; -use crate::{Encoding, Limb, NonZero, Random, RandomBits, RandomBitsError, RandomMod, Zero}; +use crate::{Limb, NonZero, Random, RandomBits, RandomBitsError, RandomMod, Zero}; use rand_core::{RngCore, TryRngCore}; use subtle::ConstantTimeLess; @@ -128,30 +130,35 @@ pub(super) fn random_mod_core( where T: AsMut<[Limb]> + AsRef<[Limb]> + ConstantTimeLess + Zero, { + #[cfg(target_pointer_width = "64")] + let next_word = |rng: &mut R| rng.try_next_u64(); + #[cfg(target_pointer_width = "32")] + let next_word = |rng: &mut R| rng.try_next_u32(); + let n_limbs = n_bits.div_ceil(Limb::BITS) as usize; let mask = !0 >> n.as_ref().as_ref()[n_limbs - 1].0.leading_zeros(); - let buffer: Word = 0; - let mut buffer = buffer.to_le_bytes(); - - loop { + 'outer: loop { for limb in &mut x.as_mut()[..n_limbs - 1] { - rng.try_fill_bytes(&mut buffer)?; - *limb = Limb::from_le_bytes(buffer); + *limb = Limb::from(next_word(rng)?); } - - rng.try_fill_bytes(&mut buffer)?; - x.as_mut()[n_limbs - 1] = Limb::from(Word::from_le_bytes(buffer) & mask); + x.as_mut()[n_limbs - 1] = Limb::from(next_word(rng)? & mask); if cfg!(target_pointer_width = "32") && n_limbs & 1 == 1 { // Read entropy in 64-bit blocks, even on 32-bit platforms. let _ = rng.try_next_u32()?; } - - if x.ct_lt(n).into() { - break; + // Do a manual, variable-time comparison loop here to avoid a copiler bug that causes a + // hang on linux-aarch64 under `--release` with `Uint` of 5 or more limbs using `ct_lt`. + let x = x.as_ref(); + let n = n.as_ref().as_ref(); + for i in (0..n_limbs).rev() { + match x[i].cmp(&n[i]) { + Ordering::Less => return Ok(()), + Ordering::Greater => continue 'outer, + Ordering::Equal => (), + } } } - Ok(()) } #[cfg(test)] From 3d1db8a2ce5351e99c59ed616b36ea0d27ded67f Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Thu, 27 Nov 2025 19:26:19 -0800 Subject: [PATCH 7/7] fix typo --- src/uint/rand.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index afa4b5a7..af091bab 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -147,7 +147,7 @@ where // Read entropy in 64-bit blocks, even on 32-bit platforms. let _ = rng.try_next_u32()?; } - // Do a manual, variable-time comparison loop here to avoid a copiler bug that causes a + // Do a manual, variable-time comparison loop here to avoid a compiler bug that causes a // hang on linux-aarch64 under `--release` with `Uint` of 5 or more limbs using `ct_lt`. let x = x.as_ref(); let n = n.as_ref().as_ref();