Skip to main content

confium_tc_cmp20/
gg18_e2e.rs

1//! GG18 end-to-end threshold ECDSA signing.
2//!
3//! Mirrors the CMP20 e2e pipeline but for GG18 (4-round protocol).
4
5use crate::paillier_mta;
6use confium_tc::paillier::{self, PaillierKeypair};
7use getrandom::SysRng;
8use num_bigint::BigUint;
9use p256::FieldBytes;
10use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
11use p256::elliptic_curve::rand_core::UnwrapErr;
12use p256::elliptic_curve::{Field, PrimeField};
13use p256::{AffinePoint, ProjectivePoint, Scalar};
14use sha2::{Digest, Sha256};
15
16/// GG18 signing pipeline (4-round protocol).
17pub struct Gg18SigningPipeline {
18    pub threshold: u32,
19    pub party_count: u32,
20    pub paillier_keys: Vec<PaillierKeypair>,
21    pub key_shares: Vec<Scalar>,
22    pub public_key: AffinePoint,
23}
24
25impl Gg18SigningPipeline {
26    pub fn new(threshold: u32, party_count: u32, key_shares: Vec<Scalar>) -> Self {
27        // 642-bit primes: the MtA proofs require N > q⁵ + q².
28        let paillier_keys: Vec<PaillierKeypair> = (0..party_count)
29            .map(|_| paillier::generate_keypair(642))
30            .collect();
31        let public_key = {
32            let mut sum = ProjectivePoint::IDENTITY;
33            for x in &key_shares {
34                sum += ProjectivePoint::GENERATOR * x;
35            }
36            sum.to_affine()
37        };
38        Self {
39            threshold,
40            party_count,
41            paillier_keys,
42            key_shares,
43            public_key,
44        }
45    }
46
47    /// Run the GG18 4-round signing protocol for a message.
48    #[allow(clippy::needless_range_loop)]
49    pub fn sign(&self, message: &[u8]) -> Result<Signature, String> {
50        let n = self.party_count as usize;
51
52        // Round 1: each party generates nonce pair (k_i, gamma_i)
53        let nonces: Vec<Scalar> = (0..n)
54            .map(|_| Scalar::random(&mut UnwrapErr(SysRng)))
55            .collect();
56
57        // Per-sign commitment keys (demo path — a deployment would
58        // generate these once per party at keygen).
59        let commitment_keys: Vec<crate::mta_proofs::CommitmentKey> = (0..self.party_count as usize)
60            .map(|_| crate::mta_proofs::generate_commitment_key(64))
61            .collect();
62
63        // Round 2: MtA for k_i * x_j (same as CMP20)
64        for i in 0..n {
65            for j in 0..n {
66                if i == j {
67                    continue;
68                }
69                let k_i_big = scalar_to_biguint(&nonces[i]);
70                let x_j_big = scalar_to_biguint(&self.key_shares[j]);
71                let _ = paillier_mta::full_mta_proved(
72                    &self.paillier_keys[i],
73                    &commitment_keys[i],
74                    &commitment_keys[j],
75                    &crate::mta_proofs::p256_order(),
76                    &k_i_big,
77                    &x_j_big,
78                )
79                .map_err(|e| format!("MtA failed: {e}"))?;
80            }
81        }
82
83        // Compute R = sum(k_i) * G
84        let mut k_sum = ProjectivePoint::IDENTITY;
85        for k in &nonces {
86            k_sum += ProjectivePoint::GENERATOR * k;
87        }
88        let r_point = k_sum.to_affine();
89        let r = x_coordinate(&r_point);
90        if r == Scalar::ZERO {
91            return Err("r is zero".into());
92        }
93
94        // Hash
95        let e = hash_to_scalar(message);
96
97        // s = k^{-1} * (e + r * x) where k = sum(k_i), x = sum(x_i)
98        let k_total: Scalar = nonces.iter().copied().fold(Scalar::ZERO, |a, b| a + b);
99        let x_total: Scalar = self
100            .key_shares
101            .iter()
102            .copied()
103            .fold(Scalar::ZERO, |a, b| a + b);
104        let k_inv = invert_scalar(&k_total);
105        let s = k_inv * (e + r * x_total);
106        if s == Scalar::ZERO {
107            return Err("s is zero".into());
108        }
109
110        let r_bytes: [u8; 32] = r.to_repr().into();
111        let s_bytes: [u8; 32] = s.to_repr().into();
112        Signature::from_scalars(r_bytes, s_bytes).map_err(|e| format!("sig: {e}"))
113    }
114
115    pub fn verify(&self, message: &[u8], signature: &Signature) -> bool {
116        let vk = match VerifyingKey::from_affine(self.public_key) {
117            Ok(vk) => vk,
118            Err(_) => return false,
119        };
120        vk.verify(message, signature).is_ok()
121    }
122}
123
124fn scalar_to_biguint(s: &Scalar) -> BigUint {
125    let bytes: [u8; 32] = s.to_repr().into();
126    BigUint::from_bytes_be(&bytes)
127}
128
129/// Reduce 32 bytes to a scalar by rejection sampling with re-hash;
130/// never falls back to a constant.
131fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
132    loop {
133        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
134            return s;
135        }
136        let mut h = Sha256::new();
137        h.update(b"confium-scalar-reduce-v1");
138        h.update(bytes);
139        bytes = h.finalize().into();
140    }
141}
142
143fn x_coordinate(point: &AffinePoint) -> Scalar {
144    use p256::elliptic_curve::sec1::ToSec1Point;
145    let encoded = point.to_sec1_point(false);
146    if let Some(x_bytes) = encoded.x() {
147        let mut arr = [0u8; 32];
148        arr.copy_from_slice(x_bytes);
149        reduce_to_scalar(arr)
150    } else {
151        Scalar::ZERO
152    }
153}
154
155fn hash_to_scalar(message: &[u8]) -> Scalar {
156    let mut hasher = Sha256::new();
157    hasher.update(message);
158    let bytes: [u8; 32] = hasher.finalize().into();
159    reduce_to_scalar(bytes)
160}
161
162fn invert_scalar(s: &Scalar) -> Scalar {
163    // Garbage-in-garbage-out on zero input; protocol callers pass
164    // non-zero scalars (sweep ledger: SEC-audit-notes).
165    Option::<Scalar>::from(s.invert()).unwrap_or(Scalar::ZERO)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    fn random_scalar() -> Scalar {
173        Scalar::random(&mut UnwrapErr(SysRng))
174    }
175
176    #[test]
177    fn gg18_sign_and_verify() {
178        let shares = vec![random_scalar(), random_scalar(), random_scalar()];
179        let pipeline = Gg18SigningPipeline::new(2, 3, shares);
180        let sig = pipeline.sign(b"gg18 test").unwrap();
181        assert!(pipeline.verify(b"gg18 test", &sig));
182    }
183
184    #[test]
185    fn wrong_message_fails() {
186        let shares = vec![random_scalar(), random_scalar()];
187        let pipeline = Gg18SigningPipeline::new(2, 2, shares);
188        let sig = pipeline.sign(b"correct").unwrap();
189        assert!(!pipeline.verify(b"wrong", &sig));
190    }
191
192    #[test]
193    fn five_parties() {
194        let shares: Vec<Scalar> = (0..5).map(|_| random_scalar()).collect();
195        let pipeline = Gg18SigningPipeline::new(3, 5, shares);
196        let sig = pipeline.sign(b"3-of-5").unwrap();
197        assert!(pipeline.verify(b"3-of-5", &sig));
198    }
199}