From 0244ade4ed19d2ac4e481bdfb68f42f446c8262c Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Sun, 23 Nov 2025 23:07:40 -0800 Subject: [PATCH 1/5] Add test for random_mod platform dependence Succeeds: ```sh cargo test # --target=x86_64-unknown-linux-gnu ``` Fails: ```sh cargo test --target=i386-unknown-linux-gnu ``` --- src/uint/rand.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index d36dc26c..fcc27142 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -164,7 +164,7 @@ where #[cfg(test)] mod tests { - use crate::uint::rand::random_bits_core; + use crate::uint::rand::{random_bits_core, random_mod_core}; use crate::{Limb, NonZero, Random, RandomBits, RandomMod, U256, U1024, Uint}; use chacha20::ChaCha8Rng; use rand_core::{RngCore, SeedableRng}; @@ -288,6 +288,27 @@ 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_u8(7)).unwrap(); + let mut val = U256::ZERO; + let _ = random_mod_core(&mut rng, &mut val, &modulus, modulus.bits_vartime()); + + let mut state = [0u8; 16]; + rng.fill_bytes(&mut state); + + // XXX Passes on 64-bit, fails on 32-bit + assert_eq!( + state, + [ + 55, 192, 216, 186, 95, 165, 70, 119, 230, 166, 226, 129, 50, 13, 251, 178, + ], + ); + } + /// Test that random bytes are sampled consecutively. #[test] fn random_bits_4_bytes_sequential() { From 533121d216f27ecd304ffe396c1c8e9049a125d7 Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Sun, 23 Nov 2025 23:17:23 -0800 Subject: [PATCH 2/5] Make `random_mod` platform-independent Replaces the fancy platform-word-based logic with a simple call to `random_bits_core` with rejection sampling. Each retry has p > 0.5 (worst case) of selecting a number inside the range; compare against [OpenBSD][0]: ```c /* * This could theoretically loop forever but each retry has * p > 0.5 (worst case, usually far better) of selecting a * number inside the range we need, so it should rarely need * to re-roll. */ for (;;) { r = arc4random(); if (r >= min) break; } ``` **NB**. This change will generally result in different numbers being produced after calls to `Uint::random_mod`, as evidenced by the test change. [0]: https://github.com/openbsd/src/blob/0c90e2b526b26d9f50dde7f7712cec107cda3326/lib/libc/crypt/arc4random_uniform.c#L43-L53 --- src/uint/rand.rs | 43 ++++++++----------------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index fcc27142..09ff7c3d 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; @@ -30,7 +30,7 @@ pub(crate) fn random_bits_core( rng: &mut R, zeroed_limbs: &mut [Limb], bit_length: u32, -) -> Result<(), RandomBitsError> { +) -> Result<(), R::Error> { if bit_length == 0 { return Ok(()); } @@ -43,8 +43,7 @@ pub(crate) fn random_bits_core( let mask = Word::MAX >> ((Word::BITS - partial_limb) % Word::BITS); for i in 0..nonzero_limbs - 1 { - rng.try_fill_bytes(&mut buffer) - .map_err(RandomBitsError::RandCore)?; + rng.try_fill_bytes(&mut buffer)?; zeroed_limbs[i] = Limb(Word::from_le_bytes(buffer)); } @@ -62,8 +61,7 @@ pub(crate) fn random_bits_core( buffer.as_mut_slice() }; - rng.try_fill_bytes(slice) - .map_err(RandomBitsError::RandCore)?; + rng.try_fill_bytes(slice)?; zeroed_limbs[nonzero_limbs - 1] = Limb(Word::from_le_bytes(buffer) & mask); Ok(()) @@ -95,7 +93,7 @@ impl RandomBits for Uint { }); } let mut limbs = [Limb::ZERO; LIMBS]; - random_bits_core(rng, &mut limbs, bit_length)?; + random_bits_core(rng, &mut limbs, bit_length).map_err(RandomBitsError::RandCore)?; Ok(Self::from(limbs)) } } @@ -128,36 +126,12 @@ pub(super) fn random_mod_core( 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 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; - loop { - while hi_word > hi_word_modulus { - hi_word = next_word()? & mask; - } - // 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()); - } - // 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. + random_bits_core(rng, n.as_mut(), n_bits)?; + if n.ct_lt(modulus).into() { break; } - hi_word = next_word()? & mask; } Ok(()) } @@ -300,11 +274,10 @@ mod tests { let mut state = [0u8; 16]; rng.fill_bytes(&mut state); - // XXX Passes on 64-bit, fails on 32-bit assert_eq!( state, [ - 55, 192, 216, 186, 95, 165, 70, 119, 230, 166, 226, 129, 50, 13, 251, 178, + 103, 247, 133, 181, 55, 192, 216, 186, 95, 165, 70, 119, 230, 166, 226, 129, ], ); } From 65ce84b8e560f4b459bdeba43f8f72a4c7d184d2 Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Sun, 23 Nov 2025 23:30:50 -0800 Subject: [PATCH 3/5] Fix tests --- src/uint/boxed/rand.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uint/boxed/rand.rs b/src/uint/boxed/rand.rs index 51252b99..a2fd4ffd 100644 --- a/src/uint/boxed/rand.rs +++ b/src/uint/boxed/rand.rs @@ -28,7 +28,7 @@ impl RandomBits for BoxedUint { } let mut ret = BoxedUint::zero_with_precision(bits_precision); - random_bits_core(rng, &mut ret.limbs, bit_length)?; + random_bits_core(rng, &mut ret.limbs, bit_length).map_err(RandomBitsError::RandCore)?; Ok(ret) } } From 0c08cab22b5875a445072d215a9e3ef26ac3336a Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Mon, 24 Nov 2025 09:18:41 -0800 Subject: [PATCH 4/5] Stronger random_mod test --- src/uint/rand.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index 09ff7c3d..ff18067d 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -267,9 +267,15 @@ mod tests { fn random_mod_platform_independence() { let mut rng = get_four_sequential_rng(); - let modulus = NonZero::new(U256::from_u8(7)).unwrap(); - let mut val = U256::ZERO; - let _ = random_mod_core(&mut rng, &mut val, &modulus, modulus.bits_vartime()); + let modulus = NonZero::new(U256::from_u32(7776)).unwrap(); + let mut vals = [U256::ZERO, U256::ZERO, U256::ZERO, U256::ZERO]; + for val in &mut vals { + random_mod_core(&mut rng, val, &modulus, modulus.bits_vartime()).unwrap(); + } + let expected = [7020, 5991, 55, 1375]; + for (want, got) in expected.into_iter().zip(vals.into_iter()) { + assert_eq!(got, U256::from_u32(want)); + } let mut state = [0u8; 16]; rng.fill_bytes(&mut state); @@ -277,7 +283,7 @@ mod tests { assert_eq!( state, [ - 103, 247, 133, 181, 55, 192, 216, 186, 95, 165, 70, 119, 230, 166, 226, 129, + 230, 166, 226, 129, 50, 13, 251, 178, 124, 136, 155, 143, 164, 96, 103, 15, ], ); } From 52ea35196e885a4353e85061762ed164990187ca Mon Sep 17 00:00:00 2001 From: Steven Dee Date: Mon, 24 Nov 2025 11:23:55 -0800 Subject: [PATCH 5/5] Test rejection case Uses a modulus with even odds of rejection so that the number sequence produced exercises the rejection logic in `random_mod_core`. Also tests a sequence of five numbers instead of four, giving us about 64 (in fact, exactly 65) bits against this modulus. --- src/uint/rand.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uint/rand.rs b/src/uint/rand.rs index ff18067d..ae060c70 100644 --- a/src/uint/rand.rs +++ b/src/uint/rand.rs @@ -267,12 +267,12 @@ mod tests { fn random_mod_platform_independence() { let mut rng = get_four_sequential_rng(); - let modulus = NonZero::new(U256::from_u32(7776)).unwrap(); - let mut vals = [U256::ZERO, U256::ZERO, U256::ZERO, U256::ZERO]; + let modulus = NonZero::new(U256::from_u32(8192)).unwrap(); + let mut vals = [U256::ZERO, U256::ZERO, U256::ZERO, U256::ZERO, U256::ZERO]; for val in &mut vals { random_mod_core(&mut rng, val, &modulus, modulus.bits_vartime()).unwrap(); } - let expected = [7020, 5991, 55, 1375]; + let expected = [55, 3378, 2172, 1657, 5323]; for (want, got) in expected.into_iter().zip(vals.into_iter()) { assert_eq!(got, U256::from_u32(want)); } @@ -283,7 +283,7 @@ mod tests { assert_eq!( state, [ - 230, 166, 226, 129, 50, 13, 251, 178, 124, 136, 155, 143, 164, 96, 103, 15, + 60, 146, 46, 106, 157, 83, 56, 212, 186, 104, 211, 210, 125, 28, 120, 239 ], ); }