Skip to main content

confium_tc_cmp20/
mta_proofs.rs

1//! Zero-knowledge proofs for the MtA protocol — GG18/GG20 Appendix A
2//! (Gennaro & Goldfeder, eprint 2019/114 A.1/A.2), the construction
3//! spec 70-cmp20 requires on every MtA ciphertext.
4//!
5//! Two proofs, both sigma protocols made non-interactive via
6//! Fiat-Shamir (SHA-256 over the full transcript, challenge reduced
7//! mod `q`, the ECDSA group order):
8//!
9//! - [`RangeProof`] (initiator): for Paillier ciphertext
10//!   `c = Γ^m·r^N mod N²`, proves knowledge of `(m, r)` with
11//!   `m ∈ [0, q³]` — the "small ciphertext" guarantee that prevents
12//!   the wrap-around attacks described in the paper.
13//! - [`RespondentProof`] (responder, plain MtA): for
14//!   `c₂ = c₁^x·Γ^y·r^N mod N²`, proves knowledge of `(x, y, r)` with
15//!   `x ∈ [0, q³]` and `y ∈ [0, q⁷]` — binding the response to the
16//!   received ciphertext and bounding the mask.
17//!
18//! The commitments use an auxiliary Strong-RSA modulus
19//! `Ñ = P̃·Q̃` with `P̃ = 2p̃+1`, `Q̃ = 2q̃+1` (safe primes) and
20//! `h₁, h₂ ∈ Z*_Ñ` with `h₂ = h₁^w` for discarded `w` — nobody (least
21//! of all the prover) knows the discrete log of `h₂` in base `h₁`,
22//! which is what makes the h-commitments binding.
23//!
24//! # Trust model and limitations (audit ledger)
25//!
26//! - A commitment key is generated by the VERIFIER and used by the
27//!   OTHER party as prover. Never prove against a key you generated
28//!   yourself — you would know the h₂/h₁ discrete log.
29//! - The paper proves Ñ is of the correct form (safe-prime product)
30//!   with an additional ZK proof ([Gennaro-Micciancio-Rabin]); this
31//!   implementation generates such moduli honestly but does NOT yet
32//!   prove their form to verifiers. A malicious verifier could
33//!   mis-generate a key to break a prover's privacy. Trusted-setup
34//!   gap, tracked in the audit ledger.
35//! - Soundness holds under Strong RSA over Ñ; size Ñ accordingly
36//!   (the paper's setting: 2048-bit Ñ alongside 2048-bit N).
37
38use confium_tc::paillier::PaillierPublicKey;
39use confium_tc::paillier::generate_prime;
40use confium_tc::paillier::miller_rabin;
41use num_bigint::BigUint;
42use num_bigint::RandBigInt;
43use num_traits::One;
44use num_traits::Zero;
45use rand::rngs::OsRng;
46use sha2::Digest;
47use sha2::Sha256;
48
49/// Auxiliary commitment key `(Ñ, h₁, h₂)` — see the module docs.
50#[derive(Debug, Clone)]
51pub struct CommitmentKey {
52    /// Safe-prime product modulus.
53    pub n_tilde: BigUint,
54    /// First generator (a random quadratic residue).
55    pub h1: BigUint,
56    /// Second generator (`h₁^w` for discarded `w`).
57    pub h2: BigUint,
58}
59
60/// Generate a commitment key: `Ñ = P̃Q̃` with `P̃, Q̃` safe primes of
61/// `prime_bits` each, `h₁` a random quadratic residue mod `Ñ`, and
62/// `h₂ = h₁^w` for uniformly random discarded `w`.
63///
64/// The generator of this key is its VERIFIER — see the module docs
65/// for the trust direction.
66pub fn generate_commitment_key(prime_bits: u32) -> CommitmentKey {
67    let p_tilde = generate_safe_prime(prime_bits);
68    let q_tilde = loop {
69        let candidate = generate_safe_prime(prime_bits);
70        if candidate != p_tilde {
71            break candidate;
72        }
73    };
74    let n_tilde = &p_tilde * &q_tilde;
75
76    let mut rng = OsRng;
77    // h1: a random quadratic residue — squaring guarantees membership
78    // in QR(Ñ) without knowing the factorization.
79    let h1 = {
80        let x = rng.gen_biguint_range(&BigUint::one(), &n_tilde);
81        (&x * &x) % &n_tilde
82    };
83    // h2 = h1^w with w discarded: the discrete log of h₂ in base h₁
84    // is unknown to everyone, including this generator.
85    let w = rng.gen_biguint_range(&BigUint::one(), &n_tilde);
86    let h2 = h1.modpow(&w, &n_tilde);
87
88    CommitmentKey { n_tilde, h1, h2 }
89}
90
91/// The P-256 group order `q`, as the proofs' range parameter.
92pub fn p256_order() -> num_bigint::BigUint {
93    num_bigint::BigUint::parse_bytes(
94        b"ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551",
95        16,
96    )
97    .expect("static hex parses")
98}
99
100fn generate_safe_prime(prime_bits: u32) -> BigUint {
101    loop {
102        let p = generate_prime(prime_bits);
103        let candidate = (&p << 1u32) + BigUint::one();
104        if miller_rabin(&candidate, 20) {
105            return candidate;
106        }
107    }
108}
109
110/// Initiator's range proof: knowledge of `(m, r)` with
111/// `c = Γ^m·r^N mod N²` and `m ∈ [0, q³]`.
112#[derive(Debug, Clone)]
113pub struct RangeProof {
114    /// Commitment `h₁^m·h₂^ρ mod Ñ`.
115    pub z: BigUint,
116    /// Announcement `Γ^α·β^N mod N²`.
117    pub u: BigUint,
118    /// Announcement `h₁^α·h₂^γ mod Ñ`.
119    pub w: BigUint,
120    /// Response `r^e·β mod N`.
121    pub s: BigUint,
122    /// Response `e·m + α` (checked `≤ q³` as an integer).
123    pub s1: BigUint,
124    /// Response `e·ρ + γ`.
125    pub s2: BigUint,
126}
127
128/// Prove that `c = Γ^m·r^N mod N²` opens to `m ∈ [0, q³)`.
129///
130/// `r` must be the exact Paillier randomness used to build `c`.
131pub fn prove_range(
132    q: &BigUint,
133    paillier: &PaillierPublicKey,
134    ck: &CommitmentKey,
135    c: &BigUint,
136    m: &BigUint,
137    r: &BigUint,
138) -> RangeProof {
139    let q3 = q * q * q;
140    let q2 = q * q;
141    let q_n_tilde = q * &ck.n_tilde;
142    let q3_n_tilde = &q3 * &ck.n_tilde;
143
144    let mut rng = OsRng;
145    // α ← [0, q³ − q²): guarantees the honest s₁ = e·m + α stays
146    // below q³ (e·m < q² for m < q), so completeness never fails.
147    let alpha = rng.gen_biguint_range(&BigUint::zero(), &(&q3 - &q2));
148    let beta = rng.gen_biguint_range(&BigUint::one(), &paillier.n);
149    let gamma = rng.gen_biguint_range(&BigUint::zero(), &(&q3_n_tilde - &q_n_tilde));
150    let rho = rng.gen_biguint_range(&BigUint::zero(), &q_n_tilde);
151
152    let z = (&ck.h1.modpow(m, &ck.n_tilde) * &ck.h2.modpow(&rho, &ck.n_tilde)) % &ck.n_tilde;
153    let u = (&paillier.g.modpow(&alpha, &paillier.n_squared)
154        * &beta.modpow(&paillier.n, &paillier.n_squared))
155        % &paillier.n_squared;
156    let w = (&ck.h1.modpow(&alpha, &ck.n_tilde) * &ck.h2.modpow(&gamma, &ck.n_tilde)) % &ck.n_tilde;
157
158    let e = range_challenge(q, paillier, ck, c, &z, &u, &w);
159
160    let s = (r.modpow(&e, &paillier.n) * &beta) % &paillier.n;
161    let s1 = &e * m + &alpha;
162    let s2 = &e * &rho + &gamma;
163
164    RangeProof { z, u, w, s, s1, s2 }
165}
166
167/// Verify an initiator range proof for ciphertext `c`.
168pub fn verify_range(
169    q: &BigUint,
170    paillier: &PaillierPublicKey,
171    ck: &CommitmentKey,
172    c: &BigUint,
173    proof: &RangeProof,
174) -> bool {
175    let q3 = q * q * q;
176    // Integer range check: this is what actually bounds m (soundness
177    // slack q⁴ for a q-range challenge).
178    if proof.s1 > q3 {
179        return false;
180    }
181    if proof.s.is_zero() || proof.s >= paillier.n || proof.s1.is_zero() {
182        return false;
183    }
184
185    let e = range_challenge(q, paillier, ck, c, &proof.z, &proof.u, &proof.w);
186
187    // u == Γ^{s1}·s^N·c^{-e}  (mod N²)
188    let c_inv = match modinv(&to_bigint_helper(c), &to_bigint_helper(&paillier.n_squared)) {
189        Some(inv) => inv.to_biguint().unwrap(),
190        None => return false,
191    };
192    let lhs = (&paillier.g.modpow(&proof.s1, &paillier.n_squared)
193        * &proof.s.modpow(&paillier.n, &paillier.n_squared)
194        % &paillier.n_squared
195        * &c_inv.modpow(&e, &paillier.n_squared))
196        % &paillier.n_squared;
197    if lhs != proof.u {
198        return false;
199    }
200
201    // w == h1^{s1}·h2^{s2}·z^{-e}  (mod Ñ)
202    let z_inv = match modinv(&to_bigint_helper(&proof.z), &to_bigint_helper(&ck.n_tilde)) {
203        Some(inv) => inv.to_biguint().unwrap(),
204        None => return false,
205    };
206    let lhs2 = (&ck.h1.modpow(&proof.s1, &ck.n_tilde) * &ck.h2.modpow(&proof.s2, &ck.n_tilde)
207        % &ck.n_tilde
208        * &z_inv.modpow(&e, &ck.n_tilde))
209        % &ck.n_tilde;
210    lhs2 == proof.w
211}
212
213/// Responder proof for plain MtA: knowledge of `(x, y, r)` with
214/// `c₂ = c₁^x·Γ^y·r^N mod N²`, `x ∈ [0, q³]`, `y ∈ [0, q⁷]`.
215#[derive(Debug, Clone)]
216pub struct RespondentProof {
217    /// Commitment `h₁^x·h₂^ρ mod Ñ` (binds the secret x).
218    pub z: BigUint,
219    /// Announcement `h₁^α·h₂^{ρ'} mod Ñ`.
220    pub z_prime: BigUint,
221    /// Commitment `h₁^y·h₂^σ mod Ñ` (binds the mask y).
222    pub t: BigUint,
223    /// Announcement `c₁^α·Γ^γ·β^N mod N²`.
224    pub v: BigUint,
225    /// Announcement `h₁^γ·h₂^τ mod Ñ`.
226    pub w: BigUint,
227    /// Response `r^e·β mod N`.
228    pub s: BigUint,
229    /// Response `e·x + α` (checked `≤ q³`).
230    pub s1: BigUint,
231    /// Response `e·ρ + ρ'`.
232    pub s2: BigUint,
233    /// Response `e·y + γ` (checked `≤ q⁷`).
234    pub t1: BigUint,
235    /// Response `e·σ + τ`.
236    pub t2: BigUint,
237}
238
239/// Prove the responder statement for `c₂ = c₁^x·Γ^y·r^N mod N²`,
240/// where `r` is the Paillier randomness used for the `Γ^y` term.
241// The interface mirrors the paper's statement one-to-one: statement
242// (c₁, c₂) + witness (x, y, r) + parameters (q, N-pair, ck).
243#[allow(clippy::too_many_arguments)]
244pub fn prove_respondent(
245    q: &BigUint,
246    paillier: &PaillierPublicKey,
247    ck: &CommitmentKey,
248    c1: &BigUint,
249    c2: &BigUint,
250    x: &BigUint,
251    y: &BigUint,
252    r: &BigUint,
253) -> RespondentProof {
254    let q3 = q * q * q;
255    let q7 = &q3 * &q3 * q;
256    let q6 = &q3 * &q3;
257    let q2 = q * q;
258    let q_n_tilde = q * &ck.n_tilde;
259    let q3_n_tilde = &q3 * &ck.n_tilde;
260
261    let mut rng = OsRng;
262    let alpha = rng.gen_biguint_range(&BigUint::zero(), &(&q3 - &q2));
263    let rho = rng.gen_biguint_range(&BigUint::zero(), &q_n_tilde);
264    let rho_prime = rng.gen_biguint_range(&BigUint::zero(), &(&q3_n_tilde - &q_n_tilde));
265    let sigma = rng.gen_biguint_range(&BigUint::zero(), &q_n_tilde);
266    let beta = rng.gen_biguint_range(&BigUint::one(), &paillier.n);
267    // γ ← [0, q⁷ − q⁶): keeps the honest t₁ = e·y + γ below q⁷.
268    let gamma = rng.gen_biguint_range(&BigUint::zero(), &(&q7 - &q6));
269    let tau = rng.gen_biguint_range(&BigUint::zero(), &(&q3_n_tilde - &q_n_tilde));
270
271    let n_tilde = &ck.n_tilde;
272    let h_pow = |a: &BigUint, b: &BigUint| ck.h1.modpow(a, n_tilde) * &ck.h2.modpow(b, n_tilde);
273
274    let z = h_pow(x, &rho) % n_tilde;
275    let z_prime = h_pow(&alpha, &rho_prime) % n_tilde;
276    let t = h_pow(y, &sigma) % n_tilde;
277    let v = (&c1.modpow(&alpha, &paillier.n_squared)
278        * &paillier.g.modpow(&gamma, &paillier.n_squared)
279        % &paillier.n_squared
280        * &beta.modpow(&paillier.n, &paillier.n_squared))
281        % &paillier.n_squared;
282    let w = h_pow(&gamma, &tau) % n_tilde;
283
284    let e = respondent_challenge(q, paillier, ck, c1, c2, &z, &z_prime, &t, &v, &w);
285
286    let s = (r.modpow(&e, &paillier.n) * &beta) % &paillier.n;
287    let s1 = &e * x + &alpha;
288    let s2 = &e * &rho + &rho_prime;
289    let t1 = &e * y + &gamma;
290    let t2 = &e * &sigma + &tau;
291
292    RespondentProof {
293        z,
294        z_prime,
295        t,
296        v,
297        w,
298        s,
299        s1,
300        s2,
301        t1,
302        t2,
303    }
304}
305
306/// Verify a responder proof binding `c₂` to `c₁` with bounded secrets.
307#[allow(clippy::too_many_arguments)]
308pub fn verify_respondent(
309    q: &BigUint,
310    paillier: &PaillierPublicKey,
311    ck: &CommitmentKey,
312    c1: &BigUint,
313    c2: &BigUint,
314    proof: &RespondentProof,
315) -> bool {
316    let q3 = q * q * q;
317    let q7 = &q3 * &q3 * q;
318    if proof.s1 > q3 || proof.t1 > q7 {
319        return false;
320    }
321    if proof.s.is_zero() || proof.s >= paillier.n || proof.s1.is_zero() || proof.t1.is_zero() {
322        return false;
323    }
324
325    let e = respondent_challenge(
326        q,
327        paillier,
328        ck,
329        c1,
330        c2,
331        &proof.z,
332        &proof.z_prime,
333        &proof.t,
334        &proof.v,
335        &proof.w,
336    );
337
338    // z' == h1^{s1}·h2^{s2}·z^{-e}  (mod Ñ)
339    let n_tilde = &ck.n_tilde;
340    let z_inv = match modinv(&to_bigint_helper(&proof.z), &to_bigint_helper(n_tilde)) {
341        Some(inv) => inv.to_biguint().unwrap(),
342        None => return false,
343    };
344    let lhs1 = (&ck.h1.modpow(&proof.s1, n_tilde) * &ck.h2.modpow(&proof.s2, n_tilde) % n_tilde
345        * &z_inv.modpow(&e, n_tilde))
346        % n_tilde;
347    if lhs1 != proof.z_prime {
348        return false;
349    }
350
351    // w == h1^{t1}·h2^{t2}·t^{-e}  (mod Ñ)
352    let t_inv = match modinv(&to_bigint_helper(&proof.t), &to_bigint_helper(n_tilde)) {
353        Some(inv) => inv.to_biguint().unwrap(),
354        None => return false,
355    };
356    let lhs2 = (&ck.h1.modpow(&proof.t1, n_tilde) * &ck.h2.modpow(&proof.t2, n_tilde) % n_tilde
357        * &t_inv.modpow(&e, n_tilde))
358        % n_tilde;
359    if lhs2 != proof.w {
360        return false;
361    }
362
363    // v == c1^{s1}·s^N·Γ^{t1}·c2^{-e}  (mod N²)
364    let c2_inv = match modinv(
365        &to_bigint_helper(c2),
366        &to_bigint_helper(&paillier.n_squared),
367    ) {
368        Some(inv) => inv.to_biguint().unwrap(),
369        None => return false,
370    };
371    let lhs3 = (&c1.modpow(&proof.s1, &paillier.n_squared)
372        * &proof.s.modpow(&paillier.n, &paillier.n_squared)
373        % &paillier.n_squared
374        * &paillier.g.modpow(&proof.t1, &paillier.n_squared)
375        % &paillier.n_squared
376        * &c2_inv.modpow(&e, &paillier.n_squared))
377        % &paillier.n_squared;
378    lhs3 == proof.v
379}
380
381// ---- helpers -----------------------------------------------------------
382
383fn range_challenge(
384    q: &BigUint,
385    paillier: &PaillierPublicKey,
386    ck: &CommitmentKey,
387    c: &BigUint,
388    z: &BigUint,
389    u: &BigUint,
390    w: &BigUint,
391) -> BigUint {
392    let mut hasher = Sha256::new();
393    hasher.update(b"confium-mta-range-v1");
394    hasher.update(q.to_bytes_be());
395    hasher.update(paillier.n.to_bytes_be());
396    hasher.update(paillier.g.to_bytes_be());
397    hasher.update(ck.n_tilde.to_bytes_be());
398    hasher.update(ck.h1.to_bytes_be());
399    hasher.update(ck.h2.to_bytes_be());
400    hasher.update(c.to_bytes_be());
401    hasher.update(z.to_bytes_be());
402    hasher.update(u.to_bytes_be());
403    hasher.update(w.to_bytes_be());
404    challenge_from_hash(hasher, q)
405}
406
407// Transcript-hashing helper: every Fiat-Shamir input is a
408// parameter by construction.
409#[allow(clippy::too_many_arguments)]
410fn respondent_challenge(
411    q: &BigUint,
412    paillier: &PaillierPublicKey,
413    ck: &CommitmentKey,
414    c1: &BigUint,
415    c2: &BigUint,
416    z: &BigUint,
417    z_prime: &BigUint,
418    t: &BigUint,
419    v: &BigUint,
420    w: &BigUint,
421) -> BigUint {
422    let mut hasher = Sha256::new();
423    hasher.update(b"confium-mta-respondent-v1");
424    hasher.update(q.to_bytes_be());
425    hasher.update(paillier.n.to_bytes_be());
426    hasher.update(paillier.g.to_bytes_be());
427    hasher.update(ck.n_tilde.to_bytes_be());
428    hasher.update(ck.h1.to_bytes_be());
429    hasher.update(ck.h2.to_bytes_be());
430    hasher.update(c1.to_bytes_be());
431    hasher.update(c2.to_bytes_be());
432    hasher.update(z.to_bytes_be());
433    hasher.update(z_prime.to_bytes_be());
434    hasher.update(t.to_bytes_be());
435    hasher.update(v.to_bytes_be());
436    hasher.update(w.to_bytes_be());
437    challenge_from_hash(hasher, q)
438}
439
440/// Reduce the digest to a challenge in [0, q) by rejection sampling
441/// with re-hash — never a biased or constant fallback.
442fn challenge_from_hash(hasher: Sha256, q: &BigUint) -> BigUint {
443    // The digest chunks are consumed 8 bytes at a time; each round
444    // extends the hashed material so the next digest is independent.
445    let mut current: Vec<u8> = hasher.finalize().to_vec();
446    loop {
447        let candidate = BigUint::from_bytes_be(&current);
448        if &candidate < q {
449            return candidate;
450        }
451        let mut h = Sha256::new();
452        h.update(b"confium-mta-challenge-reduce-v1");
453        h.update(&current);
454        current = h.finalize().to_vec();
455    }
456}
457
458/// Extended GCD can go negative; modinv needs signed arithmetic.
459fn to_bigint_helper(x: &BigUint) -> num_bigint::BigInt {
460    use num_bigint::ToBigInt;
461    x.to_bigint().unwrap_or_default()
462}
463
464fn modinv(a: &num_bigint::BigInt, m: &num_bigint::BigInt) -> Option<num_bigint::BigInt> {
465    use num_bigint::BigInt;
466    let (g, x, _) = extended_gcd(a, m);
467    if g != BigInt::one() {
468        return None;
469    }
470    let r = &x % m;
471    if r.sign() == num_bigint::Sign::Minus {
472        Some(r + m)
473    } else {
474        Some(r)
475    }
476}
477
478fn extended_gcd(
479    a: &num_bigint::BigInt,
480    b: &num_bigint::BigInt,
481) -> (num_bigint::BigInt, num_bigint::BigInt, num_bigint::BigInt) {
482    use num_bigint::BigInt;
483    if b.is_zero() {
484        return (a.clone(), BigInt::one(), BigInt::zero());
485    }
486    let (g, x, y) = extended_gcd(b, &(a % b));
487    (g, y.clone(), x - (a / b) * y)
488}