From 08197da9dab10a45493773ce1c4fc88ba2e71266 Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Mon, 19 Oct 2026 16:23:52 +0200 Subject: [PATCH] expose bootstrapping command --- Cargo.toml | 2 +- src/fileutils.rs | 74 +++++++++++++++++++++++++++++++ src/main.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ee285c5..3c87bc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "homomorphic-encryption" +name = "dghv" version = "0.1.0" edition = "2021" diff --git a/src/fileutils.rs b/src/fileutils.rs index cec40da..3b1b497 100644 --- a/src/fileutils.rs +++ b/src/fileutils.rs @@ -1,3 +1,4 @@ +use crate::bootstrapping::BootstrapKey; use crate::dghv_asym::{PrivateKey, PublicKey}; use rug::Integer; use std::fs::{read_to_string, File}; @@ -63,3 +64,76 @@ pub fn write_hex_to_file>(path: P, hex_str: &str) -> io::Result<( writeln!(file, "{}", hex_str)?; Ok(()) } + +/// Bootstrap key file (`.bskey`): +/// line 1: `kappa theta n` (decimal) +/// next n lines: `u_i enc_s_i` (both hex) +pub fn write_bskey>(prefix: P, bk: &BootstrapKey) -> io::Result<()> { + let mut file = File::create(prefix.as_ref().with_extension("bskey"))?; + writeln!(file, "{} {} {}", bk.kappa, bk.theta, bk.u.len())?; + for (u, e) in bk.u.iter().zip(&bk.enc_s) { + writeln!(file, "{} {}", u.to_string_radix(16), e.to_string_radix(16))?; + } + Ok(()) +} + +pub fn read_bskey>(path: P) -> io::Result { + let bad = |msg: &str| io::Error::new(io::ErrorKind::InvalidData, msg.to_string()); + let content = read_to_string(path)?; + let mut lines = content.lines(); + + let header: Vec<&str> = lines.next().ok_or_else(|| bad("empty bootstrap key"))?.split_whitespace().collect(); + if header.len() != 3 { + return Err(bad("bad bootstrap key header (expected `kappa theta n`)")); + } + let num = |s: &str| s.parse::().map_err(|_| bad("bad number in bootstrap key header")); + let kappa = num(header[0])?; + let theta = num(header[1])? as usize; + let n = num(header[2])? as usize; + + let hex = |s: &str| Integer::from_str_radix(s, 16).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)); + let (mut u, mut enc_s) = (Vec::with_capacity(n), Vec::with_capacity(n)); + for line in lines.filter(|l| !l.trim().is_empty()) { + let mut it = line.split_whitespace(); + match (it.next(), it.next(), it.next()) { + (Some(a), Some(b), None) => { + u.push(hex(a)?); + enc_s.push(hex(b)?); + } + _ => return Err(bad("bad line in bootstrap key (expected `u enc_s`)")), + } + } + if u.len() != n || theta == 0 || theta > n || kappa < crate::bootstrapping::precision_bits(theta) { + return Err(bad("inconsistent bootstrap key parameters")); + } + Ok(BootstrapKey { u, enc_s, kappa, theta }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bootstrapping::generate_bootstrap_key; + use crate::dghv_asym::generate_keys; + + #[test] + fn test_bskey_roundtrip() { + let (sk, pk) = generate_keys(16, 512, 16, 10); + let bk = generate_bootstrap_key(&pk, &sk.p, pk.xs[0].significant_bits() + 8, 3, 12, 16); + let prefix = std::env::temp_dir().join(format!("dghv_test_{}", std::process::id())); + write_bskey(&prefix, &bk).unwrap(); + let path = prefix.with_extension("bskey"); + let back = read_bskey(&path).unwrap(); + std::fs::remove_file(&path).unwrap(); + assert_eq!((back.kappa, back.theta), (bk.kappa, bk.theta)); + assert_eq!(back.u, bk.u); + assert_eq!(back.enc_s, bk.enc_s); + } + + #[test] + fn test_bskey_rejects_garbage() { + let path = std::env::temp_dir().join(format!("dghv_bad_{}.bskey", std::process::id())); + std::fs::write(&path, "100 3 2\nff\n").unwrap(); + assert!(read_bskey(&path).is_err()); + std::fs::remove_file(&path).unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index 095b51f..5c1393c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,14 +3,19 @@ mod dghv; mod dghv_asym; mod fileutils; mod utils; +use crate::bootstrapping::{bootstrap, generate_bootstrap_key}; use crate::dghv::{decrypt_bit, encrypt_bit, generate_secret_key}; use crate::dghv_asym::{encrypt_bit_asym, generate_keys}; use crate::fileutils::{ - read_hex_from_file, read_privkey, read_pubkey, write_hex_to_file, write_privkey, write_pubkey, + read_bskey, read_hex_from_file, read_privkey, read_pubkey, write_bskey, write_hex_to_file, + write_privkey, write_pubkey, }; use clap::{ArgGroup, Args, Parser, Subcommand}; use rug::Integer; +const WEIGHT_DEFAULT: usize = 5; +const HINT_DEFAULT: usize = 20; + #[derive(Parser)] #[command(author, version, about, long_about = None)] #[command(propagate_version = true)] @@ -46,6 +51,55 @@ enum Commands { #[arg(short = 't', long, default_value_t = 10)] theta: usize, }, + /// Generate a bootstrap key (.bskey) from a key pair + BootstrapKeygen(BootstrapKeygenArgs), + /// Bootstrap (recrypt) a ciphertext: same bit, fresh noise + Bootstrap(BootstrapArgs), +} + +#[derive(Args)] +struct BootstrapKeygenArgs { + /// path to public key file + #[arg(long)] + pubkey: String, + /// path to private key file + #[arg(long)] + privkey: String, + /// output path prefix for the bootstrap key + #[arg(short = 'o', long)] + path: String, + /// precision kappa of the hint (default: bit length of x_0 + 8) + #[arg(long)] + kappa: Option, + /// Hamming weight of the secret subset + #[arg(short = 'w', long, default_value_t = WEIGHT_DEFAULT)] + weight: usize, + /// number of hint elements + #[arg(short = 'm', long, default_value_t = HINT_DEFAULT)] + hint_size: usize, + /// noise parameter (use the one of keygen-pair) + #[arg(short = 'n', long, default_value_t = 80)] + noise: u32, +} + +#[derive(Args)] +#[command(group(ArgGroup::new("cipher_source").required(true).args(&["ciphertext", "file"])))] +struct BootstrapArgs { + /// ciphertext hex string + #[arg(short = 'c', long)] + ciphertext: Option, + /// path to ciphertext file + #[arg(short = 'f', long)] + file: Option, + /// path to public key file + #[arg(long)] + pubkey: String, + /// path to bootstrap key file + #[arg(long)] + bskey: String, + /// optional output file for the new ciphertext (hex-encoded) + #[arg(short = 'o', long)] + output: Option, } #[derive(Args)] @@ -147,5 +201,61 @@ fn main() { write_pubkey(&path, &pk).expect("Failed to write pubkey"); println!("keys written to {}.[privkey|pubkey]", path); } + Commands::BootstrapKeygen(args) => { + let pk = read_pubkey(&args.pubkey).expect("Failed to load public key"); + let sk = read_privkey(&args.privkey).expect("Failed to load private key"); + let min_kappa = pk.xs[0].significant_bits() + 4; + let kappa = args.kappa.unwrap_or(min_kappa + 4); + if kappa < min_kappa { + eprintln!("invalid --kappa: need kappa >= {} (bit length of x_0 + 4)", min_kappa); + std::process::exit(1); + } + if args.weight == 0 || args.weight > args.hint_size { + eprintln!("invalid parameters: need 1 <= --weight <= --hint-size"); + std::process::exit(1); + } + let bk = generate_bootstrap_key( + &pk, + &sk.p, + kappa, + args.weight, + args.hint_size, + args.noise, + ); + + // self-test: the parameters must be able to bootstrap a product of two fresh ciphertexts + for m in 0..=1u8 { + let c = encrypt_bit_asym(m, &pk, args.noise) * encrypt_bit_asym(1, &pk, args.noise); + let refreshed = bootstrap(&c, &bk, &pk); + if decrypt_bit(&refreshed, &sk.p) != m { + eprintln!( + "self-test failed: the noise of the bootstrapping circuit exceeds the key capacity.\n\ + Use a larger --size (eta) for the key pair, or a smaller --weight." + ); + std::process::exit(1); + } + } + write_bskey(&args.path, &bk).expect("Failed to write bootstrap key"); + println!( + "bootstrap key written to {}.bskey (kappa={}, weight={}, hint size={}), self-test passed", + args.path, kappa, args.weight, args.hint_size + ); + } + Commands::Bootstrap(args) => { + let ct_hex = if let Some(hex_str) = args.ciphertext { + hex_str + } else { + read_hex_from_file(args.file.unwrap()).expect("Failed to read ciphertext file") + }; + let ct = Integer::from_str_radix(&ct_hex, 16).expect("Invalid ciphertext"); + let pk = read_pubkey(args.pubkey).expect("Failed to load public key"); + let bk = read_bskey(args.bskey).expect("Failed to load bootstrap key"); + let hexct = bootstrap(&ct, &bk, &pk).to_string_radix(16); + if let Some(outfile) = args.output { + write_hex_to_file(outfile, &hexct).expect("Failed to write ciphertext to file"); + } else { + println!("Bootstrapped bit (hex): {}", hexct); + } + } } }