From a4070611a541456dafa4f2eacc10ddc7edf4df4b Mon Sep 17 00:00:00 2001 From: Michael Lodder Date: Wed, 21 Aug 2024 11:22:38 -0600 Subject: [PATCH] add range proof Signed-off-by: Michael Lodder --- Cargo.toml | 9 +- src/decryptionkey.rs | 5 +- src/encryptionkey.rs | 22 +- src/error.rs | 9 + src/lib.rs | 17 +- src/proof.rs | 5 + src/proof/range.rs | 378 +++++++++++++++++++++ src/{proof_psf.rs => proof/square_free.rs} | 10 +- src/utils.rs | 7 + tests/paillier.rs | 65 +++- 10 files changed, 481 insertions(+), 46 deletions(-) create mode 100644 src/proof.rs create mode 100644 src/proof/range.rs rename src/{proof_psf.rs => proof/square_free.rs} (95%) create mode 100644 src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index c935333..80022c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,15 +18,17 @@ crypto = ["unknown_order/crypto"] gmp = ["unknown_order/gmp"] openssl = ["unknown_order/openssl"] rust = ["unknown_order/rust"] -wasm = ["getrandom", "rand", "wasm-bindgen", "serde-wasm-bindgen"] +wasm = ["getrandom", "wasm-bindgen", "serde-wasm-bindgen"] [dependencies] +bit-vec = "0.8" digest = "0.10" getrandom = { version = "0.2", features = ["js"], optional = true } -rand = { version = "0.8", optional = true } +rand = "0.8" postcard = { version = "1.0.9", features = ["use-std"] } serde = { version = "1.0", features = ["serde_derive"] } serde-wasm-bindgen = { version = "0.6", optional = true } +sha2 = "0.10" thiserror = "1.0" unknown_order = { version = "0.10", default-features = false } wasm-bindgen = { version = "0.2", default-features = false, features = ["serde-serialize"], optional = true } @@ -36,9 +38,8 @@ zeroize = { version = "1.8", features = ["zeroize_derive"] } elliptic-curve = "0.13" hex = "0.4" k256 = { version = "0.13", features = ["arithmetic"] } +rand_chacha = "0.3.1" wasm-bindgen-test = "0.3" -rand = "0.8" multibase = "0.9" -sha2 = "0.10" serde_json = "1.0" unicase = "2.6" diff --git a/src/decryptionkey.rs b/src/decryptionkey.rs index c0850ac..ec2946d 100644 --- a/src/decryptionkey.rs +++ b/src/decryptionkey.rs @@ -1,4 +1,4 @@ -use crate::{mod_in, Ciphertext, EncryptionKey, error::*}; +use crate::{error::*, mod_in, Ciphertext, EncryptionKey}; use serde::{Deserialize, Serialize}; use unknown_order::BigNumber; use zeroize::Zeroize; @@ -107,8 +107,7 @@ impl DecryptionKey { /// Convert a byte representation to a decryption key pub fn from_bytes>(data: B) -> PaillierResult { let data = data.as_ref(); - let bytes = - postcard::from_bytes::(data)?; + let bytes = postcard::from_bytes::(data)?; let pk = EncryptionKey::from_bytes(bytes.n.as_slice())?; Ok(Self { pk, diff --git a/src/encryptionkey.rs b/src/encryptionkey.rs index a54f444..418ec8a 100644 --- a/src/encryptionkey.rs +++ b/src/encryptionkey.rs @@ -4,7 +4,7 @@ use unknown_order::BigNumber; use zeroize::Zeroize; /// A Paillier encryption key -#[derive(Clone, Debug, Zeroize)] +#[derive(Clone, Debug, Default, Zeroize)] pub struct EncryptionKey { pub(crate) n: BigNumber, // N = p * q, where p,q are primes pub(crate) nn: BigNumber, // N^2 @@ -66,23 +66,31 @@ impl EncryptionKey { M: AsRef<[u8]>, { let xx = BigNumber::from_slice(x); - if !mod_in(&xx, &self.n) { + let r = r.unwrap_or_else(|| Nonce::random(&self.n)); + + let c = self.encrypt_num_with_nonce(&xx, &r)?; + + Ok((c, r)) + } + + /// Encrypt a number with the encryption key and given nonce + #[allow(clippy::many_single_char_names)] + pub fn encrypt_num_with_nonce(&self, x: &BigNumber, r: &Nonce) -> PaillierResult { + if !mod_in(x, &self.n) { return Err(PaillierError::InvalidEncryptionInputs); } - let r = r.unwrap_or_else(|| Nonce::random(&self.n)); - - if !mod_in(&r, &self.n) { + if !mod_in(r, &self.n) { return Err(PaillierError::InvalidEncryptionInputs); } // a = (N+1)^m mod N^2 - let a = (&self.n + BigNumber::one()).modpow(&xx, &self.nn); + let a = (&self.n + BigNumber::one()).modpow(x, &self.nn); // b = r^N mod N^2 let b = &r.modpow(&self.n, &self.nn); let c = a.modmul(b, &self.nn); - Ok((c, r)) + Ok(c) } /// Combines two Paillier ciphertexts diff --git a/src/error.rs b/src/error.rs index 4c0c88c..8814d10 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,6 +24,15 @@ pub enum PaillierError { /// Invalid ciphertext #[error("Invalid ciphertext, unable to decrypt")] InvalidCiphertext, + /// Invalid range proof error factor number + #[error("Invalid range proof error factor number")] + InvalidRangeProofErrorFactor, + /// Invalid verifier commitment + #[error("Invalid verifier commitment")] + InvalidVerifierCommitment, + /// Invalid range proof + #[error("Invalid range proof")] + InvalidRangeProof, } /// Paillier results diff --git a/src/lib.rs b/src/lib.rs index 5e839ca..9ce9f1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,18 +21,11 @@ mod macros; mod decryptionkey; mod encryptionkey; mod error; -mod proof_psf; - -pub use error::*; -pub use unknown_order; +mod proof; +mod utils; use unknown_order::BigNumber; - -pub(crate) fn mod_in(a: &BigNumber, n: &BigNumber) -> bool { - let lhs = &BigNumber::one() <= a; - let rhs = a < n; - lhs & rhs -} +use utils::*; /// A Paillier Ciphertext pub type Ciphertext = BigNumber; @@ -41,4 +34,6 @@ pub type Nonce = BigNumber; pub use decryptionkey::*; pub use encryptionkey::*; -pub use proof_psf::*; +pub use error::*; +pub use proof::*; +pub use unknown_order; diff --git a/src/proof.rs b/src/proof.rs new file mode 100644 index 0000000..5c41358 --- /dev/null +++ b/src/proof.rs @@ -0,0 +1,5 @@ +mod range; +mod square_free; + +pub use range::*; +pub use square_free::*; diff --git a/src/proof/range.rs b/src/proof/range.rs new file mode 100644 index 0000000..86644d5 --- /dev/null +++ b/src/proof/range.rs @@ -0,0 +1,378 @@ +use crate::{Ciphertext, EncryptionKey, PaillierError, PaillierResult}; +use rand::{CryptoRng, Rng, RngCore}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha512}; +use std::mem; +use unknown_order::BigNumber; + +/// A range proof for a Paillier ciphertext as defined in +/// Lindell Appendix A. +/// +/// Verifier is given the ciphertext C = ENC(ek, x). +/// +/// Prover demonstrates that x &EncryptionKey { + &self.ek + } + + /// Get the ciphertext that was used to generate this proof + pub fn ciphertext(&self) -> &Ciphertext { + &self.ciphertext + } + + /// Get the range that was used to generate this proof + pub fn range(&self) -> &BigNumber { + &self.range + } + + /// Generate a range proof for a given ciphertext. + pub fn prove( + ek: &EncryptionKey, + range: &BigNumber, + ciphertext: &Ciphertext, + secret_x: &BigNumber, + secret_r: &BigNumber, + error_factor: RangeProofErrorFactor, + mut rng: impl RngCore + CryptoRng, + ) -> Self { + let (range_proof_ciphertext, randomness) = + RangeProofCiphertext::generate(ek, range, error_factor, &mut rng); + let e = Self::compute_challenge(ek, &range_proof_ciphertext); + let inner = + InnerRangeProof::generate(ek, secret_x, secret_r, &e, range, &randomness, error_factor); + Self { + ek: ek.clone(), + range: range.clone(), + ciphertext: ciphertext.clone(), + inner, + range_proof_ciphertext, + error_factor, + } + } + + /// Verify the range proof with the given parameters. + pub fn verify_with_params( + &self, + ek: &EncryptionKey, + ciphertext: &Ciphertext, + range: &BigNumber, + ) -> PaillierResult<()> { + if self.ek.n != ek.n { + return Err(PaillierError::InvalidEncryptionKey); + } + if &self.ciphertext != ciphertext { + return Err(PaillierError::InvalidCiphertext); + } + if &self.range != range { + return Err(PaillierError::InvalidRangeProof); + } + self.verify() + } + + /// Verify the range proof. + pub fn verify(&self) -> PaillierResult<()> { + let e = Self::compute_challenge(&self.ek, &self.range_proof_ciphertext); + self.inner.verify( + &self.ek, + &e, + &self.range_proof_ciphertext, + &self.range, + &self.ciphertext, + self.error_factor, + ) + } + + fn compute_challenge( + ek: &EncryptionKey, + range_proof_ciphertext: &RangeProofCiphertext, + ) -> Vec { + let mut hasher = Sha512::new(); + hasher.update(ek.n.to_bytes()); + range_proof_ciphertext + .c1 + .iter() + .for_each(|c| hasher.update(c.to_bytes())); + range_proof_ciphertext + .c2 + .iter() + .for_each(|c| hasher.update(c.to_bytes())); + // Hash output must be > error_factor + let e_num = BigNumber::from_digest(hasher); + e_num.to_bytes() + } +} + +/// The inner range proof for a Paillier ciphertext as defined in +/// Lindell Appendix A. +/// +/// Verifier is given the ciphertext C = ENC(ek, x). +/// +/// Prover demonstrates that x); + +impl InnerRangeProof { + /// Generate a range proof for a given ciphertext. + pub fn generate( + ek: &EncryptionKey, + secret_x: &BigNumber, + secret_r: &BigNumber, + e: &[u8], + range: &BigNumber, + data: &RangeProofRandomness, + error_factor: RangeProofErrorFactor, + ) -> Self { + let error_factor = error_factor.into(); + // q/3 + let lower = range / BigNumber::from(3u8); + // 2q/3 + let upper = &lower * BigNumber::from(2u8); + + let bits = bit_vec::BitVec::from_bytes(e); + let mut inner = Vec::with_capacity(error_factor); + for (i, ei) in bits.iter().take(error_factor).enumerate() { + if ei { + let x_w1 = secret_x + &data.w1[i]; + // Mask + if lower < x_w1 && x_w1 < upper { + inner.push(RangeProofResponseType::Mask(Box::new( + RangeProofMaskResponse { + j: 1, + masked_x: x_w1, + masked_r: secret_r.modmul(&data.r1[i], &ek.n), + }, + ))); + } else { + inner.push(RangeProofResponseType::Mask(Box::new( + RangeProofMaskResponse { + j: 2, + masked_x: secret_x + &data.w2[i], + masked_r: secret_r.modmul(&data.r2[i], &ek.n), + }, + ))); + } + } else { + // Open + inner.push(RangeProofResponseType::Open(Box::new( + RangeProofOpenResponse { + w1: data.w1[i].clone(), + w2: data.w2[i].clone(), + r1: data.r1[i].clone(), + r2: data.r2[i].clone(), + }, + ))); + } + } + Self(inner) + } + + /// Verify the range proof. + pub fn verify( + &self, + ek: &EncryptionKey, + e: &[u8], + range_ciphertext: &RangeProofCiphertext, + range: &BigNumber, + ciphertext_x: &Ciphertext, + error_factor: RangeProofErrorFactor, + ) -> PaillierResult<()> { + // q/3 + let lower = range / BigNumber::from(3u8); + // 2q/3 + let upper = &lower * BigNumber::from(2u8); + + let bits = bit_vec::BitVec::from_bytes(e); + let error_factor = error_factor.into(); + + let mut res = true; + for (i, ei) in bits.iter().take(error_factor).enumerate() { + match (ei, &self.0[i]) { + (false, RangeProofResponseType::Open(o)) => { + let expected_c1 = ek + .encrypt_num_with_nonce(&o.w1, &o.r1) + .expect("Encryption to work"); + let expected_c2 = ek + .encrypt_num_with_nonce(&o.w2, &o.r2) + .expect("Encryption to work"); + + res &= expected_c1 == range_ciphertext.c1[i]; + res &= expected_c2 == range_ciphertext.c2[i]; + + res &= o.w2 < lower && lower <= o.w1 && o.w1 <= upper + || o.w1 < lower && lower <= o.w2 && o.w2 <= upper; + } + (true, RangeProofResponseType::Mask(m)) => { + let ciphertext = if m.j == 1 { + range_ciphertext.c1[i].modmul(ciphertext_x, &ek.nn) + } else { + range_ciphertext.c2[i].modmul(ciphertext_x, &ek.nn) + }; + + let ciphertext_z = ek + .encrypt_num_with_nonce(&m.masked_x, &m.masked_r) + .expect("Encryption to work"); + res &= ciphertext == ciphertext_z; + + res &= lower <= m.masked_x && m.masked_x <= upper; + } + _ => res = false, + } + if !res { + break; + } + } + + if res { + Ok(()) + } else { + Err(PaillierError::InvalidRangeProof) + } + } +} + +/// The error factor for the range proof. +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, +)] +pub enum RangeProofErrorFactor { + /// 40 bits of error + #[default] + Bits40, + /// 80 bits of error + Bits80, + /// 128 bits of error + Bits128, +} + +macro_rules! impl_range_proof_from { + ($($type:ident),+$(,)*) => { + $( + impl TryFrom<$type> for RangeProofErrorFactor { + type Error = PaillierError; + + fn try_from(value: $type) -> Result { + match value { + 40 => Ok(Self::Bits40), + 80 => Ok(Self::Bits80), + 128 => Ok(Self::Bits128), + _ => Err(PaillierError::InvalidRangeProofErrorFactor), + } + } + } + + impl From for $type { + fn from(value: RangeProofErrorFactor) -> Self { + match value { + RangeProofErrorFactor::Bits40 => 40, + RangeProofErrorFactor::Bits80 => 80, + RangeProofErrorFactor::Bits128 => 128, + } + } + } + )+ + }; +} + +impl_range_proof_from!(u8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize); + +/// The ciphertexts for a range proof as defined in +/// Lindell Appendix A. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct RangeProofCiphertext { + pub(crate) c1: Vec, + pub(crate) c2: Vec, +} + +impl RangeProofCiphertext { + /// Create a new range proof ciphertext. + pub fn generate( + ek: &EncryptionKey, + range: &BigNumber, + error_factor: RangeProofErrorFactor, + mut rng: impl RngCore + CryptoRng, + ) -> (Self, RangeProofRandomness) { + // q/3 + let lower = range / BigNumber::from(3u8); + // 2q/3 + let upper = &lower * BigNumber::from(2u8); + + let error_factor = error_factor.into(); + let mut w1 = (0..error_factor) + .map(|_| BigNumber::random_range_with_rng(&lower, &upper, &mut rng)) + .collect::>(); + + let mut w2 = w1.iter().map(|w| w - &lower).collect::>(); + + // probability 1/2 switch w1 and w2 + for i in 0..error_factor { + if rng.gen::() { + mem::swap(&mut w1[i], &mut w2[i]); + } + } + + let r1 = (0..error_factor) + .map(|_| BigNumber::from_rng(&lower, &mut rng)) + .collect::>(); + let r2 = (0..error_factor) + .map(|_| BigNumber::from_rng(&lower, &mut rng)) + .collect::>(); + + let c1 = w1 + .iter() + .zip(r1.iter()) + .map(|(w, r)| ek.encrypt_num_with_nonce(w, r).expect("Encrypt to work")) + .collect::>(); + let c2 = w2 + .iter() + .zip(r2.iter()) + .map(|(w, r)| ek.encrypt_num_with_nonce(w, r).expect("Encrypt to work")) + .collect::>(); + + (Self { c1, c2 }, RangeProofRandomness { w1, w2, r1, r2 }) + } +} + +/// The randomness used in the range proof. +#[derive(Clone, Debug, Default)] +pub(crate) struct RangeProofRandomness { + pub(crate) w1: Vec, + pub(crate) w2: Vec, + pub(crate) r1: Vec, + pub(crate) r2: Vec, +} + +/// The response to a range proof. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) enum RangeProofResponseType { + /// Opening + Open(Box), + /// Masking + Mask(Box), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct RangeProofOpenResponse { + pub(crate) w1: BigNumber, + pub(crate) w2: BigNumber, + pub(crate) r1: BigNumber, + pub(crate) r2: BigNumber, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct RangeProofMaskResponse { + pub(crate) j: u8, + pub(crate) masked_x: BigNumber, + pub(crate) masked_r: BigNumber, +} diff --git a/src/proof_psf.rs b/src/proof/square_free.rs similarity index 95% rename from src/proof_psf.rs rename to src/proof/square_free.rs index 43ba83e..71b182b 100644 --- a/src/proof_psf.rs +++ b/src/proof/square_free.rs @@ -1,4 +1,4 @@ -use crate::{mod_in, DecryptionKey, EncryptionKey, error::*}; +use crate::{error::*, mod_in, DecryptionKey, EncryptionKey}; use digest::{ generic_array::{typenum::Unsigned, GenericArray}, Digest, @@ -20,14 +20,14 @@ use unknown_order::BigNumber; /// as part of their DKG. /// A paillier key generator can prove the parameters where created honestly. #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ProofSquareFree(Vec); +pub struct SquareFreeProof(Vec); const L: usize = 13; #[cfg(feature = "wasm")] -wasm_slice_impl!(ProofSquareFree); +wasm_slice_impl!(SquareFreeProof); -impl ProofSquareFree { +impl SquareFreeProof { /// Generate a new SF proof. /// GG20 paper uses lots of values for the entropy like /// the ECDSA Public key, the curve generator and prime, @@ -41,7 +41,7 @@ impl ProofSquareFree { for x in proof.as_mut_slice() { *x = x.modpow(&m, &sk.pk.n); } - ProofSquareFree(proof) + SquareFreeProof(proof) }) } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..4cd612d --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,7 @@ +use unknown_order::BigNumber; + +pub fn mod_in(a: &BigNumber, n: &BigNumber) -> bool { + let lhs = &BigNumber::one() <= a; + let rhs = a < n; + lhs & rhs +} diff --git a/tests/paillier.rs b/tests/paillier.rs index efa80be..4f08f89 100644 --- a/tests/paillier.rs +++ b/tests/paillier.rs @@ -27,11 +27,11 @@ fn encrypt() { let m = b"this is a test message"; let res = pk.encrypt(m, None); - assert!(res.is_some()); + assert!(res.is_ok()); let (c, _) = res.unwrap(); let res = sk.decrypt(&c); - assert!(res.is_some()); + assert!(res.is_ok()); let m1 = res.unwrap(); assert_eq!(m1, m); @@ -43,7 +43,7 @@ fn encrypt() { for b in &bad_messages { let res = pk.encrypt(&b, None); - assert!(res.is_none()); + assert!(res.is_err()); } } @@ -60,16 +60,16 @@ fn add() { let res1 = pk.encrypt(&m1.to_bytes(), None); let res2 = pk.encrypt(&m2.to_bytes(), None); - assert!(res1.is_some()); - assert!(res2.is_some()); + assert!(res1.is_ok()); + assert!(res2.is_ok()); let (c1, _) = res1.unwrap(); let (c2, _) = res2.unwrap(); let res = pk.add(&c1, &c2); - assert!(res.is_some()); + assert!(res.is_ok()); let c3 = res.unwrap(); let res = sk.decrypt(&c3); - assert!(res.is_some()); + assert!(res.is_ok()); let bytes = res.unwrap(); let m3 = BigNumber::from_slice(bytes); assert_eq!(m3, BigNumber::from(13)); @@ -87,14 +87,14 @@ fn mul() { let m2 = BigNumber::from(6); let res1 = pk.encrypt(&m1.to_bytes(), None); - assert!(res1.is_some()); + assert!(res1.is_ok()); let (c1, _) = res1.unwrap(); let res = pk.mul(&c1, &m2); - assert!(res.is_some()); + assert!(res.is_ok()); let c2 = res.unwrap(); let res = sk.decrypt(&c2); - assert!(res.is_some()); + assert!(res.is_ok()); let bytes = res.unwrap(); let m3 = BigNumber::from_slice(bytes.as_slice()); assert_eq!(m3, BigNumber::from(42)); @@ -183,20 +183,20 @@ fn proof() { nonce.extend_from_slice(spk.as_affine().to_encoded_point(true).as_bytes()); nonce.push(1u8); - let res = ProofSquareFree::generate::(&sk, nonce.as_slice()); + let res = SquareFreeProof::generate::(&sk, nonce.as_slice()); assert!(res.is_some()); let proof = res.unwrap(); assert!(proof.verify::(&pk, nonce.as_slice())); let mut bytes = proof.to_bytes(); - let res = ProofSquareFree::from_bytes(bytes.as_slice()); + let res = SquareFreeProof::from_bytes(bytes.as_slice()); assert!(res.is_ok()); let proof1 = res.unwrap(); assert_eq!(proof1.to_bytes(), proof.to_bytes()); bytes[0] = 128; - let res = ProofSquareFree::from_bytes(bytes.as_slice()); + let res = SquareFreeProof::from_bytes(bytes.as_slice()); assert!(res.is_err()); } @@ -210,11 +210,11 @@ fn all() { let m = b"this is a test message"; let res = pk.encrypt(m, None); - assert!(res.is_some()); + assert!(res.is_ok()); let (c, _) = res.unwrap(); let res = sk.decrypt(&c); - assert!(res.is_some()); + assert!(res.is_ok()); let m1 = res.unwrap(); assert_eq!(m1, m); @@ -226,6 +226,39 @@ fn all() { for b in &bad_messages { let res = pk.encrypt(&b, None); - assert!(res.is_none()); + assert!(res.is_err()); } } + +#[cfg_attr(feature = "wasm", wasm_bindgen_test::wasm_bindgen_test)] +#[test] +fn range() { + use k256::elliptic_curve::Field; + use rand::SeedableRng; + + let mut rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]); + + let res = DecryptionKey::with_primes_unchecked(&b10(TEST_PRIMES[0]), &b10(TEST_PRIMES[1])); + assert!(res.is_some()); + let sk = res.unwrap(); + let pk = EncryptionKey::from(&sk); + let signing_key = k256::Scalar::random(&mut rng); + let x = BigNumber::from_slice(&signing_key.to_bytes()); + let r = BigNumber::from_rng(pk.n(), &mut rng); + let range = BigNumber::from_slice( + &hex::decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141").unwrap(), + ); + + let cipher_x = pk.encrypt_num_with_nonce(&x, &r).unwrap(); + + let range_proof = RangeProof::prove( + &pk, + &range, + &cipher_x, + &x, + &r, + RangeProofErrorFactor::Bits40, + &mut rng, + ); + assert!(range_proof.verify().is_ok()); +}