bootstrapping

This commit is contained in:
sam.hadow committed 2026-10-19 12:18:22 +02:00
1 parent c0d7b57ed7
commit 88d75eee5d
1 file changed
+248 -189
+248 -189
View File
@@ -1,23 +1,48 @@
//! DGHV bootstrapping by squashed decryption.
//!
//! For a ciphertext c = q*p + e (0 <= e < p/4, p odd prime) the plaintext is
//! m = (c mod 2) XOR (round(c/p) mod 2).
//! The secret 1/p is hidden as a sparse subset sum of public reals y_i in [0,2):
//! sum_{i in S} y_i = 1/p (mod 2), |S| = theta,
//! so that round(c/p) mod 2 = round(sum_{i in S} z_i) mod 2 with z_i = [c*y_i]_2 kept to n fractional bits. The z_i are public once c is known; only the selector bits s_i are secret, and they are given as ciphertexts. The sum is evaluated homomorphically with a carry-save column circuit built from elementary symmetric polynomials (the Hamming weight of a column is read off the bits of e_1, e_2, e_4, ... mod 2).
use crate::dghv_asym::{encrypt_bit_asym, PublicKey}; use crate::dghv_asym::{encrypt_bit_asym, PublicKey};
use crate::utils::generate_random_integer_range; use crate::utils::generate_random_integer_range;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rand::rngs::StdRng; use rand::rngs::StdRng;
use rand::SeedableRng; use rand::SeedableRng;
use rug::{Integer, Rational}; use rug::ops::RemRounding;
use rug::Integer;
/// Bootstrap key, hint vector and encrypted bits of the private key /// Bootstrap key: hint vector and encrypted bits of the secret subset.
pub struct BootstrapKey { pub struct BootstrapKey {
pub y: Vec<Rational>, /// u_i = y_i * 2^kappa, 0 <= u_i < 2^(kappa+1) (so y_i = u_i / 2^kappa is in [0, 2)).
pub u: Vec<Integer>,
/// Encryptions of the selector bits s_i (Hamming weight theta).
pub enc_s: Vec<Integer>, pub enc_s: Vec<Integer>,
/// Precision of the y_i.
pub kappa: u32,
/// Hamming weight of the selector vector.
pub theta: usize,
}
/// Number of bits needed to write any integer in 0..=max_weight.
fn weight_bits(max_weight: usize) -> u32 {
usize::BITS - max_weight.leading_zeros()
}
/// Number of fractional bits n = ceil(log2(theta + 1)) + 3 kept for each z_i.
pub fn precision_bits(theta: usize) -> u32 {
weight_bits(theta) + 3
} }
/// Generates a bootstrap key /// Generates a bootstrap key
/// pk: public key /// pk: public key
/// sk: secret integer p /// sk: secret prime p
/// kappa: precision parameter (bits) for y_i /// kappa: precision parameter (bits) for y_i; must be >= bitlen(x_0) + 4
/// theta: Hamming weight of the secret support /// theta: Hamming weight of the secret support
/// n_theta: total number of hint elements /// n_theta: total number of hint elements
/// rho: noise parameter (encryption) /// rho: noise parameter (encryption of the selector bits)
pub fn generate_bootstrap_key( pub fn generate_bootstrap_key(
pk: &PublicKey, pk: &PublicKey,
sk: &Integer, sk: &Integer,
@@ -26,246 +51,280 @@ pub fn generate_bootstrap_key(
n_theta: usize, n_theta: usize,
rho: u32, rho: u32,
) -> BootstrapKey { ) -> BootstrapKey {
assert!(
theta >= 1 && theta <= n_theta,
"need 1 <= theta <= n_theta"
);
// The error c*|1/p - xp/2^kappa| < c*2^-kappa must stay below 1/16 for every c < x_0.
assert!(
kappa >= pk.xs[0].significant_bits() + 4,
"kappa must be at least bitlen(x_0) + 4 = {}",
pk.xs[0].significant_bits() + 4
);
// xp = ⌊2^κ / p⌋ // xp = ⌊2^κ / p⌋
let two_k = Integer::from(1) << kappa; let two_k = Integer::from(1) << kappa;
let xp: Integer = &two_k / sk.clone(); let xp = Integer::from(&two_k / sk);
let two_k1 = Integer::from(1) << (kappa + 1);
// support vector s, Hamming weight theta // support S, |S| = theta
let mut rng = StdRng::from_os_rng(); let mut rng = StdRng::from_os_rng();
let mut indices: Vec<usize> = (0..n_theta).collect(); let mut indices: Vec<usize> = (0..n_theta).collect();
indices.shuffle(&mut rng); indices.shuffle(&mut rng);
let s: Vec<usize> = indices.iter().cloned().take(theta).collect(); let s: Vec<usize> = indices[..theta].to_vec();
let mut in_s = vec![false; n_theta];
let mut s_bits = vec![0u8; n_theta];
for &i in &s { for &i in &s {
s_bits[i] = 1; in_s[i] = true;
} }
// u[i] = xp (mod 2^(k+1)) // u_i random in [0, 2^(κ+1)); the last element of S is then fixed so that
let mut u = vec![Integer::from(0); n_theta]; // sum_{i in S} u_i = xp (mod 2^(κ+1)).
let two_k1 = Integer::from(1) << (kappa + 1); let mut u: Vec<Integer> = (0..n_theta)
for i in 0..n_theta { .map(|_| generate_random_integer_range(0, kappa + 1))
if !s.contains(&i) { .collect();
u[i] = generate_random_integer_range(0, kappa + 1); let mut partial = Integer::new();
for &i in &s[..theta - 1] {
partial += &u[i];
}
u[s[theta - 1]] = Integer::from(&xp - &partial).rem_euc(&two_k1);
let enc_s: Vec<Integer> = in_s
.iter()
.map(|&b| encrypt_bit_asym(b as u8, pk, rho))
.collect();
BootstrapKey {
u,
enc_s,
kappa,
theta,
} }
} }
if theta > 0 {
for &i in s.iter().take(theta - 1) { /// e_0..e_max_deg of `items`, computed over the integers (reduced mod `modulus`, which is an exact multiple of p, so noise is unaffected). Addition/multiplication of ciphertexts act as XOR/AND on the plaintext bits, so e_j mod 2 is the usual elementary symmetric polynomial over GF(2).
u[i] = generate_random_integer_range(0, kappa + 1); fn elementary_symmetric(items: &[Integer], max_deg: usize, modulus: &Integer) -> Vec<Integer> {
let mut e = vec![Integer::new(); max_deg + 1];
e[0] = Integer::from(1);
for (idx, x) in items.iter().enumerate() {
for j in (1..=max_deg.min(idx + 1)).rev() {
if e[j - 1] == 0 {
continue;
}
let mut t = Integer::from(x * &e[j - 1]);
t += &e[j];
e[j] = t.rem_euc(modulus);
}
}
e
} }
let mut sum_s_minus_last = Integer::from(0); /// Recrypt: returns a fresh ciphertext of the same bit as `c`, whose noise no longer depends on the noise of `c` (it only has to satisfy 0 <= e < p/4 on input).
for &i in s.iter().take(theta - 1) { /// `c` may be any non-negative integer ciphertext; it is first reduced mod x_0.
sum_s_minus_last += &u[i]; pub fn bootstrap(c: &Integer, bk: &BootstrapKey, pk: &PublicKey) -> Integer {
}
let s_last = s[theta - 1];
let diff = xp.clone() - &sum_s_minus_last;
let mut remainder = diff % &two_k1;
if remainder < 0 {
remainder += &two_k1;
}
u[s_last] = remainder;
}
// y[i] = u[i] / 2^κ
let y: Vec<Rational> = u.iter().map(|ui| Rational::from(ui) / Rational::from(&two_k)).collect();
let enc_s: Vec<Integer> = s_bits.iter().map(|&bit| encrypt_bit_asym(bit, &pk, rho)).collect();
BootstrapKey { y, enc_s }
}
pub fn encrypt_and_evaluate(
c: &Integer,
bootstrap_key: &BootstrapKey,
precision_bits: u32,
) -> Vec<Rational> {
let mut result = Vec::with_capacity(bootstrap_key.y.len());
let scale = Integer::from(1) << precision_bits;
let scale_rat = Rational::from(&scale);
for y_i in &bootstrap_key.y {
let prod = Rational::from(c) * y_i;
let scaled = prod.clone() * &scale_rat;
let floored = scaled.clone().floor();
let frac = &scaled - floored;
result.push(frac);
}
result
}
pub fn bootstrap(
c: &Integer,
z: &[Rational],
encrypted_sk_bits: &[Integer],
pk: &PublicKey,
precision_bits: u32,
) -> Integer {
let int_scale = Integer::from(1) << precision_bits;
let scale_rat = Rational::from(&int_scale);
let half_rat = Rational::from((1, 2));
let x0 = &pk.xs[0]; let x0 = &pk.xs[0];
let n = precision_bits(bk.theta);
let n_cols = n as usize + 1; // bit positions 0..=n of the (n+1)-bit fixed-point z_i
let c = c.clone().rem_euc(x0);
let mut acc = Integer::from(0); // Z_i = z_i * 2^n with z_i = [c*u_i / 2^κ]_2 truncated to n fractional bits.
for (z_i, enc_s_i) in z.iter().zip(encrypted_sk_bits.iter()) { let shift = bk.kappa - n;
let rounded: Integer = (z_i.clone() * &scale_rat + &half_rat).floor_ref().into(); let zs: Vec<Integer> = bk
.u
.iter()
.map(|u| Integer::from(&c * u).keep_bits(bk.kappa + 1) >> shift)
.collect();
acc += &rounded * enc_s_i; // columns[t]: ciphertext bits of weight 2^t. Z_i contributes s_i at every set bit t.
// (Multiplying Enc(s_i) by the public bit Z_{i,t} just selects or drops it.)
let mut columns: Vec<Vec<Integer>> = vec![Vec::new(); n_cols];
for (z, enc) in zs.iter().zip(&bk.enc_s) {
for (t, col) in columns.iter_mut().enumerate() {
if z.get_bit(t as u32) {
col.push(enc.clone());
}
}
} }
acc %= x0; // Column compression, LSB first. For column t holding h ones, bit k of h is e_{2^k} mod 2 (Lucas); bit 0 is bit t of T = sum_{i in S} Z_i, bit k>0 is a carry into column t+k.
let mut carries = vec![0usize; n_cols];
let inv_scale = int_scale let mut t_bits: Vec<Integer> = Vec::with_capacity(n_cols);
.invert(x0) for t in 0..n_cols {
.expect("2^precision_bits and x0 must be coprime"); let items = std::mem::take(&mut columns[t]);
let adjusted_sum = (acc * inv_scale) % x0; // at most theta original ones plus every carry item can be one
let reachable = (bk.theta + carries[t]).min(items.len());
let mut diff = c.clone(); if reachable == 0 {
diff -= &adjusted_sum; t_bits.push(Integer::new());
diff.modulo(x0) continue;
}
let k_top = (weight_bits(reachable) - 1).min((n_cols - 1 - t) as u32);
let e = elementary_symmetric(&items, 1usize << k_top, x0);
t_bits.push(e[1].clone());
for k in 1..=k_top as usize {
columns[t + k].push(e[1 << k].clone());
carries[t + k] += 1;
}
} }
// round(sum z_i) mod 2 = bit_n(T) XOR bit_{n-1}(T); m = (c mod 2) XOR that.
let mut out = Integer::from(&t_bits[n_cols - 1] + &t_bits[n_cols - 2]);
if c.is_odd() {
out += 1u32;
}
out.rem_euc(x0)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::dghv_asym::generate_keys;
use crate::dghv::decrypt_bit; use crate::dghv::decrypt_bit;
use crate::dghv_asym::{generate_keys, PrivateKey};
// Toy parameters: tiny hint (theta = 3 of 12), 1024-bit prime.
const ETA: u32 = 1024;
const RHO: u32 = 8;
const GAMMA: u32 = 64;
const PK_SIZE: usize = 20;
fn setup(
eta: u32,
theta: usize,
n_theta: usize,
) -> (PrivateKey, PublicKey, BootstrapKey) {
let (sk, pk) = generate_keys(GAMMA, eta, RHO, PK_SIZE);
let kappa = pk.xs[0].significant_bits() + 8;
let bk = generate_bootstrap_key(&pk, &sk.p, kappa, theta, n_theta, RHO);
(sk, pk, bk)
}
/// bit length of the noise e = c mod p
fn noise_bits(c: &Integer, p: &Integer) -> u32 {
Integer::from(c % p).significant_bits()
}
#[test] #[test]
fn test_bootstrap_key_generation() { fn test_bootstrap_key_generation() {
let eta = 1024;
let rho = 10;
let theta = 10; let theta = 10;
let n_theta = 50; let n_theta = 50;
let kappa = 100; let (sk, pk, bk) = setup(ETA, theta, n_theta);
let kappa = bk.kappa;
let (sk, pk) = generate_keys(rho, eta, rho, theta); let s_bits: Vec<u8> = bk
let bootstrap_key = generate_bootstrap_key(&pk, &sk.p, kappa, theta, n_theta, rho); .enc_s
.iter()
let y = bootstrap_key.y; .map(|c| {
let enc_s = bootstrap_key.enc_s;
let s_bits: Vec<u8> = enc_s.iter().map(|c| {
let m = decrypt_bit(c, &sk.p); let m = decrypt_bit(c, &sk.p);
assert!(m == 0 || m == 1, "Decrypted bit must be 0 or 1"); assert!(m == 0 || m == 1);
m m
}).collect(); })
let hamming_weight = s_bits.iter().filter(|&&b| b == 1).count(); .collect();
assert_eq!(hamming_weight, theta, "Hamming weight of s should be {}", theta); assert_eq!(s_bits.len(), n_theta);
assert_eq!(s_bits.len(), n_theta, "s_bits length should be {}", n_theta); assert_eq!(s_bits.iter().filter(|&&b| b == 1).count(), theta);
assert_eq!(bk.u.len(), n_theta);
assert!(kappa >= pk.xs[0].significant_bits() + 4);
let two_k = Integer::from(1) << kappa; let two_k = Integer::from(1) << kappa;
let two_k1 = Integer::from(1) << (kappa + 1); let two_k1 = Integer::from(1) << (kappa + 1);
let xp = two_k.clone() / &sk.p; let xp = Integer::from(&two_k / &sk.p);
let u: Vec<Integer> = y.iter().map(|yi| Integer::from((yi.clone() * &two_k).floor_ref())).collect(); assert!(xp > 0, "xp must be non-trivial (kappa > eta)");
let mut sum_u_s = Integer::from(0); // sum of u_i over S == xp mod 2^(κ+1)
for (i, &bit) in s_bits.iter().enumerate() { let mut sum_u_s = Integer::new();
if bit == 1 { for (u, &b) in bk.u.iter().zip(&s_bits) {
sum_u_s += &u[i]; if b == 1 {
sum_u_s += u;
} }
} }
assert!(Integer::from(&sum_u_s - &xp).is_divisible(&two_k1));
let diff = (sum_u_s.clone() - &xp) % &two_k1; for u in &bk.u {
assert_eq!(diff, 0, "Sum of u_i over S should equal xp mod 2^(κ+1)"); assert!(*u >= 0 && *u < two_k1, "0 <= u_i < 2^(κ+1)");
for ui in &u {
assert!(ui >= &0, "u_i should be non-negative");
assert!(ui < &two_k1, "u_i should be less than 2^(κ+1)");
} }
for (i, yi) in y.iter().enumerate() { // regression: the hint vector used to be one repeated value (unseeded GMP RNG)
let expected_yi = Rational::from(&u[i]) / Rational::from(&two_k); let mut sorted: Vec<&Integer> = bk.u.iter().collect();
assert_eq!(yi, &expected_yi, "y[{}] should equal u[{}] / 2^κ", i, i); sorted.sort();
assert!(yi < &Rational::from(2), "y[{}] should be less than 2", i); sorted.dedup();
} assert_eq!(sorted.len(), n_theta, "u_i must be distinct");
assert_eq!(enc_s.len(), n_theta, "enc_s length should be {}", n_theta); // |xp/2^κ - 1/p| < 2^-κ <=> 0 <= 2^κ - xp*p < p
let rem = Integer::from(&two_k - Integer::from(&xp * &sk.p));
let mut sum_y_s = Rational::from(0); assert!(rem >= 0 && rem < sk.p);
for (i, &bit) in s_bits.iter().enumerate() {
if bit == 1 {
sum_y_s += &y[i];
}
}
let xp_rational = Rational::from((&xp, two_k.clone()));
let one_over_p = Rational::from(1) / Rational::from(&sk.p);
let delta_p = xp_rational - one_over_p;
let bound = Rational::from(1) / Rational::from(two_k);
assert!(
delta_p.clone().abs() < bound,
"Error |Δp| = {} should be < 2^(-κ) = {}",
delta_p,
bound
);
} }
#[test] #[test]
fn test_bootstrapping_preserve_m() { fn test_bootstrapping_preserve_m() {
let eta = 1000; // secret key bit size let (sk, pk, bk) = setup(ETA, 3, 12);
let rho = 30; // initial noise for m in 0..=1u8 {
let theta = 1000; // Hamming weight of hint support let mut c = encrypt_bit_asym(m, &pk, RHO);
let n_theta = 1000; // total hint size for _ in 0..50 {
let kappa = 1000; // precision for y_i c = bootstrap(&c, &bk, &pk);
assert_eq!(decrypt_bit(&c, &sk.p), m, "bootstrap changed the plaintext");
let (sk, pk) = generate_keys(rho, eta, rho, theta);
let bk = generate_bootstrap_key(&pk, &sk.p, kappa, theta, n_theta, rho);
for m in 0..=1 {
let mut c = encrypt_bit_asym(m, &pk, rho);
for _ in 0..=100 {
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!(
m, m_boot,
"Bootstrap failed: expected {}, got {}",
m, m_boot
);
} }
} }
} }
#[test] #[test]
fn test_bootstrapping() { fn test_bootstrapping() {
let eta = 1000; // secret key bit size // multiply by Enc(1) then bootstrap, repeatedly: noise would blow up without recryption
let rho = 30; // initial noise let (sk, pk, bk) = setup(ETA, 3, 12);
let theta = 1000; // Hamming weight of hint support let c1 = encrypt_bit_asym(1u8, &pk, RHO);
let n_theta = 1000; // total hint size for m in 0..=1u8 {
let kappa = 1000; // precision for y_i let mut c = encrypt_bit_asym(m, &pk, RHO);
for _ in 0..50 {
c *= &c1;
c = bootstrap(&c, &bk, &pk);
assert_eq!(decrypt_bit(&c, &sk.p), m);
}
println!("m={m}: noise after bootstrap = {} bits", noise_bits(&c, &sk.p));
}
}
let (sk, pk) = generate_keys(rho, eta, rho, theta); #[test]
let bk = generate_bootstrap_key(&pk, &sk.p, kappa, theta, n_theta, rho); fn test_bootstrapping_and_xor_circuit() {
// evaluate (a AND b) XOR a on encrypted bits, bootstrapping after each gate
let (sk, pk, bk) = setup(ETA, 3, 12);
for &(a, b) in &[(0u8, 0u8), (0, 1), (1, 0), (1, 1)] {
let ca = encrypt_bit_asym(a, &pk, RHO);
let cb = encrypt_bit_asym(b, &pk, RHO);
let and = bootstrap(&Integer::from(&ca * &cb), &bk, &pk);
let xor = bootstrap(&Integer::from(&and + &ca), &bk, &pk);
assert_eq!(decrypt_bit(&and, &sk.p), a & b);
assert_eq!(decrypt_bit(&xor, &sk.p), (a & b) ^ a);
}
}
// let c0 = encrypt_bit_asym(0u8, &pk, rho); #[test]
let c1 = encrypt_bit_asym(1u8, &pk, rho); fn test_bootstrapping_reduces_noise() {
let (sk, pk, bk) = setup(ETA, 3, 12);
let c1 = encrypt_bit_asym(1u8, &pk, RHO);
for m in 0..=1u8 {
// square-and-multiply a few times to build up noise (about 8x the fresh noise)
let mut c = encrypt_bit_asym(m, &pk, RHO);
for _ in 0..40 {
c *= &c1;
}
assert_eq!(decrypt_bit(&c, &sk.p), m);
let before = noise_bits(&c, &sk.p);
let fresh = bootstrap(&c, &bk, &pk);
let after = noise_bits(&fresh, &sk.p);
println!("noise before = {before} bits, after = {after} bits (p has {ETA} bits)");
assert!(after < before, "bootstrapping must reduce the noise");
assert_eq!(decrypt_bit(&fresh, &sk.p), m);
}
}
for m in 0..=1 { #[test]
let mut c = encrypt_bit_asym(m, &pk, rho); fn test_bootstrapping_theta_15() {
for _ in 0..=100 { let (sk, pk, bk) = setup(3000, 15, 60);
// c += c0.clone(); let c1 = encrypt_bit_asym(1u8, &pk, RHO);
c *= c1.clone(); for m in 0..=1u8 {
let mut c = encrypt_bit_asym(m, &pk, RHO);
let encrypted_z = encrypt_and_evaluate(&c, &bk, kappa); for _ in 0..5 {
c = bootstrap(&c, &encrypted_z, &bk.enc_s, &pk, kappa); c *= &c1;
c = bootstrap(&c, &bk, &pk);
let m_boot = decrypt_bit(&c, &sk.p); assert_eq!(decrypt_bit(&c, &sk.p), m);
assert_eq!(
m, m_boot,
"Bootstrap failed: expected {}, got {}",
m, m_boot
);
} }
println!("theta=15 noise: {} bits", noise_bits(&c, &sk.p));
} }
} }
} }