From 4e16843eb21f494494899abf3795f82c068bd5f0 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 16 Oct 2026 15:50:07 +0200 Subject: [PATCH 01/14] fix: `Arbitrary::shrink` impl for Integer Previous impl went into infinite loop with negative numbers Signed-off-by: Denis Varlakov --- src/backend/num_bigint.rs | 2 +- src/backend/rug.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index 2f8d263..d14762c 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -451,7 +451,7 @@ impl quickcheck::Arbitrary for Integer { if prev.cmp0().is_eq() { None } else { - prev >>= 1; + prev /= 2; Some(prev.clone()) } })) diff --git a/src/backend/rug.rs b/src/backend/rug.rs index 73b2294..0fbcd63 100644 --- a/src/backend/rug.rs +++ b/src/backend/rug.rs @@ -344,7 +344,7 @@ impl quickcheck::Arbitrary for Integer { if prev.cmp0().is_eq() { None } else { - prev >>= 1; + prev /= 2; Some(prev.clone()) } })) From bb691847680f5aad3f0516c5b9d11705192d5810 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 16 Oct 2026 15:52:27 +0200 Subject: [PATCH 02/14] fix: forbid whitespaces in integer encoding rug backend used to allow whitespaces in the integer encoding when provided to `Integer::from_str_radix`, even if the whitespace is in the middle of the number. This commit forbids any characters that are not ascii alphanum in the integer encoding. Signed-off-by: Denis Varlakov --- src/backend/num_bigint.rs | 11 +++++++++++ src/backend/rug.rs | 12 ++++++++++++ tests/integration/backend.rs | 24 ++++++++++++++++++++++++ tests/integration/main.rs | 5 +++++ 4 files changed, 52 insertions(+) diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index d14762c..443e04f 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -75,6 +75,17 @@ impl Integer { } /// Parses the integer using the given radix pub fn from_str_radix(s: &str, radix: u16) -> Option { + // num-bigint already enforces these checks, but we double-enforce them + // to make sure they are in sync with what we already enforce for rug. + // See RugInteger::from_str_radix + if !s + .chars() + .enumerate() + .skip_while(|&(i, c)| i == 0 && (c == '+' || c == '-')) + .all(|(_, c)| c.is_ascii_alphanumeric()) + { + return None; + } num_traits::Num::from_str_radix(s, radix.into()) .ok() .map(Integer) diff --git a/src/backend/rug.rs b/src/backend/rug.rs index 0fbcd63..d7f7e13 100644 --- a/src/backend/rug.rs +++ b/src/backend/rug.rs @@ -76,6 +76,18 @@ impl Integer { } /// Parses the integer using the given radix pub fn from_str_radix(s: &str, radix: u16) -> Option { + // rug accepts strings that have whitespaces in them, integer is parsed while + // ignoring whitespaces, even in the middle of the number. This behavior is + // different from num-bigint backend, and it is strange. We manually restrict + // strings to be ascii alphanum + if !s + .chars() + .enumerate() + .skip_while(|&(i, c)| i == 0 && (c == '+' || c == '-')) + .all(|(_, c)| c.is_ascii_alphanumeric()) + { + return None; + } rug::Integer::from_str_radix(s, radix.into()) .ok() .map(Integer) diff --git a/tests/integration/backend.rs b/tests/integration/backend.rs index 24a5b97..d438554 100644 --- a/tests/integration/backend.rs +++ b/tests/integration/backend.rs @@ -180,6 +180,17 @@ quickcheck::quickcheck! { // It matters to check if one returns `Yes|Probably`, and the other returns `No` matches!((r1, r2), (IsPrime::Yes | IsPrime::Probably, IsPrime::Yes | IsPrime::Probably) | (IsPrime::No, IsPrime::No)) } + + fn from_radix(s: AsciiString) -> bool { + let nbi = NbiInteger::from_str_radix(&s.0, 16); + let rug = RugInteger::from_str_radix(&s.0, 16); + eprintln!("{nbi:?}, {rug:?}"); + match (nbi, rug) { + (Some(a), Some(b)) => a.equals(b), + (None, None) => true, + _ => false + } + } } ///// Helper traits for macro ///// @@ -455,3 +466,16 @@ impl<'a> Arg<'a> for Prng { &mut self.rng } } + +#[derive(Debug, Clone)] +struct AsciiString(String); +impl quickcheck::Arbitrary for AsciiString { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let size = u8::arbitrary(g) % 32; + let s = std::iter::repeat_with(|| u8::arbitrary(g) & 0x7F) + .take(usize::from(size)) + .map(char::from) + .collect(); + Self(s) + } +} diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 572352b..6dee159 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -217,3 +217,8 @@ fn random_key_for_tests(rng: &mut impl rand_core::RngCore) -> DecryptionKey { let q = Integer::generate_safe_prime(rng, 512); DecryptionKey::from_primes(p, q).unwrap() } + +#[test] +fn bigint_from_radix_prohibits_whitespaces() { + assert_eq!(Integer::from_str_radix("123 4", 10), None) +} From bd1d104ec23ca9c6434fdfe275cdc75429127f03 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 16 Oct 2026 16:03:56 +0200 Subject: [PATCH 03/14] fix: remove panic in num-bigint's pow_mod when modulo=0 Signed-off-by: Denis Varlakov --- src/backend/num_bigint.rs | 4 ++++ tests/integration/backend.rs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index 443e04f..f466560 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -156,6 +156,10 @@ impl Integer { self.pow_mod_ref(exponent, modulo) } pub fn pow_mod_ref(&self, exponent: &Self, modulo: &Self) -> Option { + // num-bigint panics if modulo is zero; return `None` instead + if modulo.cmp0().is_eq() { + return None; + } let r = if exponent.0.is_negative() { let nself = self.0.modinv(&modulo.0)?; Integer(nself.modpow(&-&exponent.0, &modulo.0)) diff --git a/tests/integration/backend.rs b/tests/integration/backend.rs index d438554..aaafb1e 100644 --- a/tests/integration/backend.rs +++ b/tests/integration/backend.rs @@ -103,10 +103,10 @@ make_quickcheck!(lcm_ref(self: NbiInteger, other: RefInteger)); make_quickcheck!(gcd_ref(self: NbiInteger, other: RefInteger)); make_quickcheck!(cmp0(self: NbiInteger)); make_quickcheck!( - pow_mod(self: NbiInteger, exponent: RefInteger, modulo: NonZero) + pow_mod(self: NbiInteger, exponent: RefInteger, modulo: RefInteger) ); make_quickcheck!( - pow_mod_ref(self: NbiInteger, exponent: RefInteger, modulo: NonZero) + pow_mod_ref(self: NbiInteger, exponent: RefInteger, modulo: RefInteger) ); make_quickcheck!(Self::u_pow_u(base: u32, exponent: SmallU32)); make_quickcheck!(square(self: NbiInteger)); From 804b353a611b954eb62846ebee8cf963607a77dc Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 16 Oct 2026 16:06:46 +0200 Subject: [PATCH 04/14] fix: remove panic in num-bigint's invert when modulo=0 Signed-off-by: Denis Varlakov --- src/backend/num_bigint.rs | 4 ++++ tests/integration/backend.rs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index f466560..3dd0ad2 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -230,6 +230,10 @@ impl Integer { self.invert_ref(modulo) } pub fn invert_ref(&self, modulo: &Self) -> Option { + // When modulo=0, num-bigint panics; we return `None` instead + if modulo.cmp0().is_eq() { + return None; + } // Nbi returns wrong result if modulo = 1, and self is negative. Correct response is `0`, // but nbi gives `1`. if self.cmp0().is_lt() && modulo.is_one() { diff --git a/tests/integration/backend.rs b/tests/integration/backend.rs index aaafb1e..cc5aaaa 100644 --- a/tests/integration/backend.rs +++ b/tests/integration/backend.rs @@ -119,8 +119,8 @@ make_quickcheck!(modulo_mut(self: NbiInteger, divisor: Positive)); make_quickcheck!(mod_u(self: NbiInteger, divisor: Positive)); make_quickcheck!(significant_bits(self: NbiInteger)); make_quickcheck!(significant_dwords(self: NbiInteger)); -make_quickcheck!(invert(self: NbiInteger, modulo: Positive)); -make_quickcheck!(invert_ref(self: NbiInteger, modulo: Positive)); +make_quickcheck!(invert(self: NbiInteger, modulo: RefInteger)); +make_quickcheck!(invert_ref(self: NbiInteger, modulo: RefInteger)); make_quickcheck!(set_bit(self: NbiInteger, index: SmallU32, value: bool)); make_quickcheck!(random_below(self: Positive, rng: Prng)); make_quickcheck!(random_below_ref(self: Positive, rng: Prng)); From 29fecbc9f0f48be2dcb2b18f21d8ed52589b7379 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 19 Oct 2026 13:45:49 +0200 Subject: [PATCH 05/14] fix!: Integer::random_below returns error if sampled from empty range Signed-off-by: Denis Varlakov --- benches/comparison.rs | 10 ++++----- src/backend.rs | 29 +++++++++++++++++++------ src/backend/num_bigint.rs | 42 ++++++++++++++++++++++++++---------- src/backend/rug.rs | 37 +++++++++++++++++++++++++------ src/decryption_key.rs | 3 ++- src/encryption_key.rs | 3 ++- src/lib.rs | 5 ++++- tests/integration/backend.rs | 35 ++++++++++++++++++++++-------- tests/integration/main.rs | 37 +++++++++++++++++++++++++------ 9 files changed, 153 insertions(+), 48 deletions(-) diff --git a/benches/comparison.rs b/benches/comparison.rs index 926f754..d1f778e 100644 --- a/benches/comparison.rs +++ b/benches/comparison.rs @@ -30,8 +30,8 @@ fn encryption(c: &mut criterion::Criterion) { let mut group = c.benchmark_group("Encrypt"); let mut generate_inputs = || { - let x = ek.n().random_below_ref(&mut rng) - ek.half_n(); - let nonce = Integer::sample_in_mult_group_of(&mut rng, ek.n()); + let x = ek.n().random_below_ref(&mut rng).unwrap() - ek.half_n(); + let nonce = Integer::sample_in_mult_group_of(&mut rng, ek.n()).unwrap(); (x, nonce) }; @@ -83,7 +83,7 @@ fn decryption(c: &mut criterion::Criterion) { let mut group = c.benchmark_group("Decrypt"); - let mut generate_inputs = || Integer::sample_in_mult_group_of(&mut rng, ek.nn()); + let mut generate_inputs = || Integer::sample_in_mult_group_of(&mut rng, ek.nn()).unwrap(); group.bench_function("Decrypt with CRT", |b| { b.iter_batched( @@ -123,8 +123,8 @@ fn omul(c: &mut criterion::Criterion) { let mut group = c.benchmark_group("OMul"); let mut generate_inputs = || { - let scalar = Integer::sample_pm_in_mult_group_of(&mut rng, ek.n()); - let enc_x = Integer::sample_in_mult_group_of(&mut rng, ek.nn()); + let scalar = Integer::sample_pm_in_mult_group_of(&mut rng, ek.n()).unwrap(); + let enc_x = Integer::sample_in_mult_group_of(&mut rng, ek.nn()).unwrap(); (scalar, enc_x) }; diff --git a/src/backend.rs b/src/backend.rs index e21047d..b241ca1 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -73,28 +73,34 @@ impl Integer { } /// Samples `x` in Z*_n - pub fn sample_in_mult_group_of(rng: &mut impl rand_core::RngCore, n: &Self) -> Self { + pub fn sample_in_mult_group_of( + rng: &mut impl rand_core::RngCore, + n: &Self, + ) -> Result { let mut x = Integer::zero(); loop { - x.assign_random_below(n, rng); + x.assign_random_below(n, rng)?; if x.in_mult_group_of(n) { - return x; + return Ok(x); } } } /// Samples `x` such that abs(x) is in `Z*_n` - pub fn sample_pm_in_mult_group_of(rng: &mut impl rand_core::RngCore, n: &Self) -> Self { + pub fn sample_pm_in_mult_group_of( + rng: &mut impl rand_core::RngCore, + n: &Self, + ) -> Result { let mut x = Integer::zero(); let mut sign_buf = [0u8; 1]; loop { - x.assign_random_below(n, rng); + x.assign_random_below(n, rng)?; rng.fill_bytes(&mut sign_buf); if sign_buf[0] & 1 == 1 { x = -x; } if x.abs_in_mult_group_of(n) { - return x; + return Ok(x); } } } @@ -320,6 +326,17 @@ mod serialize { } } +/// Error returned by methods on integer when provided inputs are not acceptable +#[derive(Debug, thiserror::Error)] +#[error("invalid input: {0}")] +pub struct InvalidInput(#[from] InvalidInputReason); + +#[derive(Debug, thiserror::Error)] +enum InvalidInputReason { + #[error("random_below: upper bound must be positive")] + RandomBelowUpperBound, +} + #[cfg(test)] mod test { use super::Integer; diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index 3dd0ad2..fe045bd 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -2,10 +2,12 @@ use alloc::{string::String, vec::Vec}; -use super::IsPrime; use num_integer::Integer as _; use num_traits::Signed as _; +use super::IsPrime; +use crate::backend::{InvalidInput, InvalidInputReason}; + /// Big integer type used in this crate #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] pub struct Integer(num_bigint::BigInt); @@ -255,26 +257,41 @@ impl Integer { self } - pub fn random_below(self, rng: &mut impl rand_core::RngCore) -> Self { + /// Generates random integer from range 0..self (exlusive) + /// + /// Returns an error if `self` is not positive number + pub fn random_below(self, rng: &mut impl rand_core::RngCore) -> Result { + if self.cmp0() != core::cmp::Ordering::Greater { + return Err(InvalidInputReason::RandomBelowUpperBound.into()); + } // when the number is a power of two, the randomness generation between // rug and nbi differs, and rug uses exactly the same as random_bits let bits = self.0.bits() - 1; - if self.0.trailing_zeros() == Some(bits) { + Ok(if self.0.trailing_zeros() == Some(bits) { Self::random_bits_u64(bits, rng) } else { let range = num_traits::ConstZero::ZERO..self.0; Integer(rand::Rng::gen_range(rng, range)) - } + }) } - pub fn random_below_ref(&self, rng: &mut impl rand_core::RngCore) -> Self { + /// Generates random integer from range 0..self (exlusive) + /// + /// Returns an error if `self` is not positive number + pub fn random_below_ref( + &self, + rng: &mut impl rand_core::RngCore, + ) -> Result { + if self.cmp0() != core::cmp::Ordering::Greater { + return Err(InvalidInputReason::RandomBelowUpperBound.into()); + } // see the note in random_below let bits = self.0.bits() - 1; - if self.0.trailing_zeros() == Some(bits) { + Ok(if self.0.trailing_zeros() == Some(bits) { Self::random_bits_u64(bits, rng) } else { let range = num_traits::ConstZero::ZERO..self.0.clone(); Integer(rand::Rng::gen_range(rng, range)) - } + }) } pub fn random_bits(bits: u32, rng: &mut impl rand_core::RngCore) -> Self { Self::random_bits_u64(bits.into(), rng) @@ -299,13 +316,16 @@ impl Integer { Integer(num_bigint::BigInt::from_biguint(sign, uint)) } + /// Generates random integer from range 0..modulo (exlusive) and assigns to `self` + /// + /// Returns an error if modulo is not positive. `self` is not modified in this case. pub fn assign_random_below( &mut self, modulo: &Self, rng: &mut impl rand_core::RngCore, - ) -> &mut Self { - *self = modulo.random_below_ref(rng); - self + ) -> Result<&mut Self, InvalidInput> { + *self = modulo.random_below_ref(rng)?; + Ok(self) } pub fn assign_random_bits( @@ -518,7 +538,7 @@ mod test { let n = &p * &q; for _ in 0..100 { - let x = n.random_below_ref(&mut rng); + let x = n.random_below_ref(&mut rng).unwrap(); let root = blum_sqrt(&x, &p, &q, &n); let x_ = root.square().modulo(&n); diff --git a/src/backend/rug.rs b/src/backend/rug.rs index d7f7e13..98025a2 100644 --- a/src/backend/rug.rs +++ b/src/backend/rug.rs @@ -2,9 +2,11 @@ use alloc::{string::String, vec::Vec}; -use super::IsPrime; use rug::Complete; +use super::IsPrime; +use crate::backend::{InvalidInput, InvalidInputReason}; + /// Big integer type used in this crate #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] pub struct Integer(rug::Integer); @@ -218,13 +220,28 @@ impl Integer { self } - pub fn random_below(self, rng: &mut impl rand_core::RngCore) -> Self { + /// Generates random integer from range 0..self (exlusive) + /// + /// Returns an error if `self` is not positive number + pub fn random_below(self, rng: &mut impl rand_core::RngCore) -> Result { + if self.cmp0() != core::cmp::Ordering::Greater { + return Err(InvalidInputReason::RandomBelowUpperBound.into()); + } let mut rng = external_rand(rng); - Integer(self.0.random_below(&mut rng)) + Ok(Integer(self.0.random_below(&mut rng))) } - pub fn random_below_ref(&self, rng: &mut impl rand_core::RngCore) -> Self { + /// Generates random integer from range 0..self (exlusive) + /// + /// Returns an error if `self` is not positive number + pub fn random_below_ref( + &self, + rng: &mut impl rand_core::RngCore, + ) -> Result { + if self.cmp0() != core::cmp::Ordering::Greater { + return Err(InvalidInputReason::RandomBelowUpperBound.into()); + } let mut rng = external_rand(rng); - Integer(self.0.random_below_ref(&mut rng).complete()) + Ok(Integer(self.0.random_below_ref(&mut rng).complete())) } pub fn random_bits(bits: u32, rng: &mut impl rand_core::RngCore) -> Self { let mut rng = external_rand(rng); @@ -241,15 +258,21 @@ impl Integer { } } + /// Generates random integer from range 0..modulo (exlusive) and assigns to `self` + /// + /// Returns an error if modulo is not positive. `self` is not modified in this case. pub fn assign_random_below( &mut self, modulo: &Self, rng: &mut impl rand_core::RngCore, - ) -> &mut Self { + ) -> Result<&mut Self, InvalidInput> { + if modulo.cmp0() != core::cmp::Ordering::Greater { + return Err(InvalidInputReason::RandomBelowUpperBound.into()); + } let mut rng = external_rand(rng); let r = modulo.0.random_below_ref(&mut rng); rug::Assign::assign(&mut self.0, r); - self + Ok(self) } pub fn assign_random_bits( diff --git a/src/decryption_key.rs b/src/decryption_key.rs index ae3cda0..9ed89c4 100644 --- a/src/decryption_key.rs +++ b/src/decryption_key.rs @@ -135,7 +135,8 @@ impl DecryptionKey { rng: &mut (impl RngCore + CryptoRng), x: &Plaintext, ) -> Result<(Ciphertext, Nonce), Error> { - let nonce = Integer::sample_in_mult_group_of(rng, self.ek.n()); + let nonce = Integer::sample_in_mult_group_of(rng, self.ek.n()) + .map_err(crate::Bug::SampleInMultGroup)?; let ciphertext = self.encrypt_with(x, &nonce)?; Ok((ciphertext, nonce)) } diff --git a/src/encryption_key.rs b/src/encryption_key.rs index 2262b65..1118631 100644 --- a/src/encryption_key.rs +++ b/src/encryption_key.rs @@ -90,7 +90,8 @@ impl EncryptionKey { rng: &mut (impl RngCore + CryptoRng), x: &Plaintext, ) -> Result<(Ciphertext, Nonce), Error> { - let nonce = Integer::sample_in_mult_group_of(rng, self.n()); + let nonce = Integer::sample_in_mult_group_of(rng, self.n()) + .map_err(crate::Bug::SampleInMultGroup)?; let ciphertext = self.encrypt_with(x, &nonce)?; Ok((ciphertext, nonce)) } diff --git a/src/lib.rs b/src/lib.rs index 2453154..cfdc831 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,8 @@ enum Reason { enum Bug { #[error("pow mod undefined")] PowModUndef, + #[error("sample_in_mult_group_of: {0}")] + SampleInMultGroup(backend::InvalidInput), } impl From for Error { @@ -171,7 +173,8 @@ impl AnyEncryptionKeyExt for E { rng: &mut (impl RngCore + CryptoRng), x: &Plaintext, ) -> Result<(Ciphertext, Nonce), Error> { - let nonce = Integer::sample_in_mult_group_of(rng, self.n()); + let nonce = + Integer::sample_in_mult_group_of(rng, self.n()).map_err(Bug::SampleInMultGroup)?; let ciphertext = self.encrypt_with(x, &nonce)?; Ok((ciphertext, nonce)) } diff --git a/tests/integration/backend.rs b/tests/integration/backend.rs index cc5aaaa..342cf0d 100644 --- a/tests/integration/backend.rs +++ b/tests/integration/backend.rs @@ -12,6 +12,9 @@ macro_rules! make_quickcheck { // Case for normal methods. Called like `make_quickcheck!(method_name(self: // T1, arg: T2, arg2: T3))` ($method:ident ( self: $self_ty:ty $(, $arg:ident: $t:ty)* $(,)? ) ) => { + make_quickcheck!($method(self: $self_ty $(,$arg: $t)*), |a, b| a.equals(b)); + }; + ($method:ident ( self: $self_ty:ty $(, $arg:ident: $t:ty)* $(,)? ), |$r1:ident, $r2: ident| $compare:expr ) => { quickcheck::quickcheck! { fn $method(nbi: $self_ty $(, $arg: $t)*) -> bool { #[allow(unused_mut)] @@ -21,7 +24,7 @@ macro_rules! make_quickcheck { let mut rug = RugInteger::from_bytes_msf_signed(&bytes, sign); - let r1 = { + let $r1 = { // Since Arg::to_nbi takes mutable reference to the arg, we clone them, // so the `rug` method below receives the same args as provided by // quickcheck without any modification @@ -36,7 +39,7 @@ macro_rules! make_quickcheck { )* ) }; - let r2 = { + let $r2 = { // Change args mutability $( let mut $arg = $arg; @@ -49,7 +52,7 @@ macro_rules! make_quickcheck { ) }; - r1.equals(r2) + $compare } } }; @@ -96,6 +99,20 @@ macro_rules! make_quickcheck { }; } +/// Compare function to be used with methods that return `Result<..., E>` where `E` +/// does not implement `Eq` +fn compare_results(r1: Result, r2: Result) -> bool +where + A: HeteroEq, + E: core::fmt::Debug, +{ + match (r1, r2) { + (Ok(a), Ok(b)) => a.equals(b), + (Err(a), Err(b)) => format!("{a:?}") == format!("{b:?}"), + _ => false, + } +} + make_quickcheck!(is_one(self: NbiInteger)); make_quickcheck!(is_even(self: NbiInteger)); make_quickcheck!(cmp_abs(self: NbiInteger, other: RefInteger)); @@ -122,16 +139,16 @@ make_quickcheck!(significant_dwords(self: NbiInteger)); make_quickcheck!(invert(self: NbiInteger, modulo: RefInteger)); make_quickcheck!(invert_ref(self: NbiInteger, modulo: RefInteger)); make_quickcheck!(set_bit(self: NbiInteger, index: SmallU32, value: bool)); -make_quickcheck!(random_below(self: Positive, rng: Prng)); -make_quickcheck!(random_below_ref(self: Positive, rng: Prng)); +make_quickcheck!(random_below(self: NbiInteger, rng: Prng), |a, b| compare_results(a, b)); +make_quickcheck!(random_below_ref(self: NbiInteger, rng: Prng), |a, b| compare_results(a, b)); make_quickcheck!(Self::random_bits(bits: SmallU32, rng: Prng)); make_quickcheck!(Self::random_bits_signed(bits: SmallU32, rng: Prng)); -make_quickcheck!(assign_random_below(self: NbiInteger, modulo: Positive, rng: Prng)); +make_quickcheck!(assign_random_below(self: NbiInteger, modulo: RefInteger, rng: Prng), |a, b| compare_results(a, b)); make_quickcheck!(assign_random_bits(self: NbiInteger, bits: SmallU32, rng: Prng)); make_quickcheck!(jacobi(self: NbiInteger, n: OddPositive)); make_quickcheck!( combine( - self: NonZero, + self: NbiInteger, l: RefInteger, le: RefInteger, r: RefInteger, @@ -150,8 +167,8 @@ fn round_random() { let rug = RugInteger::from(8); for _ in 0..32 { - let nbi_random = nbi.random_below_ref(&mut nbi_rng); - let rug_random = rug.random_below_ref(&mut rug_rng); + let nbi_random = nbi.random_below_ref(&mut nbi_rng).unwrap(); + let rug_random = rug.random_below_ref(&mut rug_rng).unwrap(); assert!(nbi_random.equals(rug_random)); } } diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 6dee159..bd0aabd 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -16,7 +16,7 @@ fn encrypt_decrypt() { for _ in 0..50 { // Generate plaintext in [-N/2; N/2) - let plaintext = ek.n().random_below_ref(&mut rng); + let plaintext = ek.n().random_below_ref(&mut rng).unwrap(); let plaintext = plaintext - (ek.n() / 2u8); println!("Plaintext: {plaintext}"); @@ -79,8 +79,8 @@ fn homorphic_ops() { let ek = dk.encryption_key(); for _ in 0..100 { - let a = ek.n().random_below_ref(&mut rng); - let b = ek.n().random_below_ref(&mut rng); + let a = ek.n().random_below_ref(&mut rng).unwrap(); + let b = ek.n().random_below_ref(&mut rng).unwrap(); let a = a - (ek.n() / 2u8); let b = b - (ek.n() / 2u8); println!("a: {a}"); @@ -127,10 +127,10 @@ fn encryption_with_known_factorization() { for i in 0..100 { println!("Iteration {i}"); - let x = ek.n().random_below_ref(&mut rng); + let x = ek.n().random_below_ref(&mut rng).unwrap(); let x = x - ek.half_n(); - let nonce = Integer::sample_in_mult_group_of(&mut rng, ek.n()); + let nonce = Integer::sample_in_mult_group_of(&mut rng, ek.n()).unwrap(); let enc_x1 = ek.encrypt_with(&x, &nonce).unwrap(); let enc_x2 = dk.encrypt_with(&x, &nonce).unwrap(); @@ -151,7 +151,7 @@ fn factorized_exp_mod_n() { let crt = utils::CrtExp::build_n(&p, &q).unwrap(); for _ in 0..100 { - let x: Integer = n.random_below_ref(&mut rng); + let x: Integer = n.random_below_ref(&mut rng).unwrap(); let mut e: Integer = Integer::random_bits(1024, &mut rng); if rng.gen::() { e = -e @@ -180,7 +180,7 @@ fn factorized_exp_mod_nn() { let crt = utils::CrtExp::build_nn(&p, &q).unwrap(); for _ in 0..100 { - let x = nn.random_below_ref(&mut rng); + let x = nn.random_below_ref(&mut rng).unwrap(); let mut e = Integer::random_bits(1024, &mut rng); if rng.gen::() { e = -e @@ -222,3 +222,26 @@ fn random_key_for_tests(rng: &mut impl rand_core::RngCore) -> DecryptionKey { fn bigint_from_radix_prohibits_whitespaces() { assert_eq!(Integer::from_str_radix("123 4", 10), None) } + +#[test] +fn sample_in_multgroup_of_nonpositive_returns_err() { + let mut rng = rand_dev::DevRng::new(); + let mut x = 0u8; + rand_core::RngCore::fill_bytes(&mut rng, core::slice::from_mut(&mut x)); + + for x in (0..20).chain([x]) { + let x = -Integer::from(x); + let result = Integer::sample_in_mult_group_of(&mut rng, &x); + + assert!( + result.is_err(), + "sampling from nonpositive group must return an error" + ); + + let result = Integer::sample_pm_in_mult_group_of(&mut rng, &x); + assert!( + result.is_err(), + "sampling from nonpositive group must return an error" + ); + } +} From 85cd18ea754bf6b17ee9e115429501caa54445bc Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 19 Oct 2026 13:51:24 +0200 Subject: [PATCH 06/14] fix!: Integer::jacobi returns error if input is not valid Signed-off-by: Denis Varlakov --- src/backend.rs | 2 ++ src/backend/num_bigint.rs | 19 +++++++++---------- src/backend/rug.rs | 11 +++++++++-- tests/integration/backend.rs | 34 +--------------------------------- 4 files changed, 21 insertions(+), 45 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index b241ca1..1a90526 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -335,6 +335,8 @@ pub struct InvalidInput(#[from] InvalidInputReason); enum InvalidInputReason { #[error("random_below: upper bound must be positive")] RandomBelowUpperBound, + #[error("jacobi: `n` must be an odd number larger than 2")] + Jacobi, } #[cfg(test)] diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index fe045bd..6ffa0fb 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -391,21 +391,20 @@ impl Integer { /// Compute jacobi symbol of a over n /// - /// Requires odd `n >= 3`. If it doesn't hold, function - /// panics if debug asserts are enabled, or returns 0 otherwise. + /// Requires odd `n >= 3`. Returns an error if `n` is not in required domain. /// /// Implementation is taken from [Handbook of Applied cryptography][book], /// p. 73, Algorithm 2.149 /// /// [book]: https://cacr.uwaterloo.ca/hac/about/chap2.pdf #[inline(always)] - pub fn jacobi(&self, n: &Self) -> i32 { + pub fn jacobi(&self, n: &Self) -> Result { if n.0.is_even() || n.0 <= num_bigint::BigInt::from(2) { - debug_assert!(false, "n should be an odd number larger than 2"); - return 0; + Err(InvalidInputReason::Jacobi.into()) + } else { + let a = self.modulo_ref(n); + Ok(jacobi_inner(1, &a.0, &n.0)) } - let a = self.modulo_ref(n); - jacobi_inner(1, &a.0, &n.0) } /// Compute l^le * r^re modulo self @@ -542,9 +541,9 @@ mod test { let root = blum_sqrt(&x, &p, &q, &n); let x_ = root.square().modulo(&n); - let jp = x.modulo_ref(&p).jacobi(&p); - let jq = x.modulo_ref(&q).jacobi(&q); - let j = x.jacobi(&n); + let jp = x.modulo_ref(&p).jacobi(&p).unwrap(); + let jq = x.modulo_ref(&q).jacobi(&q).unwrap(); + let j = x.jacobi(&n).unwrap(); assert_eq!(jp * jq, j); if jp == 1 && jq == 1 { diff --git a/src/backend/rug.rs b/src/backend/rug.rs index 98025a2..bb82bd1 100644 --- a/src/backend/rug.rs +++ b/src/backend/rug.rs @@ -334,8 +334,15 @@ impl Integer { } } - pub fn jacobi(&self, n: &Self) -> i32 { - self.0.jacobi(&n.0) + /// Compute jacobi symbol of a over n + /// + /// Requires odd `n >= 3`. Returns an error if `n` is not in required domain. + pub fn jacobi(&self, n: &Self) -> Result { + if n.0.is_even() || n.0 <= 2 { + Err(InvalidInputReason::Jacobi.into()) + } else { + Ok(self.0.jacobi(&n.0)) + } } /// Compute l^le * r^re modulo self diff --git a/tests/integration/backend.rs b/tests/integration/backend.rs index 342cf0d..a3255bc 100644 --- a/tests/integration/backend.rs +++ b/tests/integration/backend.rs @@ -145,7 +145,7 @@ make_quickcheck!(Self::random_bits(bits: SmallU32, rng: Prng)); make_quickcheck!(Self::random_bits_signed(bits: SmallU32, rng: Prng)); make_quickcheck!(assign_random_below(self: NbiInteger, modulo: RefInteger, rng: Prng), |a, b| compare_results(a, b)); make_quickcheck!(assign_random_bits(self: NbiInteger, bits: SmallU32, rng: Prng)); -make_quickcheck!(jacobi(self: NbiInteger, n: OddPositive)); +make_quickcheck!(jacobi(self: NbiInteger, n: RefInteger), |a, b| compare_results(a, b)); make_quickcheck!( combine( self: NbiInteger, @@ -323,38 +323,6 @@ impl From for RefInteger { } } -/// Helper quickcheck newtype. Similar to [`RefInteger`], but generates integers -/// that are odd and larger than 2 (to be used as an argument to `jacobi`) -#[derive(Clone, Debug)] -struct OddPositive(NbiInteger, RugInteger); -impl<'a> Arg<'a> for OddPositive { - type NbiArg = &'a NbiInteger; - type RugArg = &'a RugInteger; - fn to_nbi(&'a mut self) -> Self::NbiArg { - &self.0 - } - fn to_rug(&'a mut self) -> Self::RugArg { - &self.1 - } -} -impl quickcheck::Arbitrary for OddPositive { - fn arbitrary(g: &mut quickcheck::Gen) -> Self { - let nbi = NbiInteger::arbitrary(g); - - // make odd positive - let (_, nbi) = nbi.to_num_bigint().into_parts(); - let nbi = num_bigint::BigInt::from_biguint(num_bigint::Sign::Plus, nbi); - let mut nbi = NbiInteger::from_num_bigint(nbi); - if nbi < NbiInteger::from(2) { - nbi |= 2; - } - nbi |= 1; - - let rug = RugInteger::from_bytes_msf(&nbi.to_bytes_msf()); - OddPositive(nbi, rug) - } -} - /// Helper quickcheck newtype: generates a u32 that is small enough to be used /// as a bit index or a (non-modular) exponent #[derive(Clone, Copy, Debug)] From 363c88568b3e0d0f03e0f33265ce8d533f8b07cc Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 16 Oct 2026 17:07:16 +0200 Subject: [PATCH 07/14] fix: num-bigint panic on negative shift in shl/shr Signed-off-by: Denis Varlakov --- src/backend/macro_defs.rs | 2 - src/backend/num_bigint.rs | 44 +++++++++++++++++++++ src/backend/rug.rs | 28 ++++++++++++++ tests/integration/backend.rs | 75 ++++++++++++++++++++++++++++++------ 4 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/backend/macro_defs.rs b/src/backend/macro_defs.rs index 3a22811..7e86f5f 100644 --- a/src/backend/macro_defs.rs +++ b/src/backend/macro_defs.rs @@ -165,10 +165,8 @@ macro_rules! make_all_ops { $crate::backend::macro_defs::make_assign!(Integer, RemAssign, rem_assign); $crate::backend::macro_defs::make_ops_for_primitive!(Integer, Shl, shl, u32 $(, $complete)?); - $crate::backend::macro_defs::make_ops_for_primitive!(Integer, Shl, shl, i32 $(, $complete)?); $crate::backend::macro_defs::make_ops_for_primitive!(Integer, Shl, shl, usize $(, $complete)?); $crate::backend::macro_defs::make_ops_for_primitive!(Integer, Shr, shr, u32 $(, $complete)?); - $crate::backend::macro_defs::make_ops_for_primitive!(Integer, Shr, shr, i32 $(, $complete)?); $crate::backend::macro_defs::make_ops_for_primitive!(Integer, Shr, shr, usize $(, $complete)?); impl core::ops::Neg for $type { diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index 6ffa0fb..cb7fec3 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -475,6 +475,50 @@ fn last_limb(x: &num_bigint::BigInt) -> u32 { x.magnitude().iter_u32_digits().next().unwrap_or(0) } +// num-bigint panics if `<<` or `>>` are called with negative value of i32. +// rug just reverses the direction: (x << -n) == (x >> n). +// We manually reproduce rug's behavior. +impl core::ops::Shl for Integer { + type Output = Integer; + fn shl(self, rhs: i32) -> Self::Output { + if rhs >= 0 { + Self(self.0 << rhs) + } else if rhs == i32::MIN { + // `-rhs` would overflow i32 here + // i32::MIN = -(i32::MAX + 1) + Self(self.0 >> i32::MAX >> 1) + } else { + Self(self.0 >> (-rhs)) + } + } +} +impl core::ops::Shr for Integer { + type Output = Integer; + fn shr(self, rhs: i32) -> Self::Output { + if rhs >= 0 { + Self(self.0 >> rhs) + } else if rhs == i32::MIN { + // `-rhs` would overflow i32 here + // i32::MIN = -(i32::MAX + 1) + Self(self.0 << i32::MAX << 1) + } else { + Self(self.0 << (-rhs)) + } + } +} +impl core::ops::Shl for &Integer { + type Output = Integer; + fn shl(self, rhs: i32) -> Self::Output { + self.clone() << rhs + } +} +impl core::ops::Shr for &Integer { + type Output = Integer; + fn shr(self, rhs: i32) -> Self::Output { + self.clone() >> rhs + } +} + #[cfg(feature = "quickcheck")] impl quickcheck::Arbitrary for Integer { fn arbitrary(g: &mut quickcheck::Gen) -> Self { diff --git a/src/backend/rug.rs b/src/backend/rug.rs index bb82bd1..154f77e 100644 --- a/src/backend/rug.rs +++ b/src/backend/rug.rs @@ -392,3 +392,31 @@ impl quickcheck::Arbitrary for Integer { })) } } + +// num_bigint have its own implementation of shl/shr, so we have to write manual dummy +// implementation for rug too, instead of outsourcing it to the macro that handles the rest +// arithmetic ops +impl core::ops::Shl for Integer { + type Output = Integer; + fn shl(self, rhs: i32) -> Self::Output { + Self(self.0 << rhs) + } +} +impl core::ops::Shr for Integer { + type Output = Integer; + fn shr(self, rhs: i32) -> Self::Output { + Self(self.0 >> rhs) + } +} +impl core::ops::Shl for &Integer { + type Output = Integer; + fn shl(self, rhs: i32) -> Self::Output { + self.clone() << rhs + } +} +impl core::ops::Shr for &Integer { + type Output = Integer; + fn shr(self, rhs: i32) -> Self::Output { + self.clone() >> rhs + } +} diff --git a/tests/integration/backend.rs b/tests/integration/backend.rs index a3255bc..90377e7 100644 --- a/tests/integration/backend.rs +++ b/tests/integration/backend.rs @@ -174,24 +174,19 @@ fn round_random() { } quickcheck::quickcheck! { - fn serde_json(nbi: NbiInteger) -> bool { - let r1 = serde_json::to_string(&nbi).unwrap(); - - let (bytes, sign) = nbi.to_bytes_msf_signed(); - let rug = RugInteger::from_bytes_msf_signed(&bytes, sign); - let r2 = serde_json::to_string(&rug).unwrap(); + fn serde_json(x: RefInteger) -> bool { + let r1 = serde_json::to_string(&x.0).unwrap(); + let r2 = serde_json::to_string(&x.1).unwrap(); r1 == r2 } - fn is_probably_prime(nbi: NbiInteger, rng: Prng) -> bool { + fn is_probably_prime(x: RefInteger, rng: Prng) -> bool { use fast_paillier::backend::IsPrime; let mut rng = rng; - let (bytes, sign) = nbi.to_bytes_msf_signed(); - let rug = RugInteger::from_bytes_msf_signed(&bytes, sign); - let r1 = nbi.is_probably_prime(25, &mut rng.rng); - let r2 = rug.is_probably_prime(25, &mut rng.rng); + let r1 = x.0.is_probably_prime(25, &mut rng.rng); + let r2 = x.1.is_probably_prime(25, &mut rng.rng); // It doesn't matter if one backend returns `Yes`, and the other returns `Probably`. // It matters to check if one returns `Yes|Probably`, and the other returns `No` @@ -208,6 +203,64 @@ quickcheck::quickcheck! { _ => false } } + + fn shl_i32(x: RefInteger, shift: i32) -> bool { + let shift = shift % 100; + let r1 = x.0 << shift; + let r2 = x.1 << shift; + r1.equals(r2) + } + fn shr_i32(x: RefInteger, shift: i32) -> bool { + let shift = shift % 100; + let r1 = x.0 >> shift; + let r2 = x.1 >> shift; + r1.equals(r2) + } + fn shl_u32(x: RefInteger, shift: u32) -> bool { + let shift = shift % 100; + let r1 = x.0 << shift; + let r2 = x.1 << shift; + r1.equals(r2) + } + fn shr_u32(x: RefInteger, shift: u32) -> bool { + let shift = shift % 100; + let r1 = x.0 >> shift; + let r2 = x.1 >> shift; + r1.equals(r2) + } + fn shl_usize(x: RefInteger, shift: usize) -> bool { + let shift = shift % 100; + let r1 = x.0 << shift; + let r2 = x.1 << shift; + r1.equals(r2) + } + fn shr_usize(x: RefInteger, shift: usize) -> bool { + let shift = shift % 100; + let r1 = x.0 >> shift; + let r2 = x.1 >> shift; + r1.equals(r2) + } +} + +#[test] +fn shift_by_i32_min() { + for x in [5i32, -5, 0, 1, -1] { + let nbi = NbiInteger::from(x); + let rug = RugInteger::from(x); + + // x << i32::MIN == x >> 2^31: 0 for non-negative x, -1 for negative x + assert!((&nbi << i32::MIN).equals(&rug << i32::MIN)); + assert!((nbi.clone() << i32::MIN).equals(rug.clone() << i32::MIN)); + let expected = if x < 0 { -1 } else { 0 }; + assert_eq!(nbi << i32::MIN, NbiInteger::from(expected)); + } + + // x >> i32::MIN == x << 2^31, which allocates ton of memory on heap for non-zero x, so only + // check zero to make sure it doesn't panic + let nbi = NbiInteger::from(0u8); + let rug = RugInteger::from(0u8); + assert!((&nbi >> i32::MIN).equals(&rug >> i32::MIN)); + assert!((nbi >> i32::MIN).equals(rug >> i32::MIN)); } ///// Helper traits for macro ///// From 283f695b89cefd0bd5ed935030d4bf96af16ceff Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 19 Oct 2026 14:01:03 +0200 Subject: [PATCH 08/14] fix!: generate_safe_primes returns an error for unsupported bit size Signed-off-by: Denis Varlakov --- src/backend.rs | 34 +++++++++++++++++++++++++++------- src/decryption_key.rs | 4 ++-- src/lib.rs | 2 ++ tests/integration/main.rs | 27 +++++++++++++++++++++------ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index 1a90526..eb3a9b7 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -106,7 +106,13 @@ impl Integer { } /// Generates a random safe prime - pub fn generate_safe_prime(rng: &mut impl rand_core::RngCore, bits: u32) -> Self { + /// + /// Returns an error if `bits < 100`. Note that cryptographic primes need to be way larger. + /// E.g. RSA requires primes to be at least 1536-bits. + pub fn generate_safe_prime( + rng: &mut impl rand_core::RngCore, + bits: u32, + ) -> Result { sieve_generate_safe_primes(rng, bits, 135) } } @@ -118,14 +124,24 @@ impl Integer { /// The best way to select the parameter is by trial. The one used by /// [`generate_safe_prime`] is indistinguishable from optimal for 500-1700 bit /// lengths. +/// +/// Returns an error if `bits < 100`. Note that cryptographic primes need to be way larger. +/// E.g. RSA requires primes to be at least 1536-bits. pub fn sieve_generate_safe_primes( rng: &mut impl rand_core::RngCore, bits: u32, amount: usize, -) -> Integer { +) -> Result { use crate::backend::IsPrime; use crate::utils::small_primes; + // sieve optimization, the way it is written, rejects valid safe primes of small bitsize. + // The fix would be to use subset of `small_primes` for small `bits`, but we never want + // to generate small safe primes, so no point in complicating the code. + if bits < 100 { + return Err(InvalidInputReason::SafePrimeBitSize.into()); + } + let amount = amount.min(small_primes::SMALL_PRIMES.len()); let mut x = Integer::zero(); @@ -149,7 +165,7 @@ pub fn sieve_generate_safe_primes( x <<= 1; x += 1; if let IsPrime::Yes | IsPrime::Probably = x.is_probably_prime_without_sieve() { - return x; + return Ok(x); } } } @@ -337,6 +353,12 @@ enum InvalidInputReason { RandomBelowUpperBound, #[error("jacobi: `n` must be an odd number larger than 2")] Jacobi, + #[error( + "generate_safe_prime algorithm can only generate primes from 100 bits; note that \ + cryptographic primes need to be way larger. E.g. RSA requires primes to be at least \ + 1536-bits." + )] + SafePrimeBitSize, } #[cfg(test)] @@ -347,10 +369,8 @@ mod test { fn safe_prime_size() { let mut rng = rand_dev::DevRng::new(); for size in [500, 512, 513, 514] { - let mut prime = Integer::generate_safe_prime(&mut rng, size); - // rug doesn't have bit length operations, so - prime >>= size - 1; - assert_eq!(prime, Integer::one()); + let prime = Integer::generate_safe_prime(&mut rng, size).unwrap(); + assert_eq!(prime.significant_bits(), size as u64); } } diff --git a/src/decryption_key.rs b/src/decryption_key.rs index 9ed89c4..e5bf9e5 100644 --- a/src/decryption_key.rs +++ b/src/decryption_key.rs @@ -28,8 +28,8 @@ impl DecryptionKey { /// /// Samples two safe 1536-bits primes that meets 128 bits security level pub fn generate(rng: &mut (impl RngCore + CryptoRng)) -> Result { - let p = Integer::generate_safe_prime(rng, 1536); - let q = Integer::generate_safe_prime(rng, 1536); + let p = Integer::generate_safe_prime(rng, 1536).map_err(crate::Bug::GenerateSafePrime)?; + let q = Integer::generate_safe_prime(rng, 1536).map_err(crate::Bug::GenerateSafePrime)?; Self::from_primes(p, q) } diff --git a/src/lib.rs b/src/lib.rs index cfdc831..8930323 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,8 @@ enum Bug { PowModUndef, #[error("sample_in_mult_group_of: {0}")] SampleInMultGroup(backend::InvalidInput), + #[error("generate_safe_prime: {0}")] + GenerateSafePrime(backend::InvalidInput), } impl From for Error { diff --git a/tests/integration/main.rs b/tests/integration/main.rs index bd0aabd..4842b21 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -143,8 +143,8 @@ fn encryption_with_known_factorization() { fn factorized_exp_mod_n() { let mut rng = rand_dev::DevRng::new(); - let p = Integer::generate_safe_prime(&mut rng, 512); - let q = Integer::generate_safe_prime(&mut rng, 512); + let p = Integer::generate_safe_prime(&mut rng, 512).unwrap(); + let q = Integer::generate_safe_prime(&mut rng, 512).unwrap(); let n = &p * &q; println!("n: {n}"); @@ -172,8 +172,8 @@ fn factorized_exp_mod_n() { fn factorized_exp_mod_nn() { let mut rng = rand_dev::DevRng::new(); - let p = Integer::generate_safe_prime(&mut rng, 512); - let q = Integer::generate_safe_prime(&mut rng, 512); + let p = Integer::generate_safe_prime(&mut rng, 512).unwrap(); + let q = Integer::generate_safe_prime(&mut rng, 512).unwrap(); let nn = (&p * &q).square(); println!("nn: {nn}"); @@ -213,8 +213,8 @@ fn unsigned_mod_to_signed(x: Integer, n: &Integer) -> Integer { } fn random_key_for_tests(rng: &mut impl rand_core::RngCore) -> DecryptionKey { - let p = Integer::generate_safe_prime(rng, 512); - let q = Integer::generate_safe_prime(rng, 512); + let p = Integer::generate_safe_prime(rng, 512).unwrap(); + let q = Integer::generate_safe_prime(rng, 512).unwrap(); DecryptionKey::from_primes(p, q).unwrap() } @@ -245,3 +245,18 @@ fn sample_in_multgroup_of_nonpositive_returns_err() { ); } } + +#[test] +fn gen_safe_primes_panics_on_small_bitsize() { + let mut rng = rand_dev::DevRng::new(); + for bitsize in 0..100 { + let _ = fast_paillier::backend::sieve_generate_safe_primes(&mut rng, bitsize, 1000) + .unwrap_err(); + } +} + +#[test] +fn gen_safe_primes_100bits() { + let mut rng = rand_dev::DevRng::new(); + let _ = fast_paillier::backend::sieve_generate_safe_primes(&mut rng, 100, 1000); +} From f828fb84a3e251153e8d67d44edafd66549db7fb Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 19 Oct 2026 14:09:12 +0200 Subject: [PATCH 09/14] fix!: generate_prime returns an error on impossible bitsize Signed-off-by: Denis Varlakov --- benches/comparison.rs | 2 +- src/backend.rs | 2 ++ src/backend/num_bigint.rs | 12 ++++++++++-- src/backend/rug.rs | 12 ++++++++++-- tests/integration/main.rs | 13 +++++++++++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/benches/comparison.rs b/benches/comparison.rs index d1f778e..584553c 100644 --- a/benches/comparison.rs +++ b/benches/comparison.rs @@ -148,7 +148,7 @@ fn omul(c: &mut criterion::Criterion) { pub fn naive_safe_prime(rng: &mut impl rand_core::RngCore, bits: u32) -> Integer { use fast_paillier::backend::IsPrime; loop { - let mut x = Integer::generate_prime(rng, bits - 1); + let mut x = Integer::generate_prime(rng, bits - 1).unwrap(); x <<= 1; x += 1; diff --git a/src/backend.rs b/src/backend.rs index eb3a9b7..dbae3dc 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -359,6 +359,8 @@ enum InvalidInputReason { 1536-bits." )] SafePrimeBitSize, + #[error("generate_prime was called with `bit_size < 2`, there are no primes of this bit size")] + PrimeBitSize, } #[cfg(test)] diff --git a/src/backend/num_bigint.rs b/src/backend/num_bigint.rs index cb7fec3..e8cbaca 100644 --- a/src/backend/num_bigint.rs +++ b/src/backend/num_bigint.rs @@ -374,7 +374,15 @@ impl Integer { } /// Generates a random prime - pub fn generate_prime(rng: &mut impl rand_core::RngCore, bit_size: u32) -> Self { + /// + /// Returns an error if `bit_size < 2` as there are no primes of this bit size. + pub fn generate_prime( + rng: &mut impl rand_core::RngCore, + bit_size: u32, + ) -> Result { + if bit_size < 2 { + return Err(InvalidInputReason::PrimeBitSize.into()); + } let mut x = Integer::zero(); loop { x.assign_random_bits(bit_size, rng); @@ -384,7 +392,7 @@ impl Integer { x.is_probably_prime(0, rng), IsPrime::Probably | IsPrime::Yes ) { - break x; + break Ok(x); } } } diff --git a/src/backend/rug.rs b/src/backend/rug.rs index 154f77e..09e2787 100644 --- a/src/backend/rug.rs +++ b/src/backend/rug.rs @@ -319,7 +319,15 @@ impl Integer { } /// Generates a random prime - pub fn generate_prime(rng: &mut impl rand_core::RngCore, bit_size: u32) -> Self { + /// + /// Returns an error if `bit_size < 2` as there are no primes of this bit size. + pub fn generate_prime( + rng: &mut impl rand_core::RngCore, + bit_size: u32, + ) -> Result { + if bit_size < 2 { + return Err(InvalidInputReason::PrimeBitSize.into()); + } let mut x = Integer::zero(); loop { x.assign_random_bits(bit_size, rng); @@ -329,7 +337,7 @@ impl Integer { x.is_probably_prime(25, rng), IsPrime::Probably | IsPrime::Yes ) { - break x; + break Ok(x); } } } diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 4842b21..4b47b21 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -260,3 +260,16 @@ fn gen_safe_primes_100bits() { let mut rng = rand_dev::DevRng::new(); let _ = fast_paillier::backend::sieve_generate_safe_primes(&mut rng, 100, 1000); } + +#[test] +fn gen_prime_returns_error_on_small_bitsize() { + let mut rng = rand_dev::DevRng::new(); + for bitsize in 0..2 { + let _ = Integer::generate_prime(&mut rng, bitsize).unwrap_err(); + } +} +#[test] +fn gen_prime_2bits() { + let mut rng = rand_dev::DevRng::new(); + Integer::generate_prime(&mut rng, 2).unwrap(); +} From b2e0b2eed2cde9bedb54dd43829049062712efe8 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 16 Oct 2026 17:33:24 +0200 Subject: [PATCH 10/14] Add a few missing quickcheck tests Signed-off-by: Denis Varlakov --- tests/integration/backend.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/backend.rs b/tests/integration/backend.rs index 90377e7..1d459dc 100644 --- a/tests/integration/backend.rs +++ b/tests/integration/backend.rs @@ -158,6 +158,8 @@ make_quickcheck!( make_quickcheck!(to_bytes_msf(self: NbiInteger)); make_quickcheck!(to_bytes_msf_signed(self: NbiInteger)); make_quickcheck!(to_bytes_lsf(self: NbiInteger)); +make_quickcheck!(abs(self: NbiInteger)); +make_quickcheck!(sign(self: NbiInteger)); #[test] fn round_random() { From db5507cbb200f17072212a8349f762bb8338c908 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 19 Oct 2026 14:15:31 +0200 Subject: [PATCH 11/14] fix!: `sample[_pm]_in_mult_group_of` returns an error if `n <= 1` Current implementation panics if n == 0, and hangs in infinite loop if n == 1 Signed-off-by: Denis Varlakov --- src/backend.rs | 12 ++++++++++++ tests/integration/main.rs | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/backend.rs b/src/backend.rs index dbae3dc..931d4b1 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -73,10 +73,15 @@ impl Integer { } /// Samples `x` in Z*_n + /// + /// Returns an error if `n <= 1`. pub fn sample_in_mult_group_of( rng: &mut impl rand_core::RngCore, n: &Self, ) -> Result { + if *n <= Integer::one() { + return Err(InvalidInputReason::SampleInMultGroupSize.into()); + } let mut x = Integer::zero(); loop { x.assign_random_below(n, rng)?; @@ -87,10 +92,15 @@ impl Integer { } /// Samples `x` such that abs(x) is in `Z*_n` + /// + /// Returns an error if `n <= 1`. pub fn sample_pm_in_mult_group_of( rng: &mut impl rand_core::RngCore, n: &Self, ) -> Result { + if *n <= Integer::one() { + return Err(InvalidInputReason::SampleInMultGroupSize.into()); + } let mut x = Integer::zero(); let mut sign_buf = [0u8; 1]; loop { @@ -361,6 +371,8 @@ enum InvalidInputReason { SafePrimeBitSize, #[error("generate_prime was called with `bit_size < 2`, there are no primes of this bit size")] PrimeBitSize, + #[error("sample_in_[pm_]mult_group: `n` must be at least 2")] + SampleInMultGroupSize, } #[cfg(test)] diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 4b47b21..26006bf 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -273,3 +273,20 @@ fn gen_prime_2bits() { let mut rng = rand_dev::DevRng::new(); Integer::generate_prime(&mut rng, 2).unwrap(); } + +#[test] +fn sample_in_mult_group_0_or_1_return_error() { + let mut rng = rand_dev::DevRng::new(); + for n in 0..=1 { + Integer::sample_in_mult_group_of(&mut rng, &Integer::from(n)).unwrap_err(); + Integer::sample_pm_in_mult_group_of(&mut rng, &Integer::from(n)).unwrap_err(); + } +} + +#[test] +fn sample_in_mult_group_2_works() { + let mut rng = rand_dev::DevRng::new(); + + let x = Integer::sample_in_mult_group_of(&mut rng, &Integer::from(2)).unwrap(); + assert_eq!(x, Integer::one()); +} From 90dc6663f05cc3c27c36812e7bce978806a756a6 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Fri, 16 Oct 2026 17:39:58 +0200 Subject: [PATCH 12/14] Update changelog Signed-off-by: Denis Varlakov --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff9095a..406e137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +## v0.4.0 +* Fix divergent behavior on different integer backends. [#49] \ + Each of the following change is **possibly breaking** even if it doesn't change the API: + * `Integer::from_str_radix(s, radix)` requires that `s` is composed of optional leading single + sign (`+` or `-`) and followed by ASCII alphanum characters + * `num-bigint` backend used to allow underscore characters `_` in integer encoding, which now + lead to a parsing error + * `rug` backend used to allow whitspace characters (` `, `\t`, `\n`) in integer encoding, + which is no longer accepted as well. + * `num-bigint` backend used to panic when called `invert`, `pow_mod`, or `combine` with + `modulo=0`. Now it returns `None` in these cases (which is aligned with `rug` backend) + * `Integer::random_below`, `random_below_ref`, and `assign_random_below` return an error if upper + bound is `<= 0` (there are no number to sample the integer from). `rug` used to panicked in + those cases, while `num-bigint` ignored this condition, and simply treated the input as if it + was positive. + * `jacobi` returns an error when invalid `n` is provided. Both backend behaved differently: `rug` + used to return some integer, while `num-bigint` used to return `0` in release and panic in debug + build. + * `num_bigint` integer used to panic in `x << shift` (or `>>`) when the `shift` was negative. Now + it reverses the shift direction, like `rug` does: e.g. `x << -3` becomes `x >> 3` + * Safe prime generation returns an error if asked to generate a prime of bit length `< 100`, as + sampling small primes is not supported by the underlying algorithm. Note that cryptographic + primes need to be way larger. E.g. RSA requires primes to be at least 1536-bits. + * Prime generation returns an error if asked to generate a prime of bit length `< 2` (there are no + primes of that bitsize). Previously, function would panic or hang in infinite loop. + * `sample[_pm]_in_mult_group_of` returns an error if provided `n < 2`. Previously, `n=0` would + panic or hang (depending on the backend), and `n=1` would hang in infinite loop. + +[#49]: https://github.com/LFDT-Lockness/fast-paillier/pull/49 + ## v0.3.3 * Unify encoding of integer=0 across backends [#45] * Deserialize returns an error on invalid radix, instead of panicking [#45] diff --git a/Cargo.lock b/Cargo.lock index 1195f9d..d52fc44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,7 +288,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "fast-paillier" -version = "0.3.3" +version = "0.4.0" dependencies = [ "bytemuck", "ciborium", diff --git a/Cargo.toml b/Cargo.toml index 50823f7..4cdf004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fast-paillier" -version = "0.3.3" +version = "0.4.0" edition = "2021" license = "MIT OR Apache-2.0" description = "Optimized Paillier encryption scheme" From d7e2a220d8aded56595be792975917e42f362102 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 19 Oct 2026 14:41:13 +0200 Subject: [PATCH 13/14] Reduce `.clone()`s Signed-off-by: Denis Varlakov --- src/decryption_key.rs | 4 ++-- src/encryption_key.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/decryption_key.rs b/src/decryption_key.rs index e5bf9e5..bd27c22 100644 --- a/src/decryption_key.rs +++ b/src/decryption_key.rs @@ -107,9 +107,9 @@ impl DecryptionKey { } let x = if x.cmp0().is_ge() { - x.clone() + x } else { - x + self.n() + &(x + self.n()) }; // a = (1 + N)^x mod N^2 = (1 + xN) mod N^2 diff --git a/src/encryption_key.rs b/src/encryption_key.rs index 1118631..a8b2ba7 100644 --- a/src/encryption_key.rs +++ b/src/encryption_key.rs @@ -16,9 +16,9 @@ pub struct EncryptionKey { impl EncryptionKey { /// Constructs an encryption key from `N` pub fn from_n(n: Integer) -> Self { - let nn = n.clone() * &n; - let half_n = n.clone() >> 1u32; - let neg_half_n = -half_n.clone(); + let nn = &n * &n; + let half_n = &n >> 1u32; + let neg_half_n = -&half_n; Self { n, nn, @@ -64,13 +64,13 @@ impl EncryptionKey { } let x = if x.cmp0().is_ge() { - x.clone() + x } else { - x + self.n() + &(x + self.n()) }; // a = (1 + N)^x mod N^2 = (1 + xN) mod N^2 - let a = (Integer::one() + &x * self.n()) % self.nn(); + let a = (Integer::one() + x * self.n()) % self.nn(); // b = nonce^N mod N^2 let b = nonce .pow_mod_ref(self.n(), self.nn()) From dc314d94f907c9e66270cf9d2cc7fa0b8c48c7a9 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 19 Oct 2026 14:52:18 +0200 Subject: [PATCH 14/14] Reduce `.clone()`s Signed-off-by: Denis Varlakov --- src/decryption_key.rs | 6 +----- src/encryption_key.rs | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/decryption_key.rs b/src/decryption_key.rs index bd27c22..395e709 100644 --- a/src/decryption_key.rs +++ b/src/decryption_key.rs @@ -106,11 +106,7 @@ impl DecryptionKey { return Err(Reason::Encrypt.into()); } - let x = if x.cmp0().is_ge() { - x - } else { - &(x + self.n()) - }; + let x = if x.cmp0().is_ge() { x } else { &(x + self.n()) }; // a = (1 + N)^x mod N^2 = (1 + xN) mod N^2 let a = (Integer::one() + x * self.ek.n()) % self.ek.nn(); diff --git a/src/encryption_key.rs b/src/encryption_key.rs index a8b2ba7..e4973e4 100644 --- a/src/encryption_key.rs +++ b/src/encryption_key.rs @@ -63,11 +63,7 @@ impl EncryptionKey { return Err(Reason::Encrypt.into()); } - let x = if x.cmp0().is_ge() { - x - } else { - &(x + self.n()) - }; + let x = if x.cmp0().is_ge() { x } else { &(x + self.n()) }; // a = (1 + N)^x mod N^2 = (1 + xN) mod N^2 let a = (Integer::one() + x * self.n()) % self.nn();