From 7b7b755f477c71fd4fd4f7e6f3049216b8ebc5eb Mon Sep 17 00:00:00 2001 From: Joe Doyle Date: Tue, 25 Nov 2025 17:04:13 -0500 Subject: [PATCH 1/4] Convert get_mark() to MARK associated constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Updated trait definition to use const MARK - Modified derive macro to generate constants instead of methods - Updated documentation to reflect the new API - Adapted test to use constant reference for custom marks Resolves #6 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- inscribe-derive/src/lib.rs | 14 +++++--------- src/inscribe.rs | 6 +++--- tests/inscribe_tests.rs | 8 +------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/inscribe-derive/src/lib.rs b/inscribe-derive/src/lib.rs index b499ccb..2314cda 100644 --- a/inscribe-derive/src/lib.rs +++ b/inscribe-derive/src/lib.rs @@ -175,7 +175,7 @@ fn implement_get_inscription(dstruct: &DataStruct) -> TokenStream { use decree::decree::FSInput; let mut serial_out: Vec = Vec::new(); - let mut hasher = TupleHash::v256(self.get_mark().as_bytes()); + let mut hasher = TupleHash::v256(Self::MARK.as_bytes()); // Add the struct members into the TupleHash #center @@ -196,12 +196,10 @@ fn implement_default_mark(ast: &DeriveInput) -> TokenStream { let ident = &ast.ident; let ident_str = ident.to_string(); - let get_mark = quote!{ - fn get_mark(&self) -> &'static str { - return #ident_str; - } + let mark = quote!{ + const MARK: &'static str = #ident_str; }; - get_mark + mark } fn implement_get_addl(ast: &DeriveInput) -> TokenStream { @@ -255,9 +253,7 @@ fn implement_get_mark(ast: &DeriveInput) -> TokenStream { if let Some(meta) = nested.iter().next() { match meta { Meta::Path(path) => { mark_implementation = quote!{ - fn get_mark(&self) -> &'static str { - self.#path() - } + const MARK: &'static str = #path; }}, _ => { panic!("Invalid metadata for field attribute"); }, } diff --git a/src/inscribe.rs b/src/inscribe.rs index cf9728d..858a0cb 100644 --- a/src/inscribe.rs +++ b/src/inscribe.rs @@ -8,14 +8,14 @@ pub type InscribeBuffer = [u8; INSCRIBE_LENGTH]; /// contextual data into Fiat-Shamir transcripts. There are two main methods that the trait /// requires: /// -/// `fn get_mark(&self) -> &'static str` +/// `const MARK: &'static str` /// /// and /// /// `fn get_inscription(&self) -> FSInput` /// /// For derived structs, the `get_inscription` method will do the following: -/// - Initialize a TupleHash with the results of `get_mark` +/// - Initialize a TupleHash with the contents of `MARK` /// - For each member of the struct, do one of three things: /// + For `Inscribe` implementers, call `get_inscription` and add the results to the /// TupleHash @@ -124,7 +124,7 @@ pub type InscribeBuffer = [u8; INSCRIBE_LENGTH]; /// ``` /// pub trait Inscribe { - fn get_mark(&self) -> &'static str; + const MARK: &'static str; fn get_inscription(&self) -> DecreeResult; fn get_additional(&self) -> DecreeResult { let x: Vec = Vec::new(); diff --git a/tests/inscribe_tests.rs b/tests/inscribe_tests.rs index cc75bb9..33395e2 100644 --- a/tests/inscribe_tests.rs +++ b/tests/inscribe_tests.rs @@ -12,7 +12,7 @@ mod tests { const MARK_TEST_DATA: &str = "Atypical mark!"; #[derive(Inscribe)] - #[inscribe_mark(atypical_mark)] + #[inscribe_mark(MARK_TEST_DATA)] struct Point { #[inscribe(serialize)] #[inscribe_name(input_2)] @@ -22,12 +22,6 @@ mod tests { y: i32, } - impl Point { - fn atypical_mark(&self) -> &'static str { - MARK_TEST_DATA - } - } - #[derive(Inscribe)] #[inscribe_addl(additional_data_method)] struct InscribeTest { From 8bf56426a4a042964771f06d35a8d99a6bbbc540 Mon Sep 17 00:00:00 2001 From: Joe Doyle Date: Tue, 25 Nov 2025 20:33:54 -0500 Subject: [PATCH 2/4] Fix clippy warnings in inscribe-derive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add deny(warnings) and deny(clippy::pedantic), fix all warnings. Resolves trailofbits/decree#12 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- inscribe-derive/src/lib.rs | 78 +++++++++++--------------------------- 1 file changed, 23 insertions(+), 55 deletions(-) diff --git a/inscribe-derive/src/lib.rs b/inscribe-derive/src/lib.rs index b499ccb..5cc17ab 100644 --- a/inscribe-derive/src/lib.rs +++ b/inscribe-derive/src/lib.rs @@ -1,3 +1,6 @@ +#![deny(warnings)] +#![deny(clippy::pedantic)] + use proc_macro2::TokenStream; use syn::{Attribute, AttrStyle, Data, DataStruct, DeriveInput, Field, Fields, Ident, Meta, Token}; use syn::punctuated::Punctuated; @@ -28,20 +31,14 @@ struct MemberInfo { fn parse_contained_ident(attr: &Attribute) -> Option { // Get the nested attribute data - let nested = match attr.parse_args_with(Punctuated::::parse_terminated) { - Ok(parse_result) => parse_result, - Err(_) => { return None; }, - }; + let Ok(nested) = attr.parse_args_with(Punctuated::::parse_terminated) else { return None; }; // This was originally a for loop, but clippy noted that it never actually loops, so it // has been replaced with an if-let construction. This may be something to watch if the // metadata API changes. - if let Some(meta) = nested.iter().next() { - match meta { - Meta::Path(path) => { return Some(path.get_ident().unwrap().clone()); }, - _ => { }, - } - }; + if let Some(Meta::Path(path)) = nested.iter().next() { + return Some(path.get_ident().unwrap().clone()); + } None } @@ -51,10 +48,7 @@ fn get_member_info(field: &Field) -> MemberInfo { let mut member_handling = Handling::Recurse; let mut found_handling: bool = false; let mut found_name: bool = false; - let mut sort_name = match field.ident.clone() { - Some(k) => k, - None => { panic!("Couldn't get field name"); } - }; + let Some(mut sort_name) = field.ident.clone() else { panic!("Couldn't get field name"); }; // Run over all the attributes for attr in field.clone().attrs { @@ -68,40 +62,31 @@ fn get_member_info(field: &Field) -> MemberInfo { } // Parse out whatever is inside the attribute - let inside = match parse_contained_ident(&attr) { - Some(ident) => ident, - None => { panic!("Failed to parse member attribute for Inscribe trait"); } - }; + let Some(inside) = parse_contained_ident(&attr) else { panic!("Failed to parse member attribute for Inscribe trait"); }; // Get handling specifications if attr.path().is_ident(INSCRIBE_HANDLING_IDENT) { // Don't process the same handling twice - if found_handling { - panic!("Inscribe handling attribute defined more than once"); - } + assert!(!found_handling, "Inscribe handling attribute defined more than once"); - if inside.to_string() == String::from(SKIP_IDENT) { + if inside == SKIP_IDENT { member_handling = Handling::Skip; - } else if inside.to_string() == String::from(SERIALIZE_IDENT) { + } else if inside == SERIALIZE_IDENT { member_handling = Handling::Serialize; - } else if inside.to_string() == String::from(RECURSE_IDENT) { + } else if inside == RECURSE_IDENT { member_handling = Handling::Recurse; } else { panic!("Invalid handling specification"); } found_handling = true; - continue; } // Get sorting name if attr.path().is_ident(INSCRIBE_NAME_IDENT) { // Don't process the name twice - if found_name { - panic!("Inscribe name attribute defined more than once"); - } + assert!(!found_name, "Inscribe name attribute defined more than once"); sort_name = inside.clone(); found_name = true; - continue; } } @@ -113,10 +98,7 @@ fn get_member_info(field: &Field) -> MemberInfo { } fn implement_get_inscription(dstruct: &DataStruct) -> TokenStream { - let members = match dstruct.fields.clone() { - Fields::Named(a) => a, - _ => { panic!("Invalid struct type"); } - }; + let Fields::Named(members) = dstruct.fields.clone() else { panic!("Invalid struct type"); }; // Build hash table to match each of the struct member names to an associated MemberInfo // struct @@ -124,8 +106,8 @@ fn implement_get_inscription(dstruct: &DataStruct) -> TokenStream { let mut member_vec: Vec = Vec::new(); - for field in members.named.iter() { - let member_info = get_member_info(&field); + for field in &members.named { + let member_info = get_member_info(field); let sort_name_str = member_info.sort_ident.to_string(); member_table.insert(sort_name_str.clone(), member_info); @@ -136,7 +118,7 @@ fn implement_get_inscription(dstruct: &DataStruct) -> TokenStream { let mut center = quote!{}; member_vec.sort(); - for sort_name in member_vec.iter() { + for sort_name in &member_vec { let current_member = member_table.get(sort_name).unwrap(); // Guaranteed to work let member_ident = current_member.name_ident.clone(); @@ -213,12 +195,7 @@ fn implement_get_addl(ast: &DeriveInput) -> TokenStream { // We only look for "inscribe" attributes if !attr.path().is_ident(INSCRIBE_ADDL_IDENT) { continue; } - let nested = match attr.parse_args_with(Punctuated::::parse_terminated) { - Ok(parse_result) => { - parse_result - }, - Err(_) => { panic!("Failed to parse inscribe_addl field attribute"); } - }; + let Ok(nested) = attr.parse_args_with(Punctuated::::parse_terminated) else { panic!("Failed to parse inscribe_addl field attribute"); }; if let Some(meta) = nested.iter().next() { match meta { @@ -245,12 +222,7 @@ fn implement_get_mark(ast: &DeriveInput) -> TokenStream { // We only look for "inscribe" attributes if !attr.path().is_ident(INSCRIBE_MARK_IDENT) { continue; } - let nested = match attr.parse_args_with(Punctuated::::parse_terminated) { - Ok(parse_result) => { - parse_result - }, - Err(_) => { panic!("Failed to parse inscribe_mark field attribute"); } - }; + let Ok(nested) = attr.parse_args_with(Punctuated::::parse_terminated) else { panic!("Failed to parse inscribe_mark field attribute"); }; if let Some(meta) = nested.iter().next() { match meta { @@ -294,19 +266,15 @@ fn implement_inscribe_trait(ast: DeriveInput, dstruct: &DataStruct) -> TokenStre #[proc_macro_derive(Inscribe, attributes(inscribe, inscribe_addl, inscribe_mark, inscribe_name))] +#[allow(clippy::missing_panics_doc)] pub fn inscribe_derive(item: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast: DeriveInput = syn::parse(item.clone()).unwrap(); // We don't support for derive for anything but structs - let dstruct = match ast.clone().data { - Data::Struct(d) => d, - _ => { panic!("Invalid type for derive(Inscribe)")}, - }; + let Data::Struct(dstruct) = ast.clone().data else { panic!("Invalid type for derive(Inscribe)")}; // We don't support unnamed structs - if !matches!(dstruct.fields, Fields::Named(_)) { - panic!("Unnamed structs not supported for derive(Inscribe)"); - } + assert!(matches!(dstruct.fields, Fields::Named(_)), "Unnamed structs not supported for derive(Inscribe)"); implement_inscribe_trait(ast, &dstruct).into() } \ No newline at end of file From 8688aa991df221df14a6fc9aff2e3a0ebafa0dec Mon Sep 17 00:00:00 2001 From: Joe Doyle Date: Tue, 25 Nov 2025 20:44:59 -0500 Subject: [PATCH 3/4] Add Small-Factor Proof example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Small-Factor Proof from Figure 26 (Canetti et al. 2024). Proves RSA modulus N₀ = pq has factors p, q > 2^ℓ using Pedersen commitments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/small_factor_proof.rs | 526 +++++++++++++++++++++++++++++++++ 1 file changed, 526 insertions(+) create mode 100644 examples/small_factor_proof.rs diff --git a/examples/small_factor_proof.rs b/examples/small_factor_proof.rs new file mode 100644 index 0000000..13c8928 --- /dev/null +++ b/examples/small_factor_proof.rs @@ -0,0 +1,526 @@ +//! Small-Factor Proof Example +//! +//! This example demonstrates a proof that an RSA modulus N₀ = pq has factors +//! that are not too small. Specifically, it proves that both p and q are +//! greater than 2^ℓ. +//! +//! This is useful in protocols where RSA key generation needs to be verified, +//! such as threshold RSA signatures or verifiable delay functions. +//! +//! The proof uses Pedersen commitments in an auxiliary RSA group Ñ and follows +//! the protocol from Canetti et al. 2024, Figure 26. + +#![deny(warnings)] +#![cfg_attr(not(test), allow(dead_code))] + +use decree::error::Error; +use decree::Inscribe; +use decree::decree::{Decree, FSInput}; +use num_bigint::{BigInt, BigUint, RandBigInt, Sign}; +use num_traits::{One, Zero}; +use rand::rngs::OsRng; +use rand::Rng; + +/// Security parameter for statistical distance +const EPSILON: u32 = 128; + +/// Miller-Rabin primality test +fn is_probably_prime(n: &BigUint, rounds: u32) -> bool { + if n < &BigUint::from(2u32) { + return false; + } + if n == &BigUint::from(2u32) || n == &BigUint::from(3u32) { + return true; + } + if !n.bit(0) { + return false; // even number + } + + // Write n-1 as 2^r · d + let n_minus_1 = n - BigUint::one(); + let mut d = n_minus_1.clone(); + let mut r = 0u32; + while !d.bit(0) { + d >>= 1; + r += 1; + } + + let mut rng = OsRng; + + 'witness: for _ in 0..rounds { + let a = rng.gen_biguint_range(&BigUint::from(2u32), &(n - BigUint::from(2u32))); + let mut x = a.modpow(&d, n); + + if x == BigUint::one() || x == n_minus_1 { + continue 'witness; + } + + for _ in 0..(r - 1) { + x = x.modpow(&BigUint::from(2u32), n); + if x == n_minus_1 { + continue 'witness; + } + } + + return false; + } + + true +} + +/// Generate a random prime of specified bit length +fn generate_prime(bits: usize) -> BigUint { + let mut rng = OsRng; + loop { + // Generate random odd number of specified bit length + let mut candidate = rng.gen_biguint(bits as u64); + + // Set high bit to ensure correct bit length + candidate.set_bit(bits as u64 - 1, true); + + // Make it odd + candidate.set_bit(0, true); + + // Test for primality + if is_probably_prime(&candidate, 20) { + return candidate; + } + } +} + +/// Wrapper for BigInt with proper domain separation for Inscribe +struct BigIntWrapper(BigInt); + +impl Inscribe for BigIntWrapper { + const MARK: &'static str = "BigInt"; + + fn get_inscription(&self) -> Result { + let bytes = self.0.to_signed_bytes_le(); + Ok(bytes) + } + + fn get_additional(&self) -> Result { + Ok(b"small-factor-proof-bigint".to_vec()) + } +} + +/// Wrapper for BigUint with proper domain separation for Inscribe +struct BigUintWrapper(BigUint); + +impl Inscribe for BigUintWrapper { + const MARK: &'static str = "BigUint"; + + fn get_inscription(&self) -> Result { + let bytes = self.0.to_bytes_le(); + Ok(bytes) + } + + fn get_additional(&self) -> Result { + Ok(b"small-factor-proof-biguint".to_vec()) + } +} + +/// Pedersen commitment parameters in RSA group +struct PedersenParameters { + n_tilde: BigUint, // Ñ: auxiliary RSA modulus + s: BigUint, // first generator + t: BigUint, // second generator +} + +/// Generate Pedersen parameters with auxiliary RSA modulus +fn generate_pedersen_parameters(bits: usize) -> PedersenParameters { + let mut rng = OsRng; + + // Generate two random primes for Ñ + let p_tilde = generate_prime(bits / 2); + let q_tilde = generate_prime(bits / 2); + let n_tilde = &p_tilde * &q_tilde; + + // Generate random generators s and t in Z*_Ñ + let s = rng.gen_biguint_range(&BigUint::from(2u32), &n_tilde); + let t = rng.gen_biguint_range(&BigUint::from(2u32), &n_tilde); + + PedersenParameters { n_tilde, s, t } +} + +/// Commitment phase of the small-factor proof +#[derive(Inscribe)] +struct SmallFactorCommitment { + p_commit: BigUintWrapper, // P = s^p · t^μ + q_commit: BigUintWrapper, // Q = s^q · t^ν + a_commit: BigUintWrapper, // A = s^α · t^x + b_commit: BigUintWrapper, // B = s^β · t^y + t_commit: BigUintWrapper, // T = Q^α · t^r +} + +/// Response phase of the small-factor proof +#[derive(Inscribe)] +struct SmallFactorResponse { + z1: BigIntWrapper, // z₁ = α + ep + z2: BigIntWrapper, // z₂ = β + eq + w1: BigIntWrapper, // w₁ = x + eμ + w2: BigIntWrapper, // w₂ = y + eν + v: BigIntWrapper, // v = r - e·νp +} + +/// Complete small-factor proof +#[derive(Inscribe)] +struct SmallFactorProof { + commitment: SmallFactorCommitment, + #[inscribe(skip)] + response: SmallFactorResponse, +} + +/// Modular exponentiation with signed exponent +/// Computes base^exp mod modulus, where exp can be negative +fn modpow_signed(base: &BigUint, exp: &BigInt, modulus: &BigUint) -> BigUint { + if exp.sign() == Sign::Minus { + // For negative exponent, compute (base^(-1))^|exp| mod modulus + // base^(-1) mod modulus using extended GCD + let base_int = BigInt::from_biguint(Sign::Plus, base.clone()); + let modulus_int = BigInt::from_biguint(Sign::Plus, modulus.clone()); + + // Extended GCD to find modular inverse + let (gcd, inv, _) = extended_gcd(&base_int, &modulus_int); + if gcd != BigInt::one() { + panic!("Base is not invertible mod modulus"); + } + + // Ensure inverse is positive + let inv_pos = if inv.sign() == Sign::Minus { + &modulus_int + inv + } else { + inv + }; + + inv_pos.to_biguint().unwrap().modpow(exp.magnitude(), modulus) + } else { + base.modpow(exp.magnitude(), modulus) + } +} + +/// Extended GCD algorithm +fn extended_gcd(a: &BigInt, b: &BigInt) -> (BigInt, BigInt, BigInt) { + if b.is_zero() { + return (a.clone(), BigInt::one(), BigInt::zero()); + } + + let (gcd, x1, y1) = extended_gcd(b, &(a % b)); + let x = y1.clone(); + let y = x1 - (a / b) * y1; + + (gcd, x, y) +} + +/// Sample a signed BigInt uniformly from [-bound, bound] +fn sample_pm_range(bound: &BigUint) -> BigInt { + let mut rng = OsRng; + let magnitude = rng.gen_biguint_range(&BigUint::zero(), bound); + let sign = if rng.gen::() { + Sign::Plus + } else { + Sign::Minus + }; + BigInt::from_biguint(sign, magnitude) +} + +/// Generate RSA modulus for testing +fn generate_rsa_modulus(bits: usize) -> (BigUint, BigUint, BigUint) { + let p = generate_prime(bits / 2); + let q = generate_prime(bits / 2); + let n = &p * &q; + (n, p, q) +} + +/// Compute ceiling of integer square root +fn isqrt_ceil(n: &BigUint) -> BigUint { + if n <= &BigUint::one() { + return n.clone(); + } + + // Newton's method for integer sqrt + let mut x = n.clone(); + loop { + let x_new = (&x + (n / &x)) >> 1; + if x_new >= x { + // Check if we need ceiling + if &(&x * &x) < n { + return &x + BigUint::one(); + } + return x; + } + x = x_new; + } +} + +/// Generate a small-factor proof +fn prove( + params: &PedersenParameters, + n0: &BigUint, + p: &BigUint, + q: &BigUint, + ell: u32, +) -> SmallFactorProof { + // Compute bounds for sampling + let sqrt_n0 = isqrt_ceil(n0); + let two_ell = BigUint::from(2u32).pow(ell); + let two_ell_eps = BigUint::from(2u32).pow(ell + EPSILON); + let bound_alpha = &two_ell_eps * &sqrt_n0; // ±2^(ℓ+ε) · √N₀ + let bound_mu = &two_ell * ¶ms.n_tilde; // ±2^ℓ · Ñ + let bound_r = &two_ell_eps * n0 * ¶ms.n_tilde; // ±2^(ℓ+ε) · N₀ · Ñ + let bound_x = &two_ell_eps * ¶ms.n_tilde; // ±2^(ℓ+ε) · Ñ + + // Sample randomness for Pedersen commitments + let mu = sample_pm_range(&bound_mu); + let nu = sample_pm_range(&bound_mu); + + // Compute P = s^p · t^μ mod Ñ + let p_commit = (¶ms.s.modpow(p, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &mu, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + // Compute Q = s^q · t^ν mod Ñ + let q_commit = (¶ms.s.modpow(q, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &nu, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + // Sample masking values + let alpha = sample_pm_range(&bound_alpha); + let beta = sample_pm_range(&bound_alpha); + let x = sample_pm_range(&bound_x); + let y = sample_pm_range(&bound_x); + let r = sample_pm_range(&bound_r); + + // Compute A = s^α · t^x mod Ñ + let a_commit = (&modpow_signed(¶ms.s, &alpha, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &x, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + // Compute B = s^β · t^y mod Ñ + let b_commit = (&modpow_signed(¶ms.s, &beta, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &y, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + // Compute T = Q^α · t^r mod Ñ + let t_commit = (&modpow_signed(&q_commit, &alpha, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &r, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + let commitment = SmallFactorCommitment { + p_commit: BigUintWrapper(p_commit), + q_commit: BigUintWrapper(q_commit.clone()), + a_commit: BigUintWrapper(a_commit), + b_commit: BigUintWrapper(b_commit), + t_commit: BigUintWrapper(t_commit), + }; + + // Create transcript for challenge generation + let mut transcript = Decree::new( + "small-factor-proof", + &["commitment"], + &["challenge"], + ).unwrap(); + transcript.add("commitment", &commitment).unwrap(); + + // Get challenge e ← ±2^ℓ + let mut challenge_bytes = vec![0u8; (ell as usize).div_ceil(8)]; + transcript.get_challenge("challenge", &mut challenge_bytes).unwrap(); + let e_magnitude = BigUint::from_bytes_le(&challenge_bytes) % BigUint::from(2u32).pow(ell); + let e = BigInt::from_biguint(Sign::Plus, e_magnitude); + + // Compute responses + let z1 = &alpha + (&e * BigInt::from_biguint(Sign::Plus, p.clone())); + let z2 = &beta + (&e * BigInt::from_biguint(Sign::Plus, q.clone())); + let w1 = &x + (&e * &mu); + let w2 = &y + (&e * &nu); + let v = &r - (&e * &nu * BigInt::from_biguint(Sign::Plus, p.clone())); + + let response = SmallFactorResponse { + z1: BigIntWrapper(z1), + z2: BigIntWrapper(z2), + w1: BigIntWrapper(w1), + w2: BigIntWrapper(w2), + v: BigIntWrapper(v), + }; + + SmallFactorProof { + commitment, + response, + } +} + +/// Verify a small-factor proof +fn verify( + params: &PedersenParameters, + proof: &SmallFactorProof, + n0: &BigUint, + ell: u32, +) -> bool { + // Recreate challenge from commitment + let mut transcript = Decree::new( + "small-factor-proof", + &["commitment"], + &["challenge"], + ).unwrap(); + transcript.add("commitment", &proof.commitment).unwrap(); + + let mut challenge_bytes = vec![0u8; (ell as usize).div_ceil(8)]; + transcript.get_challenge("challenge", &mut challenge_bytes).unwrap(); + let e_magnitude = BigUint::from_bytes_le(&challenge_bytes) % BigUint::from(2u32).pow(ell); + let e = BigInt::from_biguint(Sign::Plus, e_magnitude); + + // Range check: z₁, z₂ ∈ ±√N₀ · 2^(ℓ+ε) + let sqrt_n0 = isqrt_ceil(n0); + let two_ell_eps = BigUint::from(2u32).pow(ell + EPSILON); + let range_bound = &sqrt_n0 * &two_ell_eps; + + if proof.response.z1.0.magnitude() > &range_bound + || proof.response.z2.0.magnitude() > &range_bound + { + return false; + } + + // Equality checks + // Compute R = s^N₀ mod Ñ + let r_val = params.s.modpow(n0, ¶ms.n_tilde); + + // Check 1: s^z₁ · t^w₁ = A · P^e mod Ñ + let lhs1 = (&modpow_signed(¶ms.s, &proof.response.z1.0, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &proof.response.w1.0, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + let rhs1 = (&proof.commitment.a_commit.0 + * &modpow_signed(&proof.commitment.p_commit.0, &e, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + if lhs1 != rhs1 { + return false; + } + + // Check 2: s^z₂ · t^w₂ = B · Q^e mod Ñ + let lhs2 = (&modpow_signed(¶ms.s, &proof.response.z2.0, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &proof.response.w2.0, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + let rhs2 = (&proof.commitment.b_commit.0 + * &modpow_signed(&proof.commitment.q_commit.0, &e, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + if lhs2 != rhs2 { + return false; + } + + // Check 3: Q^z₁ · t^v = T · R^e mod Ñ + let lhs3 = (&modpow_signed(&proof.commitment.q_commit.0, &proof.response.z1.0, ¶ms.n_tilde) + * &modpow_signed(¶ms.t, &proof.response.v.0, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + let rhs3 = (&proof.commitment.t_commit.0 * &modpow_signed(&r_val, &e, ¶ms.n_tilde)) + % ¶ms.n_tilde; + + if lhs3 != rhs3 { + return false; + } + + true +} + +fn main() { + // This example demonstrates the small-factor proof in the tests below + println!("Run `cargo test --example small_factor_proof` to see the proof in action"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_proof() { + // Generate Pedersen parameters with 2048-bit modulus + let params = generate_pedersen_parameters(2048); + + // Generate RSA modulus with proper-sized factors + // Use ℓ = 1000 to ensure 1024-bit primes are above 2^ℓ + let ell = 1000; + let (n0, p, q) = generate_rsa_modulus(2048); + + // Verify p and q are above the threshold + let threshold = BigUint::from(2u32).pow(ell); + assert!(p > threshold); + assert!(q > threshold); + + // Generate and verify proof + let proof = prove(¶ms, &n0, &p, &q, ell); + assert!(verify(¶ms, &proof, &n0, ell)); + } + + #[test] + fn test_proof_with_different_sizes() { + // Test with 1536-bit modulus + let params = generate_pedersen_parameters(1536); + let ell = 750; // Below 768 to ensure 768-bit primes exceed threshold + let (n0, p, q) = generate_rsa_modulus(1536); + + let threshold = BigUint::from(2u32).pow(ell); + assert!(p > threshold); + assert!(q > threshold); + + let proof = prove(¶ms, &n0, &p, &q, ell); + assert!(verify(¶ms, &proof, &n0, ell)); + } + + #[test] + fn test_multiple_proofs() { + // Generate parameters once + let params = generate_pedersen_parameters(2048); + let ell = 1000; + + // Generate and verify multiple proofs + for _ in 0..3 { + let (n0, p, q) = generate_rsa_modulus(2048); + let proof = prove(¶ms, &n0, &p, &q, ell); + assert!(verify(¶ms, &proof, &n0, ell)); + } + } + + #[test] + fn test_wrong_modulus() { + // Generate proof for one modulus + let params = generate_pedersen_parameters(2048); + let ell = 1000; + let (n0, p, q) = generate_rsa_modulus(2048); + let proof = prove(¶ms, &n0, &p, &q, ell); + + // Try to verify with different modulus + let (n0_wrong, _, _) = generate_rsa_modulus(2048); + assert!(!verify(¶ms, &proof, &n0_wrong, ell)); + } + + #[test] + fn test_modified_commitment() { + let params = generate_pedersen_parameters(2048); + let ell = 1000; + let (n0, p, q) = generate_rsa_modulus(2048); + let mut proof = prove(¶ms, &n0, &p, &q, ell); + + // Modify one commitment value + proof.commitment.a_commit.0 = (&proof.commitment.a_commit.0 + BigUint::one()) % ¶ms.n_tilde; + + // Verification should fail + assert!(!verify(¶ms, &proof, &n0, ell)); + } + + #[test] + fn test_modified_response() { + let params = generate_pedersen_parameters(2048); + let ell = 1000; + let (n0, p, q) = generate_rsa_modulus(2048); + let mut proof = prove(¶ms, &n0, &p, &q, ell); + + // Modify one response value + proof.response.z1.0 = &proof.response.z1.0 + BigInt::one(); + + // Verification should fail + assert!(!verify(¶ms, &proof, &n0, ell)); + } +} From 574c6802d456f22822b7a4ae194a365dbb09e9c9 Mon Sep 17 00:00:00 2001 From: Joe Doyle Date: Tue, 25 Nov 2025 23:09:44 -0500 Subject: [PATCH 4/4] Add Paillier-Blum modulus proof and refactor shared crypto helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement Paillier-Blum modulus proof (Figure 12) with safe prime generation - Add Jacobi symbol verification, compositeness check, and p ≠ q validation - Extract shared crypto helpers (is_probably_prime, extended_gcd, jacobi_symbol, generate_prime) to examples/crypto_helpers.rs - Add opt-level = 2 to dev profile for faster test execution - All 28 Paillier-Blum tests and 8 small-factor tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Cargo.toml | 5 +- examples/crypto_helpers.rs | 135 ++++ examples/paillier_blum_proof.rs | 1043 +++++++++++++++++++++++++++++++ examples/small_factor_proof.rs | 184 +++--- 4 files changed, 1260 insertions(+), 107 deletions(-) create mode 100644 examples/crypto_helpers.rs create mode 100644 examples/paillier_blum_proof.rs diff --git a/Cargo.toml b/Cargo.toml index 670bbe2..efe7157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,7 @@ inscribe-derive = { path = "inscribe-derive" } [dev-dependencies] num-bigint = { version="0.4.4", features = ["rand", "serde"] } num-traits = { version="0.2.15" } -rand = "0.8.5" \ No newline at end of file +rand = "0.8.5" + +[profile.dev] +opt-level = 2 \ No newline at end of file diff --git a/examples/crypto_helpers.rs b/examples/crypto_helpers.rs new file mode 100644 index 0000000..e8e2b04 --- /dev/null +++ b/examples/crypto_helpers.rs @@ -0,0 +1,135 @@ +//! Cryptographic helper functions shared between zero-knowledge proof examples +//! +//! This module provides common utilities including primality testing, +//! extended GCD, and Jacobi symbol computation. + +#![allow(dead_code)] + +use num_bigint::{BigInt, BigUint, RandBigInt}; +use num_traits::{One, Zero}; +use rand::rngs::OsRng; + +/// Miller-Rabin primality test +/// +/// Returns true if n is probably prime after the given number of rounds. +/// The probability of a false positive decreases exponentially with rounds. +pub fn is_probably_prime(n: &BigUint, rounds: u32) -> bool { + if n < &BigUint::from(2u32) { + return false; + } + if n == &BigUint::from(2u32) || n == &BigUint::from(3u32) { + return true; + } + if !n.bit(0) { + return false; // even number + } + + // Write n-1 as 2^r · d + let n_minus_1 = n - BigUint::one(); + let mut d = n_minus_1.clone(); + let mut r = 0u32; + while !d.bit(0) { + d >>= 1; + r += 1; + } + + let mut rng = OsRng; + + 'witness: for _ in 0..rounds { + let a = rng.gen_biguint_range(&BigUint::from(2u32), &(n - BigUint::from(2u32))); + let mut x = a.modpow(&d, n); + + if x == BigUint::one() || x == n_minus_1 { + continue 'witness; + } + + for _ in 0..(r - 1) { + x = x.modpow(&BigUint::from(2u32), n); + if x == n_minus_1 { + continue 'witness; + } + } + + return false; + } + + true +} + +/// Extended GCD algorithm +/// +/// Returns (gcd, x, y) such that gcd = a*x + b*y +pub fn extended_gcd(a: &BigInt, b: &BigInt) -> (BigInt, BigInt, BigInt) { + if b.is_zero() { + return (a.clone(), BigInt::one(), BigInt::zero()); + } + + let (gcd, x1, y1) = extended_gcd(b, &(a % b)); + let x = y1.clone(); + let y = x1 - (a / b) * y1; + + (gcd, x, y) +} + +/// Compute Jacobi symbol (a/n) +/// +/// Returns -1, 0, or 1. +/// Panics if n is not a positive odd integer. +pub fn jacobi_symbol(a: &BigInt, n: &BigInt) -> i8 { + if n <= &BigInt::zero() || !n.bit(0) { + panic!("n must be a positive odd integer"); + } + + let mut a = a.clone() % n; + let mut n = n.clone(); + let mut result = 1i8; + + while !a.is_zero() { + // Remove factors of 2 from a + while !a.bit(0) { + a >>= 1; + let n_mod_8 = &n % BigInt::from(8); + if n_mod_8 == BigInt::from(3) || n_mod_8 == BigInt::from(5) { + result = -result; + } + } + + // Swap a and n + std::mem::swap(&mut a, &mut n); + + // Quadratic reciprocity + if &a % BigInt::from(4) == BigInt::from(3) && &n % BigInt::from(4) == BigInt::from(3) { + result = -result; + } + + a %= &n; + } + + if n == BigInt::one() { + result + } else { + 0 + } +} + +/// Generate a random prime of specified bit length +/// +/// The prime will have the specified number of bits (i.e., 2^(bits-1) <= p < 2^bits). +pub fn generate_prime(bits: usize) -> BigUint { + let mut rng = OsRng; + loop { + // Generate random odd number of specified bit length + let mut candidate = rng.gen_biguint(bits as u64); + + // Set high bit to ensure correct bit length + candidate.set_bit(bits as u64 - 1, true); + + // Make it odd + candidate.set_bit(0, true); + + // Test for primality + if is_probably_prime(&candidate, 20) { + return candidate; + } + } +} diff --git a/examples/paillier_blum_proof.rs b/examples/paillier_blum_proof.rs new file mode 100644 index 0000000..1c8ef52 --- /dev/null +++ b/examples/paillier_blum_proof.rs @@ -0,0 +1,1043 @@ +//! Paillier-Blum Modulus Proof Example +//! +//! This example demonstrates a zero-knowledge proof that an RSA modulus N +//! is a Paillier-Blum modulus, i.e., N = pq where p and q are distinct primes +//! satisfying p ≡ q ≡ 3 (mod 4). +//! +//! This property is essential for Pedersen commitments in RSA groups, as it +//! ensures that -1 has Jacobi symbol +1 modulo N (equivalently, -1 is a +//! quadratic residue mod N), which is necessary for the security of commitment schemes. +//! +//! The proof follows the protocol from Canetti et al. 2024, Figure 12. + +#![deny(warnings)] +#![cfg_attr(not(test), allow(dead_code))] + +mod crypto_helpers; + +use decree::error::Error; +use decree::Inscribe; +use decree::decree::{Decree, FSInput}; +use crypto_helpers::{is_probably_prime, extended_gcd, jacobi_symbol}; +use num_bigint::{BigInt, BigUint, RandBigInt, Sign}; +use num_traits::{One, Zero}; +use rand::rngs::OsRng; + +/// Minimum number of challenge rounds for security +const MIN_CHALLENGE_ROUNDS: u32 = 80; + +/// Generate a random safe prime p = 2q + 1 where both p and q are prime +/// Safe primes satisfy p ≡ 3 (mod 4) when q is odd (which it must be if q > 2) +fn generate_safe_prime(bits: usize) -> BigUint { + let mut rng = OsRng; + loop { + // Generate random q of bit length (bits - 1) + let mut q = rng.gen_biguint((bits - 1) as u64); + + // Set high bit of q to ensure it has the right bit length + q.set_bit((bits - 2) as u64, true); + + // Make q odd + q.set_bit(0, true); + + // Test if q is prime + if !is_probably_prime(&q, 20) { + continue; + } + + // Compute p = 2q + 1 + let p = &q * BigUint::from(2u32) + BigUint::one(); + + // Test if p is prime + if is_probably_prime(&p, 20) { + // Verify p ≡ 3 (mod 4) (should always be true for safe primes with q odd) + debug_assert_eq!(p.clone() % BigUint::from(4u32), BigUint::from(3u32)); + return p; + } + } +} + +/// Generate a Paillier-Blum modulus N = pq where p, q ≡ 3 (mod 4) +/// Returns (N, p, q) +fn generate_paillier_blum_modulus(bits: usize) -> (BigUint, BigUint, BigUint) { + let p = generate_safe_prime(bits / 2); + let q = generate_safe_prime(bits / 2); + let n = &p * &q; + (n, p, q) +} + +/// Find a 4th root of a modulo Paillier-Blum modulus N = pq +/// where p, q ≡ 3 (mod 4) and a is a 4th power residue mod N +fn fourth_root_mod_paillier_blum(a: &BigUint, p: &BigUint, q: &BigUint, n: &BigUint) -> Option { + // For p ≡ 3 (mod 4), we can compute 4th roots as follows: + // First compute square roots mod p and mod q + // Then use CRT to combine them + + // Compute r = (p+1)/4 and s = (q+1)/4 + let r = (p + BigUint::one()) / BigUint::from(4u32); + let s = (q + BigUint::one()) / BigUint::from(4u32); + + // Compute a^r mod p (this is a square root of a mod p) + let sqrt_p = a.modpow(&r, p); + + // Compute another square root to get 4th root mod p + let fourth_root_p = sqrt_p.modpow(&r, p); + + // Similarly for q + let sqrt_q = a.modpow(&s, q); + let fourth_root_q = sqrt_q.modpow(&s, q); + + // Use Chinese Remainder Theorem to combine + // Find x such that x ≡ fourth_root_p (mod p) and x ≡ fourth_root_q (mod q) + let p_int = BigInt::from_biguint(Sign::Plus, p.clone()); + let q_int = BigInt::from_biguint(Sign::Plus, q.clone()); + + let (gcd, u, v) = extended_gcd(&p_int, &q_int); + if gcd != BigInt::one() { + return None; + } + + let fourth_root_p_int = BigInt::from_biguint(Sign::Plus, fourth_root_p); + let fourth_root_q_int = BigInt::from_biguint(Sign::Plus, fourth_root_q); + + let result = (fourth_root_p_int.clone() * &v * &q_int + fourth_root_q_int * &u * &p_int) % BigInt::from_biguint(Sign::Plus, n.clone()); + + let result_pos = if result.sign() == Sign::Minus { + result + BigInt::from_biguint(Sign::Plus, n.clone()) + } else { + result + }; + + Some(result_pos.to_biguint().unwrap()) +} + +/// Wrapper for BigUint with proper domain separation for Inscribe +struct BigUintWrapper(BigUint); + +impl Inscribe for BigUintWrapper { + const MARK: &'static str = "BigUint"; + + fn get_inscription(&self) -> Result { + let bytes = self.0.to_bytes_le(); + Ok(bytes) + } + + fn get_additional(&self) -> Result { + Ok(b"paillier-blum-proof-biguint".to_vec()) + } +} + +/// Wrapper for u32 with proper domain separation for Inscribe +struct U32Wrapper(u32); + +impl Inscribe for U32Wrapper { + const MARK: &'static str = "u32"; + + fn get_inscription(&self) -> Result { + Ok(self.0.to_le_bytes().to_vec()) + } + + fn get_additional(&self) -> Result { + Ok(b"paillier-blum-proof-u32".to_vec()) + } +} + +/// Single round response in the Paillier-Blum proof +#[derive(Clone)] +struct RoundResponse { + x: BigUint, // 4th root: x_i = ∜y'_i mod N + a: bool, // a_i ∈ {0, 1} + b: bool, // b_i ∈ {0, 1} + z: BigUint, // z_i = y_i^{N^{-1} mod φ(N)} mod N +} + +impl Inscribe for RoundResponse { + const MARK: &'static str = "RoundResponse"; + + fn get_inscription(&self) -> Result { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.x.to_bytes_le()); + bytes.push(if self.a { 1u8 } else { 0u8 }); + bytes.push(if self.b { 1u8 } else { 0u8 }); + bytes.extend_from_slice(&self.z.to_bytes_le()); + Ok(bytes) + } + + fn get_additional(&self) -> Result { + Ok(b"paillier-blum-proof-round-response".to_vec()) + } +} + +/// Complete Paillier-Blum modulus proof +#[derive(Inscribe)] +struct PaillierBlumProof { + w: BigUintWrapper, // Element with Jacobi symbol -1 + #[inscribe(skip)] + responses: Vec, // Responses for each challenge round +} + +/// Prove that N is a Paillier-Blum modulus +/// +/// # Arguments +/// * `n` - The modulus to prove (N = pq) +/// * `p` - First prime factor (p ≡ 3 mod 4) +/// * `q` - Second prime factor (q ≡ 3 mod 4) +/// * `m` - Number of challenge rounds (must be >= MIN_CHALLENGE_ROUNDS) +/// +/// # Returns +/// A proof that N is a Paillier-Blum modulus, or an error +fn prove(n: &BigUint, p: &BigUint, q: &BigUint, m: u32) -> Result { + // Validate parameters + if m < MIN_CHALLENGE_ROUNDS { + return Err(Error::new_general("Number of challenge rounds must be at least 80")); + } + + // Verify N = pq + if n != &(p * q) { + return Err(Error::new_general("N must equal p * q")); + } + + // Verify p ≠ q (N must be product of two DISTINCT primes) + if p == q { + return Err(Error::new_general("p and q must be distinct (N cannot be p²)")); + } + + // Verify p, q are odd + if !p.bit(0) || !q.bit(0) { + return Err(Error::new_general("p and q must be odd")); + } + + // Verify p, q ≡ 3 (mod 4) + if p % BigUint::from(4u32) != BigUint::from(3u32) || q % BigUint::from(4u32) != BigUint::from(3u32) { + return Err(Error::new_general("p and q must be ≡ 3 (mod 4)")); + } + + let mut rng = OsRng; + + // Step 1: Sample w ∈ Z*_N with Jacobi symbol -1 + let w = loop { + let candidate = rng.gen_biguint_range(&BigUint::from(2u32), n); + let candidate_int = BigInt::from_biguint(Sign::Plus, candidate.clone()); + let n_int = BigInt::from_biguint(Sign::Plus, n.clone()); + + // Check gcd(w, N) = 1 + let (gcd, _, _) = extended_gcd(&candidate_int, &n_int); + if gcd != BigInt::one() { + continue; + } + + // Check Jacobi symbol is -1 + if jacobi_symbol(&candidate_int, &n_int) == -1 { + break candidate; + } + }; + + // Initialize Fiat-Shamir transcript + let inputs = ["n", "w", "m"]; + let challenges = ["y_challenges"]; + let mut transcript = Decree::new("paillier-blum-proof-v1", &inputs, &challenges) + .map_err(|_| Error::new_general("Failed to create transcript"))?; + + // Add public inputs to transcript + transcript.add("n", &BigUintWrapper(n.clone()))?; + transcript.add("w", &BigUintWrapper(w.clone()))?; + transcript.add("m", &U32Wrapper(m))?; + + // Get challenges from transcript + let mut challenge_bytes = vec![0u8; (m as usize) * 32]; // 32 bytes per challenge + transcript.get_challenge("y_challenges", &mut challenge_bytes)?; + + // Generate y_i challenges from transcript + let mut y_challenges = Vec::new(); + for i in 0..m { + let start = (i as usize) * 32; + let end = start + 32; + let y_bytes = &challenge_bytes[start..end]; + + // Convert bytes to BigUint and reduce mod N + // Ensure it's non-zero by adding 1 if needed + let mut y = BigUint::from_bytes_le(y_bytes) % n; + if y.is_zero() { + y = BigUint::one(); + } + + y_challenges.push(y); + } + + // Compute φ(N) = (p-1)(q-1) + let phi_n = (p - BigUint::one()) * (q - BigUint::one()); + + // Compute N^{-1} mod φ(N) + let n_int = BigInt::from_biguint(Sign::Plus, n.clone()); + let phi_n_int = BigInt::from_biguint(Sign::Plus, phi_n.clone()); + let (gcd, n_inv, _) = extended_gcd(&n_int, &phi_n_int); + + if gcd != BigInt::one() { + return Err(Error::new_general("N and φ(N) are not coprime")); + } + + let n_inv_pos = if n_inv.sign() == Sign::Minus { + n_inv + &phi_n_int + } else { + n_inv + }; + let n_inv_uint = n_inv_pos.to_biguint().unwrap(); + + // Process each challenge + let mut responses = Vec::new(); + + for y_i in &y_challenges { + // Compute z_i = y_i^{N^{-1} mod φ(N)} mod N + let z_i = y_i.modpow(&n_inv_uint, n); + + // Find a_i, b_i such that y'_i = (-1)^{a_i} * w^{b_i} * y_i has a 4th root mod N + let mut found = false; + let mut x_i = BigUint::zero(); + let mut a_i = false; + let mut b_i = false; + + // Try all 4 combinations of (a_i, b_i) + for a in [false, true] { + for b in [false, true] { + // Compute y'_i = (-1)^a * w^b * y_i mod N + let mut y_prime = y_i.clone(); + + if a { + // Multiply by -1 mod N + y_prime = (n - y_prime) % n; + } + + if b { + // Multiply by w + y_prime = (y_prime * &w) % n; + } + + // Try to compute 4th root + if let Some(x) = fourth_root_mod_paillier_blum(&y_prime, p, q, n) { + // Verify it's actually a 4th root + if x.modpow(&BigUint::from(4u32), n) == y_prime { + x_i = x; + a_i = a; + b_i = b; + found = true; + break; + } + } + } + if found { + break; + } + } + + if !found { + return Err(Error::new_general("Could not find 4th root for challenge")); + } + + responses.push(RoundResponse { + x: x_i, + a: a_i, + b: b_i, + z: z_i, + }); + } + + Ok(PaillierBlumProof { + w: BigUintWrapper(w), + responses, + }) +} + +/// Verify a Paillier-Blum modulus proof +/// +/// # Arguments +/// * `n` - The modulus being proven +/// * `proof` - The proof to verify +/// * `m` - Number of challenge rounds (must match what was used in prove) +/// +/// # Returns +/// True if the proof is valid, false otherwise +fn verify(n: &BigUint, proof: &PaillierBlumProof, m: u32) -> Result { + // Validate parameters + if m < MIN_CHALLENGE_ROUNDS { + return Err(Error::new_general("Number of challenge rounds must be at least 80")); + } + + if proof.responses.len() != m as usize { + return Ok(false); + } + + // Check N is odd composite + if !n.bit(0) { + return Ok(false); + } + + if n < &BigUint::from(4u32) { + return Ok(false); + } + + // CRITICAL: Check N is composite (not prime) + // This is required by Figure 12: "N is an odd composite number" + if is_probably_prime(n, 20) { + return Ok(false); + } + + // Check gcd(w, N) = 1 + let w_int = BigInt::from_biguint(Sign::Plus, proof.w.0.clone()); + let n_int = BigInt::from_biguint(Sign::Plus, n.clone()); + let (gcd, _, _) = extended_gcd(&w_int, &n_int); + + if gcd != BigInt::one() { + return Ok(false); + } + + // CRITICAL: Check w has Jacobi symbol -1 + // This is required by Figure 12 protocol step 1 + if jacobi_symbol(&w_int, &n_int) != -1 { + return Ok(false); + } + + // Reconstruct transcript to get challenges + let inputs = ["n", "w", "m"]; + let challenges = ["y_challenges"]; + let mut transcript = Decree::new("paillier-blum-proof-v1", &inputs, &challenges) + .map_err(|_| Error::new_general("Failed to create transcript"))?; + + transcript.add("n", &BigUintWrapper(n.clone()))?; + transcript.add("w", &BigUintWrapper(proof.w.0.clone()))?; + transcript.add("m", &U32Wrapper(m))?; + + let mut challenge_bytes = vec![0u8; (m as usize) * 32]; + transcript.get_challenge("y_challenges", &mut challenge_bytes)?; + + // Regenerate y_i challenges + let mut y_challenges = Vec::new(); + for i in 0..m { + let start = (i as usize) * 32; + let end = start + 32; + let y_bytes = &challenge_bytes[start..end]; + + // Convert bytes to BigUint and reduce mod N + // Ensure it's non-zero by adding 1 if needed + let mut y = BigUint::from_bytes_le(y_bytes) % n; + if y.is_zero() { + y = BigUint::one(); + } + + y_challenges.push(y); + } + + // Verify each round + for (y_i, response) in y_challenges.iter().zip(proof.responses.iter()) { + // Check a_i, b_i ∈ {0, 1} (implicitly true since they're bool) + + // Check y_i ∈ Z*_N + let y_i_int = BigInt::from_biguint(Sign::Plus, y_i.clone()); + let (gcd, _, _) = extended_gcd(&y_i_int, &n_int); + if gcd != BigInt::one() { + return Ok(false); + } + + // Check z_i^N = y_i mod N + let z_i_to_n = response.z.modpow(n, n); + if z_i_to_n != *y_i { + return Ok(false); + } + + // Check x_i^4 = (-1)^{a_i} * w^{b_i} * y_i mod N + let x_i_fourth = response.x.modpow(&BigUint::from(4u32), n); + + let mut expected = y_i.clone(); + if response.a { + expected = (n - expected) % n; + } + if response.b { + expected = (expected * &proof.w.0) % n; + } + + if x_i_fourth != expected { + return Ok(false); + } + } + + Ok(true) +} + +fn main() { + // This example demonstrates the Paillier-Blum modulus proof in the tests below + println!("Run `cargo test --example paillier_blum_proof` to see the proof in action"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_safe_prime_generation() { + let p = generate_safe_prime(128); + + // Check it's prime (probabilistically) + assert!(is_probably_prime(&p, 20)); + + // Check p ≡ 3 (mod 4) + assert_eq!(&p % BigUint::from(4u32), BigUint::from(3u32)); + + // CRITICAL: Check safe prime property: p = 2q + 1 where q is prime + assert!(&p > &BigUint::one(), "p must be > 1"); + let q = (&p - BigUint::one()) / BigUint::from(2u32); + + // Verify q is prime + assert!(is_probably_prime(&q, 20), "q = (p-1)/2 must be prime for safe prime p = 2q + 1"); + + // Verify p = 2q + 1 + assert_eq!(&q * BigUint::from(2u32) + BigUint::one(), p, "p must equal 2q + 1"); + } + + #[test] + fn test_paillier_blum_modulus_generation() { + let (n, p, q) = generate_paillier_blum_modulus(256); + + // Check N = pq + assert_eq!(n, &p * &q); + + // Check p, q are prime + assert!(is_probably_prime(&p, 20)); + assert!(is_probably_prime(&q, 20)); + + // Check p, q ≡ 3 (mod 4) + assert_eq!(p % BigUint::from(4u32), BigUint::from(3u32)); + assert_eq!(q % BigUint::from(4u32), BigUint::from(3u32)); + } + + #[test] + fn test_jacobi_symbol() { + // Test known values + let n = BigInt::from(15); // 15 = 3 * 5 + + assert_eq!(jacobi_symbol(&BigInt::from(1), &n), 1); + assert_eq!(jacobi_symbol(&BigInt::from(2), &n), 1); + assert_eq!(jacobi_symbol(&BigInt::from(4), &n), 1); + assert_eq!(jacobi_symbol(&BigInt::from(8), &n), 1); + } + + #[test] + fn test_valid_paillier_blum_proof() { + // Generate a Paillier-Blum modulus + let (n, p, q) = generate_paillier_blum_modulus(512); + + // Generate proof with minimum rounds + let proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Verify proof + assert!(verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap()); + } + + #[test] + fn test_valid_paillier_blum_proof_many_rounds() { + // Generate a Paillier-Blum modulus + let (n, p, q) = generate_paillier_blum_modulus(512); + + // Generate proof with more rounds + let m = 128; + let proof = prove(&n, &p, &q, m).unwrap(); + + // Verify proof + assert!(verify(&n, &proof, m).unwrap()); + } + + #[test] + fn test_proof_fails_with_wrong_m() { + // Generate a Paillier-Blum modulus + let (n, p, q) = generate_paillier_blum_modulus(512); + + // Generate proof with one value of m + let proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Try to verify with different m + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS + 10).unwrap()); + } + + #[test] + fn test_proof_fails_with_insufficient_rounds() { + // Generate a Paillier-Blum modulus + let (n, p, q) = generate_paillier_blum_modulus(512); + + // Try to generate proof with too few rounds + let result = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS - 1); + assert!(result.is_err()); + } + + #[test] + fn test_proof_fails_with_non_paillier_blum_modulus() { + // Generate primes that are NOT ≡ 3 (mod 4) + let mut rng = OsRng; + + // Generate p ≡ 1 (mod 4) + let p = loop { + let mut candidate = rng.gen_biguint(256); + candidate.set_bit(255, true); + candidate.set_bit(0, true); + candidate.set_bit(1, true); // ensures last two bits are "11" but we want "01" for ≡ 1 (mod 4) + + // Actually, let me fix this - for ≡ 1 (mod 4), bits should be "01" + let mut candidate = rng.gen_biguint(256); + candidate.set_bit(255, true); + candidate.set_bit(0, true); // odd + candidate.set_bit(1, false); // bit pattern "01" = 1 mod 4 + + if is_probably_prime(&candidate, 20) && candidate.clone() % BigUint::from(4u32) == BigUint::from(1u32) { + break candidate; + } + }; + + let q = generate_safe_prime(256); // This is ≡ 3 (mod 4) + let n = &p * &q; + + // Try to generate proof - should fail + let result = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS); + assert!(result.is_err()); + } + + #[test] + fn test_verify_rejects_even_modulus() { + let (n, p, q) = generate_paillier_blum_modulus(512); + let proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Try to verify with even modulus + let even_n = n + BigUint::one(); + assert!(!verify(&even_n, &proof, MIN_CHALLENGE_ROUNDS).unwrap()); + } + + #[test] + fn test_fourth_root_extraction() { + // Generate a Paillier-Blum modulus for testing + let (n, p, q) = generate_paillier_blum_modulus(256); + + // Pick a random element and compute its 4th power + let mut rng = OsRng; + let x = rng.gen_biguint_range(&BigUint::from(2u32), &n); + let x_fourth = x.modpow(&BigUint::from(4u32), &n); + + // Not every element has a 4th root, so we try multiple combinations + // like the proof does with the 4 options for (a_i, b_i) + let mut found_root = false; + + // Try to extract 4th root directly + if let Some(root) = fourth_root_mod_paillier_blum(&x_fourth, &p, &q, &n) { + // Verify it's a 4th root + let check = root.modpow(&BigUint::from(4u32), &n); + if check == x_fourth { + found_root = true; + } + } + + // For this test, since x_fourth = x^4, it definitely has a 4th root + // If our algorithm didn't find it, that's a bug + assert!(found_root, "Should have found a 4th root of x^4"); + } + + #[test] + fn test_w_has_jacobi_symbol_minus_one() { + // Generate a Paillier-Blum modulus and proof + let (n, p, q) = generate_paillier_blum_modulus(512); + let proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL: Verify w has Jacobi symbol -1 + let w_int = BigInt::from_biguint(Sign::Plus, proof.w.0.clone()); + let n_int = BigInt::from_biguint(Sign::Plus, n.clone()); + + assert_eq!(jacobi_symbol(&w_int, &n_int), -1, + "w must have Jacobi symbol -1 for the proof to be sound"); + } + + #[test] + fn test_verifier_rejects_wrong_jacobi_symbol() { + // Generate a Paillier-Blum modulus + let (n, p, q) = generate_paillier_blum_modulus(512); + + // Create a valid proof + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL SOUNDNESS TEST: Replace w with an element that has Jacobi symbol +1 + // NOTE: This changes the Fiat-Shamir challenges, so the proof becomes invalid + // for TWO reasons: (1) wrong Jacobi symbol, (2) responses don't match new challenges + // But the verifier MUST check Jacobi symbol explicitly, not rely on mismatch + let n_int = BigInt::from_biguint(Sign::Plus, n.clone()); + let mut rng = OsRng; + + let bad_w = loop { + let candidate = rng.gen_biguint_range(&BigUint::from(2u32), &n); + let candidate_int = BigInt::from_biguint(Sign::Plus, candidate.clone()); + + // Check gcd(candidate, N) = 1 + let (gcd, _, _) = extended_gcd(&candidate_int, &n_int); + if gcd != BigInt::one() { + continue; + } + + // We want Jacobi symbol +1 + if jacobi_symbol(&candidate_int, &n_int) == 1 { + break candidate; + } + }; + + // Replace w in the proof + proof.w = BigUintWrapper(bad_w); + + // Verifier should reject this proof + // With the fixed verifier, this should be caught at the Jacobi symbol check + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject proof with w having wrong Jacobi symbol"); + } + + #[test] + fn test_verifier_rejects_wrong_z_i() { + // Generate a Paillier-Blum modulus and valid proof + let (n, p, q) = generate_paillier_blum_modulus(512); + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL SOUNDNESS TEST: Corrupt one of the z_i values + if !proof.responses.is_empty() { + let n_uint = n.clone(); + proof.responses[0].z = (&proof.responses[0].z + BigUint::one()) % n_uint; + + // Verifier should reject + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject proof with corrupted z_i"); + } + } + + #[test] + fn test_verifier_rejects_wrong_x_i() { + // Generate a Paillier-Blum modulus and valid proof + let (n, p, q) = generate_paillier_blum_modulus(512); + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL SOUNDNESS TEST: Corrupt one of the x_i values + if !proof.responses.is_empty() { + let n_uint = n.clone(); + proof.responses[0].x = (&proof.responses[0].x + BigUint::one()) % n_uint; + + // Verifier should reject + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject proof with corrupted x_i"); + } + } + + #[test] + fn test_verifier_rejects_flipped_bits() { + // Generate a Paillier-Blum modulus and valid proof + let (n, p, q) = generate_paillier_blum_modulus(512); + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL SOUNDNESS TEST: Flip the bits a_i or b_i + if !proof.responses.is_empty() { + proof.responses[0].a = !proof.responses[0].a; + + // Verifier should reject + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject proof with flipped a_i bit"); + } + } + + #[test] + fn test_verifier_rejects_non_paillier_blum_modulus() { + // Generate a NON-Paillier-Blum modulus (product of primes ≡ 1 mod 4) + let mut rng = OsRng; + + // Generate p ≡ 1 (mod 4) + let p = loop { + let mut candidate = rng.gen_biguint(256); + candidate.set_bit(255, true); + candidate.set_bit(0, true); // odd + candidate.set_bit(1, false); // bit pattern "01" = 1 mod 4 + + if is_probably_prime(&candidate, 20) && candidate.clone() % BigUint::from(4u32) == BigUint::from(1u32) { + break candidate; + } + }; + + // Generate another p ≡ 1 (mod 4) + let q = loop { + let mut candidate = rng.gen_biguint(256); + candidate.set_bit(255, true); + candidate.set_bit(0, true); + candidate.set_bit(1, false); + + if is_probably_prime(&candidate, 20) && candidate.clone() % BigUint::from(4u32) == BigUint::from(1u32) { + break candidate; + } + }; + + let bad_n = &p * &q; + + // Generate a valid Paillier-Blum modulus and proof + let (good_n, good_p, good_q) = generate_paillier_blum_modulus(512); + let proof = prove(&good_n, &good_p, &good_q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL SOUNDNESS TEST: Try to verify proof against wrong modulus + // This should fail because w won't have the right Jacobi symbol, and + // the challenges will be different + assert!(!verify(&bad_n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject proof when used with non-Paillier-Blum modulus"); + } + + #[test] + fn test_verifier_rejects_composite_non_biprime() { + // Generate a Paillier-Blum modulus and valid proof + let (n, p, q) = generate_paillier_blum_modulus(512); + let proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL SOUNDNESS TEST: Try to verify against N^2 (not a biprime) + let n_squared = &n * &n; + + // This should fail + let result = verify(&n_squared, &proof, MIN_CHALLENGE_ROUNDS).unwrap(); + assert!(!result, "Verifier must reject proof for non-biprime modulus"); + } + + #[test] + fn test_z_i_satisfies_equation() { + // Generate a Paillier-Blum modulus and proof + let (n, p, q) = generate_paillier_blum_modulus(512); + let proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Reconstruct challenges + let inputs = ["n", "w", "m"]; + let challenges = ["y_challenges"]; + let mut transcript = Decree::new("paillier-blum-proof-v1", &inputs, &challenges).unwrap(); + transcript.add("n", &BigUintWrapper(n.clone())).unwrap(); + transcript.add("w", &BigUintWrapper(proof.w.0.clone())).unwrap(); + transcript.add("m", &U32Wrapper(MIN_CHALLENGE_ROUNDS)).unwrap(); + + let mut challenge_bytes = vec![0u8; (MIN_CHALLENGE_ROUNDS as usize) * 32]; + transcript.get_challenge("y_challenges", &mut challenge_bytes).unwrap(); + + let mut y_challenges = Vec::new(); + for _i in 0..MIN_CHALLENGE_ROUNDS { + let start = (_i as usize) * 32; + let end = start + 32; + let y_bytes = &challenge_bytes[start..end]; + + let mut y = BigUint::from_bytes_le(y_bytes) % &n; + if y.is_zero() { + y = BigUint::one(); + } + y_challenges.push(y); + } + + // CRITICAL: Verify z_i^N = y_i for all i + for (y_i, response) in y_challenges.iter().zip(proof.responses.iter()) { + let z_i_to_n = response.z.modpow(&n, &n); + assert_eq!(z_i_to_n, *y_i, + "z_i^N must equal y_i mod N (this proves prover knows factorization)"); + } + } + + #[test] + fn test_verifier_rejects_prime_n() { + // CRITICAL SOUNDNESS TEST: Verifier must reject prime N + // Generate a large prime + let prime_n = loop { + let mut rng = OsRng; + let mut candidate = rng.gen_biguint(512); + candidate.set_bit(511, true); + candidate.set_bit(0, true); + + if is_probably_prime(&candidate, 20) { + break candidate; + } + }; + + // Generate a valid proof for a real Paillier-Blum modulus + let (good_n, p, q) = generate_paillier_blum_modulus(512); + let proof = prove(&good_n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Try to verify the proof against the prime N + // This MUST fail because N is not composite + assert!(!verify(&prime_n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject prime N (violates 'odd composite' requirement)"); + } + + #[test] + fn test_verifier_rejects_n_equals_p_squared() { + // CRITICAL SOUNDNESS TEST: N = p² should be rejected + // This tests that N must be a product of two DISTINCT primes + let p = generate_safe_prime(256); + let n_squared = &p * &p; + + // Generate a valid proof for a real Paillier-Blum modulus + let (good_n, good_p, good_q) = generate_paillier_blum_modulus(512); + let proof = prove(&good_n, &good_p, &good_q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Try to verify against N = p² + // The verifier should reject this (challenges will differ due to different N in transcript) + assert!(!verify(&n_squared, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject N = p² (not a biprime)"); + } + + #[test] + fn test_prover_rejects_n_equals_p_squared() { + // Prover should reject N = p² at prove time + let p = generate_safe_prime(256); + let n_squared = &p * &p; + + // Try to create proof with N = p² + let result = prove(&n_squared, &p, &p, MIN_CHALLENGE_ROUNDS); + + // Should fail because prover validates N = p*q + assert!(result.is_err(), "Prover must reject N = p²"); + } + + #[test] + fn test_jacobi_symbol_comprehensive() { + // More comprehensive Jacobi symbol tests + // Test with n = 15 = 3 * 5 + let n = BigInt::from(15); + + // Test known values + assert_eq!(jacobi_symbol(&BigInt::from(1), &n), 1); + assert_eq!(jacobi_symbol(&BigInt::from(2), &n), 1); + assert_eq!(jacobi_symbol(&BigInt::from(4), &n), 1); + + // CRITICAL: Test Jacobi symbol = -1 + assert_eq!(jacobi_symbol(&BigInt::from(7), &n), -1); + assert_eq!(jacobi_symbol(&BigInt::from(13), &n), -1); + + // Test Jacobi symbol = 0 (not coprime) + assert_eq!(jacobi_symbol(&BigInt::from(3), &n), 0); + assert_eq!(jacobi_symbol(&BigInt::from(5), &n), 0); + assert_eq!(jacobi_symbol(&BigInt::from(15), &n), 0); + } + + #[test] + fn test_small_paillier_blum_proof() { + // Test with small known values: N = 15 = 3 * 5 + // Both 3 and 5 are ≡ 3 (mod 4), so this is a valid Paillier-Blum modulus + let p = BigUint::from(3u32); + let q = BigUint::from(5u32); + let n = BigUint::from(15u32); + + // Try to create and verify proof + // This tests the protocol works on small values + let result = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS); + + if let Ok(proof) = result { + // If prover succeeds, verify should also succeed + assert!(verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Valid proof for N=15 should verify"); + + // Verify w has Jacobi symbol -1 + let w_int = BigInt::from_biguint(Sign::Plus, proof.w.0.clone()); + let n_int = BigInt::from_biguint(Sign::Plus, n.clone()); + assert_eq!(jacobi_symbol(&w_int, &n_int), -1, + "w must have Jacobi symbol -1"); + } else { + // If prover fails, that's also acceptable for small moduli + // (might not find 4th roots easily) + println!("Note: Proof generation failed for N=15 (acceptable for small moduli)"); + } + } + + #[test] + fn test_verifier_rejects_gcd_w_n_not_one() { + // Generate a Paillier-Blum modulus + let (n, p, q) = generate_paillier_blum_modulus(512); + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // CRITICAL: Replace w with a value that shares a factor with N + // Use p as w (which divides N) + proof.w = BigUintWrapper(p.clone()); + + // Verifier must reject because gcd(w, N) ≠ 1 + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject when gcd(w, N) ≠ 1"); + } + + #[test] + fn test_all_responses_checked() { + // Test that corruption in ANY position is detected, not just first + let (n, p, q) = generate_paillier_blum_modulus(512); + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Test corrupting middle response + let middle = proof.responses.len() / 2; + proof.responses[middle].z = (&proof.responses[middle].z + BigUint::one()) % &n; + + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must check ALL responses, not just first"); + } + + #[test] + fn test_b_i_bit_corruption() { + // Test that flipping b_i bit is also detected + let (n, p, q) = generate_paillier_blum_modulus(512); + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Flip b_i instead of a_i + proof.responses[0].b = !proof.responses[0].b; + + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject proof with flipped b_i bit"); + } + + #[test] + fn test_both_bits_flipped() { + // Test that flipping both a_i and b_i is detected + let (n, p, q) = generate_paillier_blum_modulus(512); + let mut proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Flip both bits + proof.responses[0].a = !proof.responses[0].a; + proof.responses[0].b = !proof.responses[0].b; + + assert!(!verify(&n, &proof, MIN_CHALLENGE_ROUNDS).unwrap(), + "Verifier must reject proof with both bits flipped"); + } + + #[test] + fn test_x_i_satisfies_equation() { + // Generate a Paillier-Blum modulus and proof + let (n, p, q) = generate_paillier_blum_modulus(512); + let proof = prove(&n, &p, &q, MIN_CHALLENGE_ROUNDS).unwrap(); + + // Reconstruct challenges + let inputs = ["n", "w", "m"]; + let challenges = ["y_challenges"]; + let mut transcript = Decree::new("paillier-blum-proof-v1", &inputs, &challenges).unwrap(); + transcript.add("n", &BigUintWrapper(n.clone())).unwrap(); + transcript.add("w", &BigUintWrapper(proof.w.0.clone())).unwrap(); + transcript.add("m", &U32Wrapper(MIN_CHALLENGE_ROUNDS)).unwrap(); + + let mut challenge_bytes = vec![0u8; (MIN_CHALLENGE_ROUNDS as usize) * 32]; + transcript.get_challenge("y_challenges", &mut challenge_bytes).unwrap(); + + let mut y_challenges = Vec::new(); + for _i in 0..MIN_CHALLENGE_ROUNDS { + let start = (_i as usize) * 32; + let end = start + 32; + let y_bytes = &challenge_bytes[start..end]; + + let mut y = BigUint::from_bytes_le(y_bytes) % &n; + if y.is_zero() { + y = BigUint::one(); + } + y_challenges.push(y); + } + + // CRITICAL: Verify x_i^4 = (-1)^{a_i} * w^{b_i} * y_i for all i + for (y_i, response) in y_challenges.iter().zip(proof.responses.iter()) { + let x_i_fourth = response.x.modpow(&BigUint::from(4u32), &n); + + let mut expected = y_i.clone(); + if response.a { + expected = (&n - expected) % &n; + } + if response.b { + expected = (expected * &proof.w.0) % &n; + } + + assert_eq!(x_i_fourth, expected, + "x_i^4 must equal (-1)^a_i * w^b_i * y_i mod N"); + } + } +} diff --git a/examples/small_factor_proof.rs b/examples/small_factor_proof.rs index 13c8928..118246f 100644 --- a/examples/small_factor_proof.rs +++ b/examples/small_factor_proof.rs @@ -13,80 +13,19 @@ #![deny(warnings)] #![cfg_attr(not(test), allow(dead_code))] +mod crypto_helpers; + use decree::error::Error; use decree::Inscribe; use decree::decree::{Decree, FSInput}; +use crypto_helpers::{extended_gcd, generate_prime}; use num_bigint::{BigInt, BigUint, RandBigInt, Sign}; use num_traits::{One, Zero}; use rand::rngs::OsRng; use rand::Rng; -/// Security parameter for statistical distance -const EPSILON: u32 = 128; - -/// Miller-Rabin primality test -fn is_probably_prime(n: &BigUint, rounds: u32) -> bool { - if n < &BigUint::from(2u32) { - return false; - } - if n == &BigUint::from(2u32) || n == &BigUint::from(3u32) { - return true; - } - if !n.bit(0) { - return false; // even number - } - - // Write n-1 as 2^r · d - let n_minus_1 = n - BigUint::one(); - let mut d = n_minus_1.clone(); - let mut r = 0u32; - while !d.bit(0) { - d >>= 1; - r += 1; - } - - let mut rng = OsRng; - - 'witness: for _ in 0..rounds { - let a = rng.gen_biguint_range(&BigUint::from(2u32), &(n - BigUint::from(2u32))); - let mut x = a.modpow(&d, n); - - if x == BigUint::one() || x == n_minus_1 { - continue 'witness; - } - - for _ in 0..(r - 1) { - x = x.modpow(&BigUint::from(2u32), n); - if x == n_minus_1 { - continue 'witness; - } - } - - return false; - } - - true -} - -/// Generate a random prime of specified bit length -fn generate_prime(bits: usize) -> BigUint { - let mut rng = OsRng; - loop { - // Generate random odd number of specified bit length - let mut candidate = rng.gen_biguint(bits as u64); - - // Set high bit to ensure correct bit length - candidate.set_bit(bits as u64 - 1, true); - - // Make it odd - candidate.set_bit(0, true); - - // Test for primality - if is_probably_prime(&candidate, 20) { - return candidate; - } - } -} +/// Statistical hiding parameter for small-factor proof +const SMALL_FACTOR_EPSILON: u32 = 524; /// Wrapper for BigInt with proper domain separation for Inscribe struct BigIntWrapper(BigInt); @@ -199,19 +138,6 @@ fn modpow_signed(base: &BigUint, exp: &BigInt, modulus: &BigUint) -> BigUint { } } -/// Extended GCD algorithm -fn extended_gcd(a: &BigInt, b: &BigInt) -> (BigInt, BigInt, BigInt) { - if b.is_zero() { - return (a.clone(), BigInt::one(), BigInt::zero()); - } - - let (gcd, x1, y1) = extended_gcd(b, &(a % b)); - let x = y1.clone(); - let y = x1 - (a / b) * y1; - - (gcd, x, y) -} - /// Sample a signed BigInt uniformly from [-bound, bound] fn sample_pm_range(bound: &BigUint) -> BigInt { let mut rng = OsRng; @@ -264,7 +190,7 @@ fn prove( // Compute bounds for sampling let sqrt_n0 = isqrt_ceil(n0); let two_ell = BigUint::from(2u32).pow(ell); - let two_ell_eps = BigUint::from(2u32).pow(ell + EPSILON); + let two_ell_eps = BigUint::from(2u32).pow(ell + SMALL_FACTOR_EPSILON); let bound_alpha = &two_ell_eps * &sqrt_n0; // ±2^(ℓ+ε) · √N₀ let bound_mu = &two_ell * ¶ms.n_tilde; // ±2^ℓ · Ñ let bound_r = &two_ell_eps * n0 * ¶ms.n_tilde; // ±2^(ℓ+ε) · N₀ · Ñ @@ -323,10 +249,11 @@ fn prove( transcript.add("commitment", &commitment).unwrap(); // Get challenge e ← ±2^ℓ - let mut challenge_bytes = vec![0u8; (ell as usize).div_ceil(8)]; + let mut challenge_bytes = vec![0u8; (ell as usize).div_ceil(8) + 1]; transcript.get_challenge("challenge", &mut challenge_bytes).unwrap(); - let e_magnitude = BigUint::from_bytes_le(&challenge_bytes) % BigUint::from(2u32).pow(ell); - let e = BigInt::from_biguint(Sign::Plus, e_magnitude); + let e_magnitude = BigUint::from_bytes_le(&challenge_bytes[..challenge_bytes.len()-1]) % BigUint::from(2u32).pow(ell); + let sign = if challenge_bytes[challenge_bytes.len()-1] & 1 == 0 { Sign::Plus } else { Sign::Minus }; + let e = BigInt::from_biguint(sign, e_magnitude); // Compute responses let z1 = &alpha + (&e * BigInt::from_biguint(Sign::Plus, p.clone())); @@ -355,7 +282,23 @@ fn verify( proof: &SmallFactorProof, n0: &BigUint, ell: u32, -) -> bool { +) -> Result<(), Error> { + // Check protocol preconditions + // Requirement 1: N₀ > 2^4ℓ + let four_ell = 4 * ell; + if n0.bits() as u32 <= four_ell { + return Err(Error::new_general("N₀ must be > 2^4ℓ")); + } + + // Requirement 2: 2^(ℓ+ε) ≈ √N₀ + // Check that |2^(ℓ+ε) - √N₀| is reasonable (within factor of 2) + let sqrt_n0 = isqrt_ceil(n0); + let sqrt_n0_bits = sqrt_n0.bits() as u32; + let ell_plus_epsilon = ell + SMALL_FACTOR_EPSILON; + if sqrt_n0_bits < ell_plus_epsilon.saturating_sub(1) || sqrt_n0_bits > ell_plus_epsilon + 1 { + return Err(Error::new_general("2^(ℓ+ε) must approximate √N₀")); + } + // Recreate challenge from commitment let mut transcript = Decree::new( "small-factor-proof", @@ -364,20 +307,21 @@ fn verify( ).unwrap(); transcript.add("commitment", &proof.commitment).unwrap(); - let mut challenge_bytes = vec![0u8; (ell as usize).div_ceil(8)]; + let mut challenge_bytes = vec![0u8; (ell as usize).div_ceil(8) + 1]; transcript.get_challenge("challenge", &mut challenge_bytes).unwrap(); - let e_magnitude = BigUint::from_bytes_le(&challenge_bytes) % BigUint::from(2u32).pow(ell); - let e = BigInt::from_biguint(Sign::Plus, e_magnitude); + let e_magnitude = BigUint::from_bytes_le(&challenge_bytes[..challenge_bytes.len()-1]) % BigUint::from(2u32).pow(ell); + let sign = if challenge_bytes[challenge_bytes.len()-1] & 1 == 0 { Sign::Plus } else { Sign::Minus }; + let e = BigInt::from_biguint(sign, e_magnitude); // Range check: z₁, z₂ ∈ ±√N₀ · 2^(ℓ+ε) let sqrt_n0 = isqrt_ceil(n0); - let two_ell_eps = BigUint::from(2u32).pow(ell + EPSILON); + let two_ell_eps = BigUint::from(2u32).pow(ell + SMALL_FACTOR_EPSILON); let range_bound = &sqrt_n0 * &two_ell_eps; if proof.response.z1.0.magnitude() > &range_bound || proof.response.z2.0.magnitude() > &range_bound { - return false; + return Err(Error::new_general("Range check failed")); } // Equality checks @@ -394,7 +338,7 @@ fn verify( % ¶ms.n_tilde; if lhs1 != rhs1 { - return false; + return Err(Error::new_general("Equality check 1 failed")); } // Check 2: s^z₂ · t^w₂ = B · Q^e mod Ñ @@ -407,7 +351,7 @@ fn verify( % ¶ms.n_tilde; if lhs2 != rhs2 { - return false; + return Err(Error::new_general("Equality check 2 failed")); } // Check 3: Q^z₁ · t^v = T · R^e mod Ñ @@ -419,10 +363,10 @@ fn verify( % ¶ms.n_tilde; if lhs3 != rhs3 { - return false; + return Err(Error::new_general("Equality check 3 failed")); } - true + Ok(()) } fn main() { @@ -440,8 +384,9 @@ mod tests { let params = generate_pedersen_parameters(2048); // Generate RSA modulus with proper-sized factors - // Use ℓ = 1000 to ensure 1024-bit primes are above 2^ℓ - let ell = 1000; + // For 2048-bit N₀: need N₀ > 2^4ℓ and 2^(ℓ+524) ≈ 2^1024 + // So ℓ ≈ 500 + let ell = 500; let (n0, p, q) = generate_rsa_modulus(2048); // Verify p and q are above the threshold @@ -451,14 +396,15 @@ mod tests { // Generate and verify proof let proof = prove(¶ms, &n0, &p, &q, ell); - assert!(verify(¶ms, &proof, &n0, ell)); + assert!(verify(¶ms, &proof, &n0, ell).is_ok()); } #[test] fn test_proof_with_different_sizes() { // Test with 1536-bit modulus let params = generate_pedersen_parameters(1536); - let ell = 750; // Below 768 to ensure 768-bit primes exceed threshold + // For 1536-bit N₀: √N₀ ≈ 2^768, so ℓ + 524 ≈ 768, so ℓ ≈ 244 + let ell = 244; let (n0, p, q) = generate_rsa_modulus(1536); let threshold = BigUint::from(2u32).pow(ell); @@ -466,20 +412,20 @@ mod tests { assert!(q > threshold); let proof = prove(¶ms, &n0, &p, &q, ell); - assert!(verify(¶ms, &proof, &n0, ell)); + assert!(verify(¶ms, &proof, &n0, ell).is_ok()); } #[test] fn test_multiple_proofs() { // Generate parameters once let params = generate_pedersen_parameters(2048); - let ell = 1000; + let ell = 500; // Generate and verify multiple proofs for _ in 0..3 { let (n0, p, q) = generate_rsa_modulus(2048); let proof = prove(¶ms, &n0, &p, &q, ell); - assert!(verify(¶ms, &proof, &n0, ell)); + assert!(verify(¶ms, &proof, &n0, ell).is_ok()); } } @@ -487,19 +433,19 @@ mod tests { fn test_wrong_modulus() { // Generate proof for one modulus let params = generate_pedersen_parameters(2048); - let ell = 1000; + let ell = 500; let (n0, p, q) = generate_rsa_modulus(2048); let proof = prove(¶ms, &n0, &p, &q, ell); // Try to verify with different modulus let (n0_wrong, _, _) = generate_rsa_modulus(2048); - assert!(!verify(¶ms, &proof, &n0_wrong, ell)); + assert!(verify(¶ms, &proof, &n0_wrong, ell).is_err()); } #[test] fn test_modified_commitment() { let params = generate_pedersen_parameters(2048); - let ell = 1000; + let ell = 500; let (n0, p, q) = generate_rsa_modulus(2048); let mut proof = prove(¶ms, &n0, &p, &q, ell); @@ -507,13 +453,13 @@ mod tests { proof.commitment.a_commit.0 = (&proof.commitment.a_commit.0 + BigUint::one()) % ¶ms.n_tilde; // Verification should fail - assert!(!verify(¶ms, &proof, &n0, ell)); + assert!(verify(¶ms, &proof, &n0, ell).is_err()); } #[test] fn test_modified_response() { let params = generate_pedersen_parameters(2048); - let ell = 1000; + let ell = 500; let (n0, p, q) = generate_rsa_modulus(2048); let mut proof = prove(¶ms, &n0, &p, &q, ell); @@ -521,6 +467,32 @@ mod tests { proof.response.z1.0 = &proof.response.z1.0 + BigInt::one(); // Verification should fail - assert!(!verify(¶ms, &proof, &n0, ell)); + assert!(verify(¶ms, &proof, &n0, ell).is_err()); + } + + #[test] + fn test_invalid_ell_too_large() { + let params = generate_pedersen_parameters(2048); + let (n0, p, q) = generate_rsa_modulus(2048); + + // Use ℓ that violates N₀ > 2^4ℓ (2048 ≤ 4*512 = 2048) + let ell = 512; + let proof = prove(¶ms, &n0, &p, &q, ell); + + // Verification should fail parameter check + assert!(verify(¶ms, &proof, &n0, ell).is_err()); + } + + #[test] + fn test_invalid_ell_wrong_approximation() { + let params = generate_pedersen_parameters(2048); + let (n0, p, q) = generate_rsa_modulus(2048); + + // Use ℓ that violates 2^(ℓ+ε) ≈ √N₀ approximation + let ell = 300; // 2^(300+524) = 2^824, but √N₀ ≈ 2^1024 + let proof = prove(¶ms, &n0, &p, &q, ell); + + // Verification should fail approximation check + assert!(verify(¶ms, &proof, &n0, ell).is_err()); } }