From 057ca33b6b9723c1a65feaf806f4d5059425c623 Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Mon, 19 Oct 2026 12:11:08 +0200 Subject: [PATCH] generate prime number + comments --- src/bootstrapping.rs | 4 +- src/dghv.rs | 18 +++--- src/dghv_asym.rs | 45 +++++++++------ src/utils.rs | 132 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 169 insertions(+), 30 deletions(-) diff --git a/src/bootstrapping.rs b/src/bootstrapping.rs index 9632696..99d891e 100644 --- a/src/bootstrapping.rs +++ b/src/bootstrapping.rs @@ -256,8 +256,8 @@ mod tests { // c += c0.clone(); c *= c1.clone(); - let encrypted_z = encrypt_and_evaluate(&c, &bk, kappa*2); - c = bootstrap(&c, &encrypted_z, &bk.enc_s, &pk, kappa*2); + let encrypted_z = encrypt_and_evaluate(&c, &bk, kappa); + c = bootstrap(&c, &encrypted_z, &bk.enc_s, &pk, kappa); let m_boot = decrypt_bit(&c, &sk.p); assert_eq!( diff --git a/src/dghv.rs b/src/dghv.rs index c0e05d2..304e978 100644 --- a/src/dghv.rs +++ b/src/dghv.rs @@ -1,14 +1,14 @@ -use crate::utils::{generate_random_integer, generate_random_odd_integer}; +use crate::utils::{generate_random_odd_integer, generate_random_prime}; use rug::{Assign, Integer}; pub fn encrypt_bit(bit: u8, key: &Integer, gamma: u32, rho: u32) -> Integer { assert!(bit == 0 || bit == 1, "Only bits (0 or 1) are allowed"); - let q: Integer = generate_random_integer(gamma); + let q: Integer = generate_random_odd_integer(gamma); // noise let p_div_4: Integer = key.clone() / 4; - let r = std::iter::repeat_with(|| generate_random_integer(rho)) + let r = std::iter::repeat_with(|| generate_random_odd_integer(rho)) .find(|r| r.clone() * 2 < p_div_4) .unwrap(); @@ -24,8 +24,9 @@ pub fn decrypt_bit(encrypted: &Integer, key: &Integer) -> u8 { bit.to_u8().unwrap() } +/// Secret key: a random eta-bit *prime* (Fermat-tested, see `utils::generate_random_prime`). pub fn generate_secret_key(eta: u32) -> Integer { - generate_random_odd_integer(eta) + generate_random_prime(eta) } #[cfg(test)] @@ -36,14 +37,15 @@ mod tests { fn test_secret_key_generation_properties() { let eta = 1024; let key = generate_secret_key(eta); - assert!(key.significant_bits() <= eta); - assert!(key.significant_bits() >= eta - 1); + assert_eq!(key.significant_bits(), eta); assert!(key.is_odd()); + // independent primality cross-check (GMP Miller-Rabin) + assert_ne!(key.is_probably_prime(30), rug::integer::IsPrime::No); } #[test] fn test_encrypt_decrypt_bit() { - let eta = 10000; + let eta = 1024; let key = generate_secret_key(eta); let bit = 0; @@ -59,7 +61,7 @@ mod tests { #[test] fn test_multiple_encryptions_are_different() { - let eta = 10000; + let eta = 1024; let key = generate_secret_key(eta); let bit = 1; diff --git a/src/dghv_asym.rs b/src/dghv_asym.rs index 883f35a..4e15703 100644 --- a/src/dghv_asym.rs +++ b/src/dghv_asym.rs @@ -13,22 +13,31 @@ pub struct PrivateKey { pub p: Integer, } +/// Generates a key pair. +/// gamma: bit-length of the q_i, eta: bit-length of the (prime) secret p, +/// rho: noise bit-length, theta: total number of public elements (x_0 included, >= 2). +/// +/// x_0 = q_0 * p exactly (no noise) and is the largest element; the other x_i = q_i*p + 2*r_i. +/// Reducing a ciphertext modulo x_0 therefore never changes its noise, which is what makes products of ciphertexts (and the bootstrapping circuit) reducible. With a noisy x_0, reducing a product would add k*r_0 to the noise with k ~ x_0 and break decryption. pub fn generate_keys(gamma: u32, eta: u32, rho: u32, theta: usize) -> (PrivateKey, PublicKey) { + assert!(theta >= 2, "need at least x_0 and one more public element"); let p: Integer = generate_secret_key(eta); - let mut xs: Vec = Vec::with_capacity(theta); - for _ in 0..theta { - let xi = encrypt_bit(0u8, &p, gamma, rho); - xs.push(xi); - } - xs.sort_by(|a, b| b.cmp(a)); + // noisy elements x_1 .. x_{theta-1} + let mut xs: Vec = (1..theta) + .map(|_| encrypt_bit(0u8, &p, gamma, rho)) + .collect(); - while xs[0].is_even() { - xs.remove(0); - let xi = encrypt_bit(0u8, &p, gamma, rho); - xs.push(xi); - xs.sort_by(|a, b| b.cmp(a)); - } + // q_0 = (largest q_i) + 2 is odd and strictly above every other q_i, so that + // x_i < (q_i + 1) * p <= x_0 for all i. + let q_max = xs + .iter() + .map(|x| Integer::from(x / &p)) + .max() + .expect("theta >= 2"); + let q0 = q_max + 2u32; + xs.push(q0 * &p); + xs.sort_by(|a, b| b.cmp(a)); (PrivateKey { p }, PublicKey { xs }) } @@ -65,22 +74,26 @@ pub fn encrypt_bit_asym(m: u8, pk: &PublicKey, rho: u32) -> Integer { mod tests { use super::*; use crate::decrypt_bit; + use rug::integer::IsPrime; #[test] fn test_key_generation_properties() { - let eta: u32 = 10000; + let eta: u32 = 1024; let theta: usize = 5; let rho: u32 = 100; let (sk, pk) = generate_keys(rho, eta, rho, theta); assert_eq!(sk.p.significant_bits(), eta); assert!(sk.p.is_odd()); + assert_ne!(sk.p.is_probably_prime(30), IsPrime::No); assert_eq!(pk.xs.len(), theta); assert!(pk.xs[0].is_odd()); + // x_0 is an exact multiple of p + assert!(pk.xs[0].is_divisible(&sk.p)); for xi in pk.xs.iter().skip(1) { - assert!(pk.xs[0] >= *xi); + assert!(pk.xs[0] > *xi); } } @@ -88,11 +101,11 @@ mod tests { fn test_encrypt_decrypt_bit() { use rayon::prelude::*; - let eta: u32 = 10000; + let eta: u32 = 1024; let theta: usize = 50; let rho: u32 = 100; - (0..=127).into_par_iter().for_each(|_| { + (0..=31).into_par_iter().for_each(|_| { let (sk, pk) = generate_keys(rho, eta, rho, theta); for &m in &[0u8, 1u8] { let c = encrypt_bit_asym(m, &pk, rho); diff --git a/src/utils.rs b/src/utils.rs index df80f6a..6465918 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,15 @@ use rand::rngs::StdRng; use rand::Rng; use rand::SeedableRng; -use rug::rand::RandState; +use rug::integer::Order; use rug::Integer; +/// Number of random bases used by the Fermat test when generating primes. +pub const FERMAT_ROUNDS: u32 = 10; + +/// Trial-division bound used to discard most composites before the (expensive) Fermat test. +const SIEVE_BOUND: u32 = 5000; + pub fn generate_random_odd_integer(num_bits: u32) -> Integer { let mut rng = StdRng::from_os_rng(); let mut x = Integer::from(1); @@ -26,10 +32,128 @@ pub fn generate_random_integer(num_bits: u32) -> Integer { x } +/// Uniform random `finish - start` bit integer, shifted left by `start`. pub fn generate_random_integer_range(start: u32, finish: u32) -> Integer { assert!(finish > start, "finish must be greater than start"); let k = finish - start; - let mut rand = RandState::new(); - let i = Integer::from(Integer::random_bits(k, &mut rand)) << start; - i + let mut rng = StdRng::from_os_rng(); + let mut buf = vec![0u8; k.div_ceil(8) as usize]; + rng.fill(&mut buf[..]); + let x = Integer::from_digits(&buf, Order::Lsf).keep_bits(k); + x << start +} + +/// Primes below `SIEVE_BOUND`. +fn small_primes() -> Vec { + let mut is_composite = vec![false; SIEVE_BOUND as usize + 1]; + let mut primes = Vec::new(); + for i in 2..=SIEVE_BOUND as usize { + if !is_composite[i] { + primes.push(i as u32); + let mut j = i * i; + while j <= SIEVE_BOUND as usize { + is_composite[j] = true; + j += i; + } + } + } + primes +} + +/// Fermat primality test with `rounds` random bases. +/// +/// `n` is declared (probably) prime iff `a^(n-1) = 1 (mod n)` for every random base `a` in `[2, n-2]`. Small numbers are settled exactly by trial division first. +/// (Carmichael numbers fool every coprime base, but they are rare among random large candidates.) +pub fn is_probable_prime_fermat(n: &Integer, rounds: u32) -> bool { + if *n < 2 { + return false; + } + for p in small_primes() { + if *n == p { + return true; + } + if n.is_divisible_u(p) { + return false; + } + } + + // n > SIEVE_BOUND and has no small factor + let n_minus_1 = Integer::from(n - 1u32); + let range = Integer::from(n - 3u32); // bases in [2, n-2] + for _ in 0..rounds.max(1) { + let a = generate_random_integer_range(0, n.significant_bits() + 1) % &range + 2u32; + match a.pow_mod(&n_minus_1, n) { + Ok(r) if r == 1 => {} + _ => return false, + } + } + true +} + +/// Random `num_bits`-bit prime (top bit set), found by rejection sampling + Fermat test. +pub fn generate_random_prime(num_bits: u32) -> Integer { + assert!(num_bits >= 2, "a prime needs at least 2 bits"); + loop { + let candidate = generate_random_odd_integer(num_bits); + if is_probable_prime_fermat(&candidate, FERMAT_ROUNDS) { + return candidate; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rug::integer::IsPrime; + + #[test] + fn test_range_generator_is_random() { + let a = generate_random_integer_range(0, 128); + let b = generate_random_integer_range(0, 128); + assert_ne!(a, b); + for _ in 0..50 { + let x = generate_random_integer_range(3, 40); + assert!(x < (Integer::from(1) << 40u32)); + assert!(x.is_divisible_2pow(3)); + } + } + + #[test] + fn test_fermat_small_numbers() { + let primes = [2u32, 3, 5, 7, 11, 4999, 5003, 7919, 104729]; + for p in primes { + assert!(is_probable_prime_fermat(&Integer::from(p), 5), "{p}"); + } + let composites = [0u32, 1, 4, 9, 561, 1105, 1729, 4999 * 4999, 5003 * 5009]; + for c in composites { + assert!(!is_probable_prime_fermat(&Integer::from(c), 5), "{c}"); + } + } + + #[test] + fn test_fermat_large_numbers() { + // Mersenne prime 2^127 - 1 and 2^521 - 1 + for e in [127u32, 521] { + let m = (Integer::from(1) << e) - 1u32; + assert!(is_probable_prime_fermat(&m, 10)); + } + // product of two large primes + let p = generate_random_prime(128); + let q = generate_random_prime(128); + assert!(!is_probable_prime_fermat(&(p * q), 10)); + // 2^127 + 1 is divisible by 3 + let c = (Integer::from(1) << 127u32) + 1u32; + assert!(!is_probable_prime_fermat(&c, 10)); + } + + #[test] + fn test_generate_random_prime() { + for bits in [16u32, 64, 256, 512] { + let p = generate_random_prime(bits); + assert_eq!(p.significant_bits(), bits); + assert!(p.is_odd()); + // independent cross-check with GMP's Miller-Rabin + assert_ne!(p.is_probably_prime(30), IsPrime::No); + } + } }