Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/fhe/benches/trbfv_bfv_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use criterion::{Criterion, criterion_group, criterion_main};
use fhe::bfv::{BfvParametersBuilder, Encoding, Plaintext, PublicKey, SecretKey};
use fhe::mbfv::{CommonRandomPoly, PublicKeyShare};
use fhe::trbfv::{ShareManager, TRBFV};
use fhe::trbfv::{Lambda, ShareManager, TRBFV};
use fhe_traits::{FheDecoder, FheDecrypter, FheEncoder, FheEncrypter};
use rand::rng as make_rng;
use std::sync::Arc;
Expand Down Expand Up @@ -107,7 +107,9 @@ fn bench_data_sizes(c: &mut Criterion) {
.unwrap();

// Generate smudging error shares
let esi_coeffs = trbfv.generate_smudging_error(100, 80, &mut rng).unwrap();
let esi_coeffs = trbfv
.generate_smudging_error(100, Lambda::secure(80).unwrap(), &mut rng)
.unwrap();
let esi_poly = share_manager.bigints_to_poly(&esi_coeffs).unwrap();
let esi_sss = share_manager
.generate_secret_shares_from_poly(esi_poly, &mut rng)
Expand Down
8 changes: 6 additions & 2 deletions crates/fhe/examples/trbfv_add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use console::style;
use fhe::{
bfv::{self, Ciphertext, Encoding, Plaintext, PublicKey, SecretKey},
mbfv::{AggregateIter, CommonRandomPoly, PublicKeyShare},
trbfv::{ShareManager, TRBFV},
trbfv::{Lambda, ShareManager, TRBFV},
};

use fhe_math::rq::{Poly, PowerBasis};
Expand Down Expand Up @@ -135,6 +135,10 @@ fn main() -> Result<(), Box<dyn Error>> {

// The parameters are within bound, let's go! Let's first display some
// information about the threshold sum.
// Secure example: rejects lambda below the secure minimum. See
// trbfv_add_bfv_share_insecure.rs for the explicit insecure test mode.
let security = Lambda::secure(lambda)?;

println!("# Addition with trBFV");
println!("\tnum_summed = {num_summed}");
println!("\tnum_parties = {num_parties}");
Expand Down Expand Up @@ -193,7 +197,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let d_share_poly = Poly::<PowerBasis>::zero(ctx);

let esi_coeffs = temp_trbfv
.generate_smudging_error(num_summed, lambda, &mut rng)
.generate_smudging_error(num_summed, security, &mut rng)
.unwrap();
let esi_poly = share_manager.bigints_to_poly(&esi_coeffs).unwrap();
let esi_sss = share_manager
Expand Down
8 changes: 6 additions & 2 deletions crates/fhe/examples/trbfv_add_bfv_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use console::style;
use fhe::{
bfv::{self, Ciphertext, Encoding, Plaintext, PublicKey, SecretKey},
mbfv::{AggregateIter, CommonRandomPoly, PublicKeyShare},
trbfv::{ShareManager, TRBFV},
trbfv::{Lambda, ShareManager, TRBFV},
};

use fhe_math::rq::{Poly, PowerBasis};
Expand Down Expand Up @@ -151,6 +151,10 @@ fn main() -> Result<(), Box<dyn Error>> {
))
}

// Secure example: rejects lambda below the secure minimum. See
// trbfv_add_bfv_share_insecure.rs for the explicit insecure test mode.
let security = Lambda::secure(lambda)?;

println!("# Addition with trBFV (with encrypted share transmission)");
println!("\tnum_summed = {num_summed}");
println!("\tnum_parties = {num_parties}");
Expand Down Expand Up @@ -204,7 +208,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let d_share_poly = Poly::<PowerBasis>::zero(ctx);

let esi_coeffs = temp_trbfv
.generate_smudging_error(num_summed, lambda, &mut rng)
.generate_smudging_error(num_summed, security, &mut rng)
.unwrap();
let esi_poly = share_manager.bigints_to_poly(&esi_coeffs).unwrap();
let esi_sss = share_manager
Expand Down
8 changes: 6 additions & 2 deletions crates/fhe/examples/trbfv_add_bfv_share_insecure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use console::style;
use fhe::{
bfv::{self, Ciphertext, Encoding, Plaintext, PublicKey, SecretKey},
mbfv::{AggregateIter, CommonRandomPoly, PublicKeyShare},
trbfv::{ShareManager, TRBFV},
trbfv::{Lambda, ShareManager, TRBFV},
};

use fhe_math::rq::{Poly, PowerBasis};
Expand Down Expand Up @@ -149,6 +149,10 @@ fn main() -> Result<(), Box<dyn Error>> {
))
}

// Insecure test mode: small lambda for speed; the smudging noise does NOT
// hide the decryption noise. Never use this in production.
let security = Lambda::insecure(lambda);

println!("# Addition with trBFV (with encrypted share transmission)");
println!("\tnum_summed = {num_summed}");
println!("\tnum_parties = {num_parties}");
Expand Down Expand Up @@ -202,7 +206,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let d_share_poly = Poly::<PowerBasis>::zero(ctx);

let esi_coeffs = temp_trbfv
.generate_smudging_error(num_summed, lambda, &mut rng)
.generate_smudging_error(num_summed, security, &mut rng)
.unwrap();
let esi_poly = share_manager.bigints_to_poly(&esi_coeffs).unwrap();
let esi_sss = share_manager
Expand Down
4 changes: 3 additions & 1 deletion crates/fhe/src/trbfv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ pub mod threshold;
// Re-export main types for convenience
pub use shamir::ShamirSecretSharing;
pub use shares::ShareManager;
pub use smudging::{SmudgingBoundCalculator, SmudgingBoundCalculatorConfig};
pub use smudging::{
Lambda, MIN_SECURE_LAMBDA, SmudgingBoundCalculator, SmudgingBoundCalculatorConfig,
};
pub use threshold::TRBFV;
111 changes: 82 additions & 29 deletions crates/fhe/src/trbfv/smudging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,63 @@ use num_bigint::{BigInt, BigUint};
use rand::{CryptoRng, RngCore};
use std::sync::Arc;

/// Minimum statistical security parameter accepted for production use.
pub const MIN_SECURE_LAMBDA: usize = 50;

/// Statistical security level for smudging noise generation.
///
/// The smudging bound is always computed as `B_sm = 2^lambda * B_C`; this type
/// only controls which values of lambda the library accepts. Production code
/// must use [`Lambda::secure`], which rejects lambda below
/// [`MIN_SECURE_LAMBDA`]. Test setups that deliberately trade security for
/// speed must opt in explicitly via [`Lambda::insecure`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Lambda {
/// Statistical security parameter validated to be >= [`MIN_SECURE_LAMBDA`].
/// Construct via [`Lambda::secure`].
Secure(usize),
/// NOT SECURE: lambda below [`MIN_SECURE_LAMBDA`], so the smudging noise
/// does not statistically hide the decryption noise. For testing only.
/// Construct via [`Lambda::insecure`].
Insecure(usize),
}

impl Lambda {
/// Create a secure level. Fails if `lambda < MIN_SECURE_LAMBDA`.
pub fn secure(lambda: usize) -> Result<Self, Error> {
if lambda < MIN_SECURE_LAMBDA {
return Err(Error::UnspecifiedInput(format!(
"lambda {lambda} is below the secure minimum {MIN_SECURE_LAMBDA}; \
for testing, opt in explicitly with Lambda::insecure"
)));
}
Ok(Self::Secure(lambda))
}

/// Create an explicitly insecure level for fast testing (lambda clamped to >= 2).
///
/// The smudging noise generated under this level does NOT hide the
/// decryption noise. Never use in production.
#[must_use]
pub fn insecure(lambda: usize) -> Self {
Self::Insecure(lambda.max(2))
}

/// The statistical security parameter lambda.
#[must_use]
pub fn value(&self) -> usize {
match self {
Self::Secure(lambda) | Self::Insecure(lambda) => *lambda,
}
}

/// Whether this level meets the secure minimum.
#[must_use]
pub fn is_secure(&self) -> bool {
matches!(self, Self::Secure(_))
}
}

/// Configuration for calculating optimal smudging variance in threshold BFV.
///
/// All parameters use arbitrary precision arithmetic to handle cryptographically large values.
Expand All @@ -35,8 +92,8 @@ pub struct SmudgingBoundCalculatorConfig {
pub public_key_error: u64,
/// Secret key poly for infinity norm calculation
pub secret_key_bound: u64,
/// Statistical Security parameter
pub lambda: usize,
/// Statistical security level
pub lambda: Lambda,
}

impl SmudgingBoundCalculatorConfig {
Expand All @@ -46,9 +103,9 @@ impl SmudgingBoundCalculatorConfig {
/// * `params` - BFV parameters
/// * `n` - Number of parties in threshold scheme
/// * `m` - Number of ciphertexts to process
/// * `lambda` - Statistical security parameter
/// * `lambda` - Statistical security level
#[must_use]
pub fn new(params: Arc<BfvParameters>, n: usize, m: usize, lambda: usize) -> Self {
pub fn new(params: Arc<BfvParameters>, n: usize, m: usize, lambda: Lambda) -> Self {
let variance = params.variance();
let error1_variance = params.get_error1_variance().clone();
// B_enc ≈ sqrt(3 * error1_variance)
Expand Down Expand Up @@ -124,26 +181,18 @@ impl SmudgingBoundCalculator {
));
}

if self.config.lambda >= 40 {
// Calculate optimal B_sm: balance security (2^λ·B_c) and correctness ((Q/2t - B_c)/n)
let lower_bound = BigUint::from(2u64).pow(self.config.lambda as u32) * &b_c;
let upper_bound = (&q_over_2t - &b_c) / BigUint::from(self.config.n);
let b_sm = if upper_bound >= lower_bound {
lower_bound
} else {
return Err(Error::UnspecifiedInput(
"Upper bound is less than lower bound, cannot calculate B_sm".to_string(),
));
};
Ok(b_sm)
} else {
// WARNING: INSECURE PARAMETER SET.
// This is just for INSECURE parameter set.
// The security parameter is too low to calculate the optimal B_sm.
// We use a simple bound of 2 * B_c.
// This is not secure and should not be used in production.
Ok(b_c * BigUint::from(2u64))
// Calculate optimal B_sm: balance security (2^λ·B_c) and correctness ((Q/2t - B_c)/n).
// The same formula applies at every security level; Lambda only
// controls which lambdas are accepted in the first place.
let lambda = self.config.lambda.value();
let lower_bound = BigUint::from(2u64).pow(lambda as u32) * &b_c;
let upper_bound = (&q_over_2t - &b_c) / BigUint::from(self.config.n);
if upper_bound < lower_bound {
return Err(Error::UnspecifiedInput(
"Upper bound is less than lower bound, cannot calculate B_sm".to_string(),
));
}
Ok(lower_bound)
}
}

Expand Down Expand Up @@ -233,12 +282,13 @@ mod tests {
#[test]
fn test_smudging_bound_calculator_config() {
let params = test_params();
let config = SmudgingBoundCalculatorConfig::new(params.clone(), 5, 2, 80);
let config =
SmudgingBoundCalculatorConfig::new(params.clone(), 5, 2, Lambda::secure(80).unwrap());

assert_eq!(config.params, params);
assert_eq!(config.n, 5);
assert_eq!(config.m, 2);
assert_eq!(config.lambda, 80);
assert_eq!(config.lambda.value(), 80);
// b_enc is now BigUint
assert_eq!(
config.b_enc,
Expand All @@ -248,13 +298,14 @@ mod tests {
assert_eq!(config.b_e, (params.variance() * 2) as u64);
assert_eq!(config.public_key_error, 2 * params.variance() as u64);
assert_eq!(config.secret_key_bound, 5);
assert_eq!(config.lambda, 80);
assert_eq!(config.lambda.value(), 80);
}

#[test]
fn test_smudging_bound_calculator_minimal_case() {
let params = test_params();
let config = SmudgingBoundCalculatorConfig::new(params.clone(), 3, 1, 80);
let config =
SmudgingBoundCalculatorConfig::new(params.clone(), 3, 1, Lambda::secure(80).unwrap());
let calculator = SmudgingBoundCalculator::new(config);

let result = calculator.calculate_sm_bound();
Expand Down Expand Up @@ -291,7 +342,8 @@ mod tests {
#[test]
fn test_smudging_noise_generator_from_calculator() {
let params = test_params();
let config = SmudgingBoundCalculatorConfig::new(params.clone(), 3, 1, 80);
let config =
SmudgingBoundCalculatorConfig::new(params.clone(), 3, 1, Lambda::secure(80).unwrap());
let calculator = SmudgingBoundCalculator::new(config);

let result = SmudgingNoiseGenerator::from_bound_calculator(calculator);
Expand Down Expand Up @@ -368,7 +420,8 @@ mod tests {
let m = 1;

// Try the complete workflow
let config = SmudgingBoundCalculatorConfig::new(params.clone(), n, m, 80);
let config =
SmudgingBoundCalculatorConfig::new(params.clone(), n, m, Lambda::secure(80).unwrap());
let calculator = SmudgingBoundCalculator::new(config);

let bound_result = calculator.calculate_sm_bound();
Expand Down
11 changes: 6 additions & 5 deletions crates/fhe/src/trbfv/threshold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::bfv::{BfvParameters, Ciphertext, Plaintext};
use crate::trbfv::config::validate_threshold_config;
use crate::trbfv::shares::ShareManager;
use crate::trbfv::smudging::{
SmudgingBoundCalculator, SmudgingBoundCalculatorConfig, SmudgingNoiseGenerator,
Lambda, SmudgingBoundCalculator, SmudgingBoundCalculatorConfig, SmudgingNoiseGenerator,
};
use fhe_math::rq::{Ntt, Poly, PowerBasis};
use fhe_traits::FheParametrized;
Expand Down Expand Up @@ -111,15 +111,16 @@ impl TRBFV {
///
/// # Arguments
/// * `num_ciphertexts` - Number of ciphertexts being processed (e.g., votes to count, numbers to sum)
/// * `lambda` - Statistical security parameter
/// * `lambda` - Statistical security level (use `Lambda::secure(lambda)`
/// in production; `Lambda::insecure(lambda)` for fast tests)
/// * `rng` - Cryptographically secure random number generator
///
/// # Returns
/// Vector of smudging error coefficients
pub fn generate_smudging_error<R: RngCore + CryptoRng>(
&self,
num_ciphertexts: usize,
lambda: usize,
lambda: Lambda,
rng: &mut R,
) -> Result<Vec<BigInt>, Error> {
let config = SmudgingBoundCalculatorConfig::new(
Expand Down Expand Up @@ -269,7 +270,7 @@ mod tests {
let trbfv = TRBFV::new(n, threshold, params.clone()).unwrap();

let mut rng = rng();
let result = trbfv.generate_smudging_error(1, 80, &mut rng);
let result = trbfv.generate_smudging_error(1, Lambda::secure(80).unwrap(), &mut rng);
//Checking if all the coefficients of the smudging noise are different than 0,
//having one equal to zero is hardly likely to happen if the smudging noise was generated.
//TODO: add a test that calculates the empirical variance from the coefficients, so as to
Expand All @@ -293,7 +294,7 @@ mod tests {

// Test with multiple ciphertexts (this should increase the bound requirements)
let mut rng = rng();
let result = trbfv.generate_smudging_error(10, 80, &mut rng);
let result = trbfv.generate_smudging_error(10, Lambda::secure(80).unwrap(), &mut rng);

for (poly_idx, poly) in result.iter().enumerate() {
for (coeff_idx, coeff) in poly.iter().enumerate() {
Expand Down
16 changes: 11 additions & 5 deletions crates/fhe/tests/trbfv_secure_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use std::sync::Arc;
use fhe::bfv::{self, BfvParameters, Ciphertext, Encoding, Plaintext, PublicKey, SecretKey};
use fhe::mbfv::{AggregateIter, CommonRandomPoly, PublicKeyShare};
use fhe::trbfv::smudging::SmudgingNoiseGenerator;
use fhe::trbfv::{ShareManager, SmudgingBoundCalculator, SmudgingBoundCalculatorConfig, TRBFV};
use fhe::trbfv::{
Lambda, ShareManager, SmudgingBoundCalculator, SmudgingBoundCalculatorConfig, TRBFV,
};
use fhe_math::rq::{Poly, PowerBasis};
use fhe_traits::{FheDecoder, FheDecrypter, FheEncoder, FheEncrypter};
use ndarray::{Array, Array2, ArrayView};
Expand Down Expand Up @@ -74,7 +76,7 @@ fn run_threshold_sum_e2e(noise_mode: NoiseMode) {
params_trbfv.clone(),
NUM_PARTIES,
NUM_SUMMED,
LAMBDA,
Lambda::secure(LAMBDA).unwrap(),
);
let bound = SmudgingBoundCalculator::new(config)
.calculate_sm_bound()
Expand Down Expand Up @@ -119,7 +121,7 @@ fn run_threshold_sum_e2e(noise_mode: NoiseMode) {
let esi_coeffs: Vec<BigInt> = match &smudging_bound {
None => trbfv
.clone()
.generate_smudging_error(NUM_SUMMED, LAMBDA, &mut rng)
.generate_smudging_error(NUM_SUMMED, Lambda::secure(LAMBDA).unwrap(), &mut rng)
.unwrap(),
Some(bound) => vec![bound.clone(); DEGREE],
};
Expand Down Expand Up @@ -299,8 +301,12 @@ fn trbfv_smudging_bound_matches_paper_formula() {
use num_bigint::BigUint;

let params = trbfv_params();
let config =
SmudgingBoundCalculatorConfig::new(params.clone(), NUM_PARTIES, NUM_SUMMED, LAMBDA);
let config = SmudgingBoundCalculatorConfig::new(
params.clone(),
NUM_PARTIES,
NUM_SUMMED,
Lambda::secure(LAMBDA).unwrap(),
);
let calculator = SmudgingBoundCalculator::new(config.clone());
let bound = calculator.calculate_sm_bound().unwrap();

Expand Down
Loading