Skip to main content

confium_tc_cmp20/
paillier_mta.rs

1//! Paillier-based Multiplicative-to-Additive (MtA) share conversion.
2//!
3//! Two surfaces:
4//!
5//! - The **proved** functions ([`full_mta_proved`] and friends): the
6//!   GG18/GG20 §3 + Appendix A protocol, which spec 70-cmp20
7//!   requires — every ciphertext carries a zero-knowledge proof, the
8//!   responder refuses unproven input, the initiator refuses unbound
9//!   responses, and the mask is the paper's small `β′ ∈ [0, q⁵)` so
10//!   honest shares never wrap: `α − β′ = k_i·x_j` exactly.
11//!
12//! Key direction: the exchange runs under the INITIATOR's Paillier
13//! key — party i encrypts k_i under its own public key, party j
14//! responds using only public material, and only party i can decrypt
15//! the response. The share contract demands it: α − β′ = k_i·x_j
16//! exactly, so whichever party ever holds BOTH α and β′ recovers the
17//! peer's secret outright. Keeping the final decryption with the
18//! initiator (who never learns β′) is what makes the three split
19//! functions safe to run across processes — the responder never sees
20//! a private key.
21//!
22//! Trust direction for the commitment keys: each party generates its
23//! own `(Ñ, h₁, h₂)` and the OTHER party proves against it — see
24//! `mta_proofs` for why a party must never prove to its own key.
25
26use confium_tc::paillier::{
27    PaillierError, PaillierKeypair, PaillierPrivateKey, PaillierPublicKey, add as paillier_add,
28    decrypt as paillier_decrypt, encrypt as paillier_encrypt, scalar_mul as paillier_scalar_mul,
29};
30use num_bigint::{BigUint, RandBigInt};
31use num_traits::Zero;
32use rand::rngs::OsRng;
33
34use crate::mta_proofs::CommitmentKey;
35use crate::mta_proofs::RangeProof;
36use crate::mta_proofs::RespondentProof;
37use crate::mta_proofs::prove_range;
38use crate::mta_proofs::prove_respondent;
39use crate::mta_proofs::verify_range;
40use crate::mta_proofs::verify_respondent;
41
42// ---- proved path (GG18 §3 + Appendix A) --------------------------------
43
44/// Proved round-1 message: ciphertext plus its range proof.
45#[derive(Debug, Clone)]
46pub struct ProvedMessage1 {
47    /// Encrypted k_i under i's (the initiator's) Paillier public key.
48    pub ciphertext: BigUint,
49    /// ZK range proof that the encrypted value is `< q³`.
50    pub range_proof: RangeProof,
51}
52
53/// Proved round-2 message: bound ciphertext plus its respondent proof.
54#[derive(Debug, Clone)]
55pub struct ProvedMessage2 {
56    /// Encrypted k_i·x_j + β′ under i's Paillier public key.
57    pub ciphertext: BigUint,
58    /// ZK proof that this ciphertext is `c₁^x·Γ^{β'}·r^N` with
59    /// `x < q³`, `β′ < q⁷`.
60    pub respondent_proof: RespondentProof,
61    /// The mask β′ (party j's negated additive share).
62    pub beta: BigUint,
63}
64
65/// Errors on the proved MtA path.
66#[derive(Debug)]
67pub enum MtaProofError {
68    /// Underlying Paillier failure.
69    Paillier(PaillierError),
70    /// A ZK proof failed verification — the peer deviated from the
71    /// protocol.
72    InvalidProof,
73}
74
75impl std::fmt::Display for MtaProofError {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Self::Paillier(e) => write!(f, "paillier error: {e}"),
79            Self::InvalidProof => write!(f, "MtA ZK proof failed verification"),
80        }
81    }
82}
83
84impl std::error::Error for MtaProofError {}
85
86impl From<PaillierError> for MtaProofError {
87    fn from(e: PaillierError) -> Self {
88        Self::Paillier(e)
89    }
90}
91
92/// Party i initiates the proved MtA: encrypt k_i under i's OWN
93/// public key and prove it is in range.
94///
95/// `ck_j` is party j's commitment key (j verifies this proof);
96/// `q` is the ECDSA group order. Party j will respond using only
97/// public material — it can never open this ciphertext.
98pub fn party_i_init_proved(
99    i_public: &PaillierPublicKey,
100    ck_j: &CommitmentKey,
101    q: &BigUint,
102    k_i: &BigUint,
103) -> Result<ProvedMessage1, MtaProofError> {
104    let r = random_below(&i_public.n);
105    let ciphertext = paillier_encrypt(i_public, k_i, &r)?;
106    let range_proof = prove_range(q, i_public, ck_j, &ciphertext, k_i, &r);
107    Ok(ProvedMessage1 {
108        ciphertext,
109        range_proof,
110    })
111}
112
113/// Party j responds in the proved MtA: verify the range proof, then
114/// multiply by x_j and mask with β′.
115///
116/// `i_public` is the initiator's Paillier public key — the exchange
117/// runs under it, so j needs NO private material (j could never open
118/// the initiator's ciphertext anyway). `ck_j` is j's own commitment
119/// key (the initiator proved against it); `ck_i` is party i's key (i
120/// verifies the respondent proof j produces). Returns the response
121/// for i plus j's own share β′.
122pub fn party_j_respond_proved(
123    i_public: &PaillierPublicKey,
124    ck_i: &CommitmentKey,
125    ck_j: &CommitmentKey,
126    q: &BigUint,
127    msg: &ProvedMessage1,
128    x_j: &BigUint,
129) -> Result<(ProvedMessage2, BigUint), MtaProofError> {
130    if !verify_range(q, i_public, ck_j, &msg.ciphertext, &msg.range_proof) {
131        return Err(MtaProofError::InvalidProof);
132    }
133
134    // Small mask per the paper: β′ ∈ [0, q⁵) so k·x + β′ never wraps
135    // mod N and the respondent proof's range checks are satisfiable.
136    let q5 = {
137        let q2 = q * q;
138        &q2 * &q2 * q
139    };
140    let mut rng = OsRng;
141    let beta_prime = rng.gen_biguint_range(&BigUint::zero(), &q5);
142
143    // c' = c^{x_j} · Γ^{β'} · r'^N = E(k·x + β')
144    let c_mul = paillier_scalar_mul(i_public, &msg.ciphertext, x_j);
145    let r_prime = random_below(&i_public.n);
146    let c_beta = paillier_encrypt(i_public, &beta_prime, &r_prime)?;
147    let c_prime = paillier_add(i_public, &c_mul, &c_beta);
148
149    let respondent_proof = prove_respondent(
150        q,
151        i_public,
152        ck_i,
153        &msg.ciphertext,
154        &c_prime,
155        x_j,
156        &beta_prime,
157        &r_prime,
158    );
159
160    Ok((
161        ProvedMessage2 {
162            ciphertext: c_prime,
163            respondent_proof,
164            beta: beta_prime.clone(),
165        },
166        beta_prime,
167    ))
168}
169
170/// Party i finishes the proved MtA: verify the respondent proof,
171/// then decrypt with i's OWN private key.
172///
173/// `ck_i` is party i's commitment key (the proof is addressed to i).
174/// Returns `α = k_i·x_j + β′`; pair with j's `β′` via `α − β′`. Only
175/// the initiator can run this step — the response is encrypted under
176/// i's key — which is exactly what keeps α and β′ in different hands.
177pub fn party_i_finish_proved(
178    i_public: &PaillierPublicKey,
179    ck_i: &CommitmentKey,
180    q: &BigUint,
181    msg1_ciphertext: &BigUint,
182    i_private: &PaillierPrivateKey,
183    msg: &ProvedMessage2,
184) -> Result<BigUint, MtaProofError> {
185    if !verify_respondent(
186        q,
187        i_public,
188        ck_i,
189        msg1_ciphertext,
190        &msg.ciphertext,
191        &msg.respondent_proof,
192    ) {
193        return Err(MtaProofError::InvalidProof);
194    }
195    let alpha = paillier_decrypt(i_private, i_public, &msg.ciphertext)?;
196    Ok(alpha)
197}
198
199/// Run the full proved MtA protocol between party i and party j,
200/// in-process (one coordinator holding i's keypair).
201///
202/// `i_keypair` is the INITIATOR's Paillier keypair — the exchange
203/// runs under it end to end. `ck_i`/`ck_j` are the parties'
204/// commitment keys (each proves against the OTHER's). Returns
205/// `(α, β′)` with `α − β′ = k_i·x_j` exactly (hence also mod q and
206/// mod N) — honest shares never wrap.
207pub fn full_mta_proved(
208    i_keypair: &PaillierKeypair,
209    ck_i: &CommitmentKey,
210    ck_j: &CommitmentKey,
211    q: &BigUint,
212    k_i: &BigUint,
213    x_j: &BigUint,
214) -> Result<(BigUint, BigUint), MtaProofError> {
215    let msg1 = party_i_init_proved(&i_keypair.public, ck_j, q, k_i)?;
216    let (msg2, beta) = party_j_respond_proved(&i_keypair.public, ck_i, ck_j, q, &msg1, x_j)?;
217    let alpha = party_i_finish_proved(
218        &i_keypair.public,
219        ck_i,
220        q,
221        &msg1.ciphertext,
222        &i_keypair.private,
223        &msg2,
224    )?;
225    Ok((alpha, beta))
226}
227
228fn random_below(n: &BigUint) -> BigUint {
229    let mut rng = OsRng;
230    loop {
231        let r = rng.gen_biguint(n.bits());
232        if r < *n && !r.is_zero() {
233            return r;
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use confium_tc::paillier::generate_keypair;
242
243    // Honest execution needs N > k·x + β' < q⁵ + q² ≈ 2^1282, so the
244    // test Paillier key uses 642-bit primes (~1284-bit N). Shared
245    // across the module's tests — keygen at this size takes seconds.
246    // The keypair plays the INITIATOR (the exchange runs under it).
247    pub(super) fn shared_fixtures()
248    -> &'static (PaillierKeypair, CommitmentKey, CommitmentKey, BigUint) {
249        use std::sync::OnceLock;
250        static FIX: OnceLock<(PaillierKeypair, CommitmentKey, CommitmentKey, BigUint)> =
251            OnceLock::new();
252        FIX.get_or_init(|| {
253            let kp = generate_keypair(642);
254            // 64-bit safe primes: enough structure for tests (NOT
255            // production strength — see the module docs).
256            let ck_i = crate::mta_proofs::generate_commitment_key(64);
257            let ck_j = crate::mta_proofs::generate_commitment_key(64);
258            // P-256 group order.
259            let q = BigUint::parse_bytes(
260                b"ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551",
261                16,
262            )
263            .unwrap();
264            (kp, ck_i, ck_j, q)
265        })
266    }
267
268    #[test]
269    fn proved_mta_shares_subtract_to_product() {
270        let (kp, ck_i, ck_j, q) = shared_fixtures();
271        let k_i = BigUint::from(42u32);
272        let x_j = BigUint::from(17u32);
273        let (alpha, beta) = full_mta_proved(kp, ck_i, ck_j, q, &k_i, &x_j).unwrap();
274        // α − β' = k_i * x_j exactly (the paper's share contract).
275        assert_eq!((&alpha - &beta) % q, (&k_i * &x_j) % q);
276    }
277
278    #[test]
279    fn proved_mta_no_modular_wrap() {
280        // With k, x < q the honest α = k·x + β' must be < N (no wrap).
281        let (kp, ck_i, ck_j, q) = shared_fixtures();
282        let k_i = q - BigUint::from(1u32);
283        let x_j = q - BigUint::from(2u32);
284        let (alpha, beta) = full_mta_proved(kp, ck_i, ck_j, q, &k_i, &x_j).unwrap();
285        assert!(alpha < kp.public.n);
286        assert_eq!((&alpha - &beta) % q, (&k_i * &x_j) % q);
287    }
288
289    #[test]
290    fn proved_mta_multiple_pairs() {
291        let (kp, ck_i, ck_j, q) = shared_fixtures();
292        for (k, x) in [(10u32, 20u32), (100u32, 50u32), (7u32, 13u32)] {
293            let k_i = BigUint::from(k);
294            let x_j = BigUint::from(x);
295            let (alpha, beta) = full_mta_proved(kp, ck_i, ck_j, q, &k_i, &x_j).unwrap();
296            assert_eq!((&alpha - &beta) % q, (&k_i * &x_j) % q, "pair ({k}, {x})");
297        }
298    }
299
300    #[test]
301    fn proved_mta_shares_neither_reveals_product() {
302        let (kp, ck_i, ck_j, q) = shared_fixtures();
303        let k_i = BigUint::from(42u32);
304        let x_j = BigUint::from(17u32);
305        let (alpha, beta) = full_mta_proved(kp, ck_i, ck_j, q, &k_i, &x_j).unwrap();
306        let product = &k_i * &x_j;
307        assert_ne!(alpha, product);
308        assert_ne!(beta, product);
309    }
310
311    #[test]
312    fn proved_mta_beta_is_random() {
313        let (kp, ck_i, ck_j, q) = shared_fixtures();
314        let k_i = BigUint::from(42u32);
315        let x_j = BigUint::from(17u32);
316        let (_, beta1) = full_mta_proved(kp, ck_i, ck_j, q, &k_i, &x_j).unwrap();
317        let (_, beta2) = full_mta_proved(kp, ck_i, ck_j, q, &k_i, &x_j).unwrap();
318        assert_ne!(beta1, beta2);
319    }
320}
321
322#[cfg(test)]
323mod adversarial_tests {
324    //! Paired rejects-forgery tests for every proof verifier.
325
326    use super::*;
327
328    fn shared_fixtures() -> &'static (PaillierKeypair, CommitmentKey, CommitmentKey, BigUint) {
329        crate::paillier_mta::tests::shared_fixtures()
330    }
331
332    #[test]
333    fn responder_rejects_tampered_ciphertext() {
334        // A ciphertext not built as c₁^x·Γ^{β'}·r^N must fail the
335        // respondent proof even if the proof itself is honest.
336        let (kp, ck_i, ck_j, q) = shared_fixtures();
337        let k_i = BigUint::from(42u32);
338        let x_j = BigUint::from(17u32);
339        let msg1 = party_i_init_proved(&kp.public, ck_j, q, &k_i).unwrap();
340        let (mut msg2, _) = party_j_respond_proved(&kp.public, ck_i, ck_j, q, &msg1, &x_j).unwrap();
341        msg2.ciphertext = &msg2.ciphertext * BigUint::from(2u32) % &kp.public.n_squared;
342        let err = party_i_finish_proved(&kp.public, ck_i, q, &msg1.ciphertext, &kp.private, &msg2);
343        assert!(matches!(err, Err(MtaProofError::InvalidProof)));
344    }
345
346    #[test]
347    fn responder_rejects_proof_for_different_statement() {
348        // Valid (c₁, proof) pair presented against a different c₁ —
349        // Fiat-Shamir binds the statement, so it must fail.
350        let (kp, ck_i, ck_j, q) = shared_fixtures();
351        let k_i = BigUint::from(42u32);
352        let x_j = BigUint::from(17u32);
353        let msg1 = party_i_init_proved(&kp.public, ck_j, q, &k_i).unwrap();
354        let (msg2, _) = party_j_respond_proved(&kp.public, ck_i, ck_j, q, &msg1, &x_j).unwrap();
355        let other_c1 = party_i_init_proved(&kp.public, ck_j, q, &BigUint::from(7u32))
356            .unwrap()
357            .ciphertext;
358        let err = party_i_finish_proved(&kp.public, ck_i, q, &other_c1, &kp.private, &msg2);
359        assert!(matches!(err, Err(MtaProofError::InvalidProof)));
360    }
361
362    #[test]
363    fn responder_rejects_tampered_response() {
364        let (kp, ck_i, ck_j, q) = shared_fixtures();
365        let k_i = BigUint::from(42u32);
366        let x_j = BigUint::from(17u32);
367        let msg1 = party_i_init_proved(&kp.public, ck_j, q, &k_i).unwrap();
368        let (mut msg2, _) = party_j_respond_proved(&kp.public, ck_i, ck_j, q, &msg1, &x_j).unwrap();
369        msg2.respondent_proof.s1 += BigUint::from(1u32);
370        let err = party_i_finish_proved(&kp.public, ck_i, q, &msg1.ciphertext, &kp.private, &msg2);
371        assert!(matches!(err, Err(MtaProofError::InvalidProof)));
372    }
373
374    #[test]
375    fn responder_rejects_secret_above_bound() {
376        // The responder's secret x = q⁵ (far past the proven q³ bound)
377        // must fail the s₁ ≤ q³ integer check. (The mask bound t₁ ≤ q⁷
378        // only becomes reachable with paper-sized N > q⁸; at test key
379        // sizes every encryptable mask is below q⁷ by construction.)
380        let (kp, ck_i, _ck_j, q) = shared_fixtures();
381        let k_i = BigUint::from(42u32);
382        let x_j = {
383            let q2 = q * q;
384            &q2 * &q2 * q
385        };
386        let msg1 = party_i_init_proved(&kp.public, ck_i, q, &k_i).unwrap();
387
388        let mut rng = OsRng;
389        let beta_prime = rng.gen_biguint_range(&BigUint::zero(), &(q * q));
390        let r_prime = random_below(&kp.public.n);
391        let c_mul = paillier_scalar_mul(&kp.public, &msg1.ciphertext, &x_j);
392        let c_beta = paillier_encrypt(&kp.public, &beta_prime, &r_prime).unwrap();
393        let c_prime = paillier_add(&kp.public, &c_mul, &c_beta);
394        let proof = prove_respondent(
395            q,
396            &kp.public,
397            ck_i,
398            &msg1.ciphertext,
399            &c_prime,
400            &x_j,
401            &beta_prime,
402            &r_prime,
403        );
404        let msg2 = ProvedMessage2 {
405            ciphertext: c_prime,
406            respondent_proof: proof,
407            beta: beta_prime,
408        };
409        let err = party_i_finish_proved(&kp.public, ck_i, q, &msg1.ciphertext, &kp.private, &msg2);
410        assert!(matches!(err, Err(MtaProofError::InvalidProof)));
411    }
412
413    #[test]
414    fn initiator_rejects_out_of_range_plaintext() {
415        // An honest proof over m = q⁵ (way past the q³ bound) fails
416        // the s₁ ≤ q³ check — the wrap-around attack is blocked.
417        let (kp, _ck_i, ck_j, q) = shared_fixtures();
418        let q5 = {
419            let q2 = q * q;
420            &q2 * &q2 * q
421        };
422        let r = random_below(&kp.public.n);
423        let c = paillier_encrypt(&kp.public, &q5, &r).unwrap();
424        let proof = prove_range(q, &kp.public, ck_j, &c, &q5, &r);
425        assert!(!verify_range(q, &kp.public, ck_j, &c, &proof));
426    }
427
428    #[test]
429    fn initiator_rejects_proof_for_a_different_ciphertext() {
430        let (kp, _ck_i, ck_j, q) = shared_fixtures();
431        let m = BigUint::from(12345u32);
432        let r = random_below(&kp.public.n);
433        let c = paillier_encrypt(&kp.public, &m, &r).unwrap();
434        let proof = prove_range(q, &kp.public, ck_j, &c, &m, &r);
435        let other_c = paillier_encrypt(&kp.public, &m, &random_below(&kp.public.n)).unwrap();
436        assert!(!verify_range(q, &kp.public, ck_j, &other_c, &proof));
437    }
438
439    #[test]
440    fn initiator_rejects_wrong_commitment_key() {
441        // A proof verified under a different commitment key must
442        // fail — the transcript binds the key.
443        let (kp, _ck_i, ck_j, q) = shared_fixtures();
444        let m = BigUint::from(12345u32);
445        let r = random_below(&kp.public.n);
446        let c = paillier_encrypt(&kp.public, &m, &r).unwrap();
447        let proof = prove_range(q, &kp.public, ck_j, &c, &m, &r);
448        let wrong_ck = crate::mta_proofs::generate_commitment_key(64);
449        assert!(!verify_range(q, &kp.public, &wrong_ck, &c, &proof));
450    }
451}