Skip to main content

paillier_zk/
paillier_encryption_in_range.rs

1//! ZK-proof of paillier encryption in range. Called Пenc or Renc in the CGGMP24
2//! paper.
3//!
4//! ## Description
5//!
6//! A party P has `key`, `pkey` - public and private keys in paillier
7//! cryptosystem. P also has `plaintext`, `nonce`, and
8//! `ciphertext = key.encrypt_with(plaintext, nonce)`.
9//!
10//! P wants to prove that `plaintext` is at most `l` bits, without disclosing
11//! it, the `pkey`, and `nonce`
12
13//! ## Example
14//!
15//! ```
16//! use paillier_zk::{paillier_encryption_in_range as p, IntegerExt};
17//! use fast_paillier::backend::Integer;
18//! # mod pregenerated {
19//! #     use super::*;
20//! #     paillier_zk::load_pregenerated_data!(
21//! #         verifier_aux: p::Aux,
22//! #         prover_decryption_key: fast_paillier::DecryptionKey,
23//! #     );
24//! # }
25//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
26//!
27//! let shared_state = "some shared state";
28//!
29//! let mut rng = rand_core::OsRng;
30//! # let mut rng = rand_dev::DevRng::new();
31//!
32//! // 0. Setup: prover and verifier share common Ring-Pedersen parameters:
33//!
34//! let aux: p::Aux = pregenerated::verifier_aux();
35//! let security = p::SecurityParams {
36//!     l: 256,
37//!     epsilon: 512,
38//!     q: Integer::curve_order::<generic_ec::curves::Secp256k1>(),
39//! };
40//!
41//! // 1. Setup: prover prepares the paillier keys
42//!
43//! let private_key: fast_paillier::DecryptionKey =
44//!     pregenerated::prover_decryption_key();
45//! let key = private_key.encryption_key();
46//!
47//! // 2. Setup: prover has some plaintext and encrypts it
48//!
49//! let plaintext = Integer::from_rng_half_pm(&mut rng, &(Integer::one() << security.l));
50//! let (ciphertext, nonce) = key.encrypt_with_random(&mut rng, &plaintext)?;
51//!
52//! // 3. Prover computes a non-interactive proof that plaintext is at most 1024 bits:
53//!
54//! let data = p::Data { key, ciphertext: &ciphertext };
55//! let proof = p::non_interactive::prove::<sha2::Sha256>(
56//!     &shared_state,
57//!     &aux,
58//!     data,
59//!     p::PrivateData {
60//!         plaintext: &plaintext,
61//!         nonce: &nonce,
62//!     },
63//!     &security,
64//!     &mut rng,
65//! )?;
66//!
67//! // 4. Prover sends this data to verifier
68//!
69//! # fn send(_: &p::Data, _: &p::NiProof) {  }
70//! send(&data, &proof);
71//!
72//! // 5. Verifier receives the data and the proof and verifies it
73//!
74//! # let recv = || (data, proof);
75//! let (data, proof) = recv();
76//! p::non_interactive::verify::<sha2::Sha256>(
77//!     &shared_state,
78//!     &aux,
79//!     data,
80//!     &security,
81//!     &proof,
82//! );
83//! # Ok(()) }
84//! ```
85//!
86//! If the verification succeeded, verifier can continue communication with prover
87
88use fast_paillier::backend::Integer;
89use fast_paillier::{AnyEncryptionKey, Ciphertext, Nonce};
90
91#[cfg(feature = "serde")]
92use serde::{Deserialize, Serialize};
93
94pub use crate::common::Aux;
95pub use crate::common::InvalidProof;
96
97/// Security parameters for proof. Choosing the values is a tradeoff between
98/// speed and chance of rejecting a valid proof or accepting an invalid proof
99#[derive(Debug, Clone, udigest::Digestable)]
100#[udigest(bound = "")]
101pub struct SecurityParams {
102    /// l in paper, security parameter for bit size of plaintext: it needs to
103    /// be in range [-2^l; 2^l] or equivalently 2^l
104    pub l: usize,
105    /// Epsilon in paper, slackness parameter
106    pub epsilon: usize,
107    /// Determines a domain of challenges
108    ///
109    /// Must be equal to order of the curve
110    #[udigest(as = crate::common::encoding::Integer)]
111    pub q: Integer,
112}
113
114/// Public data that both parties know
115#[derive(Debug, Clone, Copy, udigest::Digestable)]
116pub struct Data<'a> {
117    /// N0 in paper, public key that k -> K was encrypted on
118    #[udigest(as = crate::common::encoding::AnyEncryptionKey)]
119    pub key: &'a dyn AnyEncryptionKey,
120    /// K in paper
121    #[udigest(as = &crate::common::encoding::Integer)]
122    pub ciphertext: &'a Ciphertext,
123}
124
125/// Private data of prover
126#[derive(Clone, Copy)]
127pub struct PrivateData<'a> {
128    /// k in paper, plaintext of K
129    pub plaintext: &'a Integer,
130    /// rho in paper, nonce of encryption k -> K
131    pub nonce: &'a Nonce,
132}
133
134// As described in cggmp24 at page 33
135/// Prover's first message, obtained by [`interactive::commit`]
136#[derive(Debug, Clone, udigest::Digestable)]
137#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
138pub struct Commitment {
139    #[udigest(as = crate::common::encoding::Integer)]
140    pub s: Integer,
141    #[udigest(as = crate::common::encoding::Integer)]
142    pub a: Integer,
143    #[udigest(as = crate::common::encoding::Integer)]
144    pub c: Integer,
145}
146
147/// Prover's data accompanying the commitment. Kept as state between rounds in
148/// the interactive protocol.
149#[derive(Clone)]
150pub struct PrivateCommitment {
151    pub alpha: Integer,
152    pub mu: Integer,
153    pub r: Integer,
154    pub gamma: Integer,
155}
156
157/// Verifier's challenge to prover. Can be obtained deterministically by
158/// [`non_interactive::challenge`] or randomly by [`interactive::challenge`]
159pub type Challenge = Integer;
160
161// As described in cggmp24 at page 33
162/// The ZK proof. Computed by [`interactive::prove`].
163#[derive(Debug, Clone)]
164#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
165pub struct Proof {
166    pub z1: Integer,
167    pub z2: Integer,
168    pub z3: Integer,
169}
170
171/// The non-interactive ZK proof. Computed by [`non_interactive::prove`].
172/// Combines commitment and proof.
173#[derive(Debug, Clone)]
174#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
175pub struct NiProof {
176    pub commitment: Commitment,
177    pub proof: Proof,
178}
179
180/// The interactive version of the ZK proof. Should be completed in 3 rounds:
181/// prover commits to data, verifier responds with a random challenge, and
182/// prover gives proof with commitment and challenge.
183pub mod interactive {
184    use fast_paillier::backend::Integer;
185
186    use crate::{
187        common::{fail_if, fail_if_ne, InvalidProofReason},
188        BadExponent, Error,
189    };
190
191    use crate::common::{IntegerExt, InvalidProof};
192
193    use super::{
194        Aux, Challenge, Commitment, Data, PrivateCommitment, PrivateData, Proof, SecurityParams,
195    };
196
197    /// Create random commitment
198    pub fn commit(
199        aux: &Aux,
200        data: Data,
201        pdata: PrivateData,
202        security: &SecurityParams,
203        rng: &mut impl rand_core::RngCore,
204    ) -> Result<(Commitment, PrivateCommitment), Error> {
205        let two_to_l_plus_e = Integer::one() << (security.l + security.epsilon);
206        let hat_n_at_two_to_l = (Integer::one() << security.l) * &aux.rsa_modulo;
207        let hat_n_at_two_to_l_plus_e =
208            (Integer::one() << (security.l + security.epsilon)) * &aux.rsa_modulo;
209
210        let alpha = Integer::from_rng_half_pm(rng, &two_to_l_plus_e);
211        let mu = Integer::from_rng_half_pm(rng, &hat_n_at_two_to_l);
212        let r = Integer::sample_in_mult_group_of(rng, data.key.n());
213        let gamma = Integer::from_rng_half_pm(rng, &hat_n_at_two_to_l_plus_e);
214
215        let s = aux.combine(pdata.plaintext, &mu)?;
216        let a = data.key.encrypt_with(&alpha, &r)?;
217        let c = aux.combine(&alpha, &gamma)?;
218
219        Ok((
220            Commitment { s, a, c },
221            PrivateCommitment {
222                alpha,
223                mu,
224                r,
225                gamma,
226            },
227        ))
228    }
229
230    /// Compute proof for given data and prior protocol values
231    pub fn prove(
232        data: Data,
233        pdata: PrivateData,
234        private_commitment: &PrivateCommitment,
235        challenge: &Challenge,
236    ) -> Result<Proof, Error> {
237        let z1 = &private_commitment.alpha + (challenge * pdata.plaintext);
238        let nonce_to_challenge_mod_n: Integer = pdata
239            .nonce
240            .pow_mod_ref(challenge, data.key.n())
241            .ok_or(BadExponent::undefined())?;
242        let z2 = (&private_commitment.r * nonce_to_challenge_mod_n).modulo(data.key.n());
243        let z3 = &private_commitment.gamma + (challenge * &private_commitment.mu);
244        Ok(Proof { z1, z2, z3 })
245    }
246
247    /// Verify the proof
248    pub fn verify(
249        aux: &Aux,
250        data: Data,
251        commitment: &Commitment,
252        security: &SecurityParams,
253        challenge: &Challenge,
254        proof: &Proof,
255    ) -> Result<(), InvalidProof> {
256        fail_if(
257            InvalidProofReason::RangeCheck(1),
258            data.ciphertext.in_mult_group_of(data.key.nn()),
259        )?;
260        fail_if(
261            InvalidProofReason::RangeCheck(2),
262            aux.is_in_mult_group(&commitment.s),
263        )?;
264        fail_if(
265            InvalidProofReason::RangeCheck(3),
266            commitment.a.in_mult_group_of(data.key.nn()),
267        )?;
268        fail_if(
269            InvalidProofReason::RangeCheck(4),
270            aux.is_in_mult_group(&commitment.c),
271        )?;
272
273        fail_if(
274            InvalidProofReason::RangeCheck(5),
275            proof
276                .z1
277                .is_in_half_pm(&(Integer::one() << (security.l + security.epsilon))),
278        )?;
279        fail_if(
280            InvalidProofReason::RangeCheck(6),
281            proof.z3.is_in_half_pm(
282                &(&aux.rsa_modulo * (Integer::one() << (security.l + security.epsilon + 1))),
283            ),
284        )?;
285
286        {
287            let lhs = data
288                .key
289                .encrypt_with(&proof.z1, &proof.z2)
290                .map_err(|_| InvalidProofReason::PaillierEnc)?;
291            let rhs = {
292                let e_at_k = data
293                    .key
294                    .omul(challenge, data.ciphertext)
295                    .map_err(|_| InvalidProofReason::PaillierOp)?;
296                data.key
297                    .oadd(&commitment.a, &e_at_k)
298                    .map_err(|_| InvalidProofReason::PaillierOp)?
299            };
300            fail_if_ne(InvalidProofReason::EqualityCheck(2), lhs, rhs)?;
301        }
302
303        {
304            let lhs = aux.combine(&proof.z1, &proof.z3)?;
305            let s_to_e = aux.pow_mod(&commitment.s, challenge)?;
306            let rhs = (&commitment.c * s_to_e).modulo(&aux.rsa_modulo);
307            fail_if_ne(InvalidProofReason::EqualityCheck(3), lhs, rhs)?;
308        }
309
310        Ok(())
311    }
312
313    /// Generate random challenge
314    ///
315    /// `security` parameter is used to generate challenge in correct range
316    pub fn challenge(rng: &mut impl rand_core::RngCore, security: &SecurityParams) -> Challenge {
317        Integer::from_rng_half_pm(rng, &security.q)
318    }
319}
320
321/// The non-interactive version of proof. Completed in one round, for example
322/// see the documentation of parent module.
323pub mod non_interactive {
324    use digest::Digest;
325
326    use crate::{Error, InvalidProof};
327
328    use super::{Aux, Challenge, Commitment, Data, NiProof, PrivateData, SecurityParams};
329
330    /// Compute proof for the given data, producing random commitment and
331    /// deriving determenistic challenge.
332    ///
333    /// Obtained from the above interactive proof via Fiat-Shamir heuristic.
334    pub fn prove<D: Digest>(
335        shared_state: &impl udigest::Digestable,
336        aux: &Aux,
337        data: Data,
338        pdata: PrivateData,
339        security: &SecurityParams,
340        rng: &mut impl rand_core::RngCore,
341    ) -> Result<NiProof, Error> {
342        let (commitment, pcomm) = super::interactive::commit(aux, data, pdata, security, rng)?;
343        let challenge = challenge::<D>(shared_state, aux, data, &commitment, security);
344        let proof = super::interactive::prove(data, pdata, &pcomm, &challenge)?;
345        Ok(NiProof { commitment, proof })
346    }
347
348    /// Deterministically compute challenge based on prior known values in protocol
349    pub fn challenge<D: Digest>(
350        shared_state: &impl udigest::Digestable,
351        aux: &Aux,
352        data: Data,
353        commitment: &Commitment,
354        security: &SecurityParams,
355    ) -> Challenge {
356        let tag = "paillier_zk.encryption_in_range.ni_challenge";
357        let seed = udigest::inline_struct!(tag {
358            security,
359            shared_state,
360            aux: aux.digest_public_data(),
361            data,
362            commitment,
363        });
364        let mut rng = rand_hash::HashRng::<D, _>::from_seed(seed);
365        super::interactive::challenge(&mut rng, security)
366    }
367
368    /// Verify the proof, deriving challenge independently from same data
369    pub fn verify<D: Digest>(
370        shared_state: &impl udigest::Digestable,
371        aux: &Aux,
372        data: Data,
373        security: &SecurityParams,
374        proof: &NiProof,
375    ) -> Result<(), InvalidProof> {
376        let challenge = challenge::<D>(shared_state, aux, data, &proof.commitment, security);
377        super::interactive::verify(
378            aux,
379            data,
380            &proof.commitment,
381            security,
382            &challenge,
383            &proof.proof,
384        )
385    }
386}
387
388#[cfg(test)]
389mod test {
390    use fast_paillier::backend::Integer;
391    use sha2::Digest;
392
393    use crate::common::{IntegerExt, InvalidProofReason};
394
395    fn run_with<D: Digest>(
396        mut rng: &mut impl rand_core::CryptoRngCore,
397        security: super::SecurityParams,
398        plaintext: Integer,
399    ) -> Result<(), crate::common::InvalidProof> {
400        let aux = crate::common::test::aux(&mut rng);
401        let private_key = crate::common::test::random_key(&mut rng).unwrap();
402        let key = private_key.encryption_key();
403        let (ciphertext, nonce) = key.encrypt_with_random(&mut rng, &plaintext).unwrap();
404        let data = super::Data {
405            key,
406            ciphertext: &ciphertext,
407        };
408        let pdata = super::PrivateData {
409            plaintext: &plaintext,
410            nonce: &nonce,
411        };
412
413        let shared_state = "shared state";
414        let proof =
415            super::non_interactive::prove::<D>(&shared_state, &aux, data, pdata, &security, rng)
416                .unwrap();
417        super::non_interactive::verify::<D>(&shared_state, &aux, data, &security, &proof)
418    }
419
420    #[test]
421    fn passing() {
422        let mut rng = rand_dev::DevRng::new();
423        let security = super::SecurityParams {
424            l: 256,
425            epsilon: 512,
426            q: Integer::curve_order::<generic_ec::curves::Secp256k1>(),
427        };
428        let plaintext = Integer::from_rng_half_pm(&mut rng, &(Integer::one() << security.l));
429        let r = run_with::<sha2::Sha256>(&mut rng, security, plaintext);
430        match r {
431            Ok(()) => (),
432            Err(e) => panic!("{e:?}"),
433        }
434    }
435    #[test]
436    fn failing() {
437        let mut rng = rand_dev::DevRng::new();
438        let security = super::SecurityParams {
439            l: 256,
440            epsilon: 512,
441            q: Integer::curve_order::<generic_ec::curves::Secp256k1>(),
442        };
443        let plaintext = (Integer::one() << (security.l + security.epsilon)) + 1;
444        let r = run_with::<sha2::Sha256>(&mut rng, security, plaintext);
445        match r.map_err(|e| e.reason()) {
446            Ok(()) => panic!("proof should not pass"),
447            Err(InvalidProofReason::RangeCheck(_)) => (),
448            Err(e) => panic!("proof should not fail with {e:?}"),
449        }
450    }
451}