Skip to main content

confium_tc_gg18/
sign.rs

1//! GG18 threshold ECDSA signing over P-256.
2//!
3//! Consumes shares from [`crate::keygen`] and produces a standard
4//! `(r, s)` ECDSA signature verifiable under `p256::ecdsa::VerifyingKey`.
5//!
6//! ## Protocol (simplified — NOT production)
7//!
8//! Four rounds:
9//!
10//! - **Round 1 — nonce commit.** Broadcast `R_i = k_i * G` + 1-based idx.
11//! - **Round 2 — nonce reveal.** Broadcast `k_i`.
12//! - **Round 3 — partial sign.** From all reveals compute aggregate
13//!   `k = sum k_i`, `R = sum R_i`, `r = R.x mod n`, Lagrange weights.
14//!   Compute `s_i = k^{-1} * r * lambda_i * x_i`. Broadcast `s_i`.
15//! - **Round 4 — combine.** `s = k^{-1} * z + sum s_i` (z = H(m)).
16//!   Verify `(r, s)` against the joint public key; complete.
17//!
18//! The arithmetic is identical to a real GG18 run for honest coalitions.
19//! Nonces are revealed in the clear — this leaks the joint nonce `k`,
20//! which is safe for a single signature but would be catastrophic across
21//! multiple signatures over the same secret. Production GG18 hides `k`
22//! via Paillier-based MtA. See [`crate::mta`] for the gap.
23
24use elliptic_curve::Generate;
25use elliptic_curve::{PrimeField, ops::Invert, point::AffineCoordinates, sec1::ToSec1Point};
26use p256::FieldBytes;
27use p256::{AffinePoint, NonZeroScalar, ProjectivePoint, Scalar};
28use sha2::{Digest, Sha256};
29
30use confium_tc::Result;
31use confium_tc::message::Message;
32use confium_tc::registry::{RoundResult, SessionImpl};
33use confium_tc::session::SessionParams;
34
35use crate::error::{Gg18ErrorCode, scheme_error};
36use crate::lagrange;
37use crate::share::Gg18Share;
38
39/// GG18 signing scheme over P-256. Registered as `GG18-ECDSA-P256-SIGN`.
40pub struct Gg18SignP256;
41
42impl Gg18SignP256 {
43    pub fn build_session(params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
44        let party_id = params.parties.get(params.this_party_idx)?.id.clone();
45        let message = params.message.clone().unwrap_or_default();
46        let share_bytes = params
47            .local_share
48            .as_ref()
49            .map(|s| s.bytes().to_vec())
50            .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_SHARE))?;
51        let share = Gg18Share::from_bytes(&share_bytes)?;
52
53        let k_i = NonZeroScalar::generate();
54        let r_i_point = (ProjectivePoint::GENERATOR * *k_i).to_affine();
55
56        Ok(Box::new(Gg18SignSession {
57            party_id,
58            message,
59            share,
60            k_i,
61            r_i_point,
62            round1_seen: Vec::new(),
63            round2_seen: Vec::new(),
64            k_inv: None,
65            r_scalar: None,
66            z: None,
67            our_partial: None,
68            round_done: 0,
69            signature: None,
70        }))
71    }
72}
73
74pub struct Gg18SignSession {
75    party_id: String,
76    message: Vec<u8>,
77    share: Gg18Share,
78    k_i: NonZeroScalar,
79    r_i_point: AffinePoint,
80    round1_seen: Vec<(String, u64, AffinePoint)>,
81    round2_seen: Vec<(String, u64, Scalar)>,
82    k_inv: Option<Scalar>,
83    r_scalar: Option<Scalar>,
84    z: Option<Scalar>,
85    our_partial: Option<Scalar>,
86    round_done: u8,
87    signature: Option<Vec<u8>>,
88}
89
90const TAG_NONCE_POINT: u8 = 0xD1;
91const TAG_NONCE_REVEAL: u8 = 0xD2;
92const TAG_PARTIAL: u8 = 0xD3;
93
94impl Gg18SignSession {
95    fn round1_commit(&mut self) -> Result<RoundResult> {
96        let mut payload = Vec::with_capacity(1 + 1 + 33);
97        payload.push(TAG_NONCE_POINT);
98        payload.push(self.share.party_idx as u8);
99        payload.extend_from_slice(self.r_i_point.to_sec1_point(true).as_bytes());
100        Ok(RoundResult::new(
101            vec![Message::broadcast(&self.party_id, 1, payload)],
102            false,
103        ))
104    }
105
106    fn round2_reveal(&mut self, incoming: &[Message]) -> Result<RoundResult> {
107        for msg in incoming {
108            if msg.round != 1 || msg.payload.is_empty() {
109                continue;
110            }
111            if msg.payload[0] != TAG_NONCE_POINT {
112                continue;
113            }
114            if msg.payload.len() != 1 + 1 + 33 {
115                return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
116            }
117            let idx = msg.payload[1] as u64;
118            let pt = decode_affine(&msg.payload[2..35])?;
119            if msg.from_party_id == self.party_id {
120                if idx != self.share.party_idx as u64 {
121                    return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
122                }
123                continue;
124            }
125            self.round1_seen.push((msg.from_party_id.clone(), idx, pt));
126        }
127
128        let mut reveal = Vec::with_capacity(1 + 1 + 32);
129        reveal.push(TAG_NONCE_REVEAL);
130        reveal.push(self.share.party_idx as u8);
131        reveal.extend_from_slice(&self.k_i.to_bytes());
132        Ok(RoundResult::new(
133            vec![Message::broadcast(&self.party_id, 2, reveal)],
134            false,
135        ))
136    }
137
138    fn round3_partial(&mut self, incoming: &[Message]) -> Result<RoundResult> {
139        for msg in incoming {
140            if msg.round != 2 || msg.payload.is_empty() {
141                continue;
142            }
143            if msg.payload[0] != TAG_NONCE_REVEAL {
144                continue;
145            }
146            if msg.payload.len() != 1 + 1 + 32 {
147                return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
148            }
149            let idx = msg.payload[1] as u64;
150            let mut kb = [0u8; 32];
151            kb.copy_from_slice(&msg.payload[2..34]);
152            let fb: p256::FieldBytes = kb.into();
153            let k: Scalar = Option::from(Scalar::from_repr(fb))
154                .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE))?;
155            if msg.from_party_id == self.party_id {
156                continue;
157            }
158            self.round2_seen.push((msg.from_party_id.clone(), idx, k));
159        }
160
161        let mut parts: Vec<(u64, String, Scalar, AffinePoint)> = Vec::new();
162        parts.push((
163            self.share.party_idx as u64,
164            self.party_id.clone(),
165            *self.k_i,
166            self.r_i_point,
167        ));
168        for (pid, idx, k) in &self.round2_seen {
169            let r_j = self
170                .round1_seen
171                .iter()
172                .find(|(p, _, _)| p == pid)
173                .map(|(_, _, pt)| *pt)
174                .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE))?;
175            parts.push((*idx, pid.clone(), *k, r_j));
176        }
177        parts.sort_by_key(|(idx, _, _, _)| *idx);
178
179        let k_sum: Scalar = parts
180            .iter()
181            .map(|(_, _, k, _)| *k)
182            .fold(Scalar::ZERO, |a, b| a + b);
183        let k_nz: NonZeroScalar = Option::from(NonZeroScalar::new(k_sum))
184            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
185        let k_inv: Scalar = *k_nz.invert();
186
187        let mut r_proj = ProjectivePoint::IDENTITY;
188        for (_, _, _, pt) in &parts {
189            r_proj += ProjectivePoint::from(*pt);
190        }
191        if r_proj == ProjectivePoint::IDENTITY {
192            return Err(scheme_error(Gg18ErrorCode::INTERNAL));
193        }
194        let r_affine = r_proj.to_affine();
195        let r_scalar = reduce_x_mod_n(r_affine);
196
197        let z = hash_to_scalar(&self.message);
198
199        let xs_scalar: Vec<Scalar> = parts
200            .iter()
201            .map(|(idx, _, _, _)| Scalar::from(*idx))
202            .collect();
203        let our_idx_scalar = Scalar::from(self.share.party_idx as u64);
204        let lambda_i = lagrange::lagrange_basis_scalar(our_idx_scalar, &xs_scalar);
205        let our_partial = k_inv * r_scalar * lambda_i * self.share.scalar();
206
207        let mut partial_payload = Vec::with_capacity(1 + 1 + 32);
208        partial_payload.push(TAG_PARTIAL);
209        partial_payload.push(self.share.party_idx as u8);
210        partial_payload.extend_from_slice(&our_partial.to_bytes());
211
212        self.k_inv = Some(k_inv);
213        self.r_scalar = Some(r_scalar);
214        self.z = Some(z);
215        self.our_partial = Some(our_partial);
216
217        Ok(RoundResult::new(
218            vec![Message::broadcast(&self.party_id, 3, partial_payload)],
219            false,
220        ))
221    }
222
223    fn round4_combine(&mut self, incoming: &[Message]) -> Result<RoundResult> {
224        let mut partials: Vec<(u64, Scalar)> = Vec::new();
225        for msg in incoming {
226            if msg.round != 3 || msg.payload.is_empty() {
227                continue;
228            }
229            if msg.payload[0] != TAG_PARTIAL {
230                continue;
231            }
232            if msg.payload.len() != 1 + 1 + 32 {
233                return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
234            }
235            let idx = msg.payload[1] as u64;
236            let mut sb = [0u8; 32];
237            sb.copy_from_slice(&msg.payload[2..34]);
238            let fb: p256::FieldBytes = sb.into();
239            let s: Scalar = Option::from(Scalar::from_repr(fb))
240                .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE))?;
241            if msg.from_party_id == self.party_id {
242                continue;
243            }
244            partials.push((idx, s));
245        }
246        let our_partial = self
247            .our_partial
248            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
249        partials.push((self.share.party_idx as u64, our_partial));
250
251        let k_inv = self
252            .k_inv
253            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
254        let r_scalar = self
255            .r_scalar
256            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
257        let z = self
258            .z
259            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
260
261        let mut s = k_inv * z;
262        for (_, si) in &partials {
263            s += si;
264        }
265        let s = normalize_s_low(s);
266
267        let r_nz: NonZeroScalar = Option::from(NonZeroScalar::new(r_scalar))
268            .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_PARTIAL_SIGNATURE))?;
269        let s_nz: NonZeroScalar = Option::from(NonZeroScalar::new(s))
270            .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_PARTIAL_SIGNATURE))?;
271        let sig = p256::ecdsa::Signature::from_scalars(r_nz, s_nz)
272            .map_err(|_| scheme_error(Gg18ErrorCode::INTERNAL))?;
273        let vk = p256::ecdsa::VerifyingKey::from_affine(self.share.public_key)
274            .map_err(|_| scheme_error(Gg18ErrorCode::INTERNAL))?;
275        use p256::ecdsa::signature::Verifier;
276        vk.verify(&self.message, &sig)
277            .map_err(|_| scheme_error(Gg18ErrorCode::BAD_PARTIAL_SIGNATURE))?;
278
279        let mut out = Vec::with_capacity(64);
280        out.extend_from_slice(&r_scalar.to_bytes());
281        out.extend_from_slice(&s.to_bytes());
282        self.signature = Some(out);
283
284        Ok(RoundResult::done())
285    }
286}
287
288impl SessionImpl for Gg18SignSession {
289    fn round(&mut self, incoming: &[Message]) -> Result<RoundResult> {
290        self.round_done = self.round_done.checked_add(1).ok_or_else(|| {
291            confium_tc::error::RoundOverflowSnafu {
292                round: self.round_done,
293            }
294            .build()
295        })?;
296        match self.round_done {
297            1 => self.round1_commit(),
298            2 => self.round2_reveal(incoming),
299            3 => self.round3_partial(incoming),
300            4 => self.round4_combine(incoming),
301            other => Err(confium_tc::error::RoundOverflowSnafu { round: other }.build()),
302        }
303    }
304
305    fn result(&self) -> Result<Vec<u8>> {
306        if self.round_done < 4 {
307            return Err(confium_tc::error::SessionNotCompleteSnafu {}.build());
308        }
309        self.signature
310            .clone()
311            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))
312    }
313
314    fn destroy(&mut self) {
315        self.k_i = Option::from(NonZeroScalar::new(Scalar::ONE))
316            .unwrap_or_else(|| NonZeroScalar::new(Scalar::ONE).unwrap());
317        self.k_inv = None;
318        self.r_scalar = None;
319        self.z = None;
320        self.our_partial = None;
321        self.signature = None;
322    }
323}
324
325fn reduce_x_mod_n(point: AffinePoint) -> Scalar {
326    let x = point.x();
327    let x_bytes: &[u8] = x.as_slice();
328    let mut arr = [0u8; 32];
329    let n = x_bytes.len().min(32);
330    arr[..n].copy_from_slice(&x_bytes[..n]);
331    reduce_to_scalar(arr)
332}
333
334/// Reduce 32 bytes to a scalar by rejection sampling with re-hash;
335/// never falls back to a constant.
336fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
337    loop {
338        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
339            return s;
340        }
341        let mut h = Sha256::new();
342        h.update(b"confium-scalar-reduce-v1");
343        h.update(bytes);
344        bytes = h.finalize().into();
345    }
346}
347
348fn hash_to_scalar(message: &[u8]) -> Scalar {
349    let mut h = Sha256::new();
350    h.update(message);
351    let digest: [u8; 32] = h.finalize().into();
352    reduce_to_scalar(digest)
353}
354
355fn normalize_s_low(s: Scalar) -> Scalar {
356    use crypto_bigint::Limb;
357    use elliptic_curve::Curve;
358    use p256::NistP256;
359    let n_u = <NistP256 as Curve>::ORDER.get();
360    let half = n_u >> 1usize;
361    let s_u = decode_scalar_to_uint(s);
362    if s_u > half {
363        let (s_prime, _) = n_u.borrowing_sub(&s_u, Limb::ZERO);
364        let be = s_prime.to_be_bytes();
365        let src: &[u8] = be.as_slice();
366        let mut bytes = [0u8; 32];
367        let off = src.len().saturating_sub(32);
368        let take = src.len() - off;
369        bytes[..take].copy_from_slice(&src[off..]);
370        let fb: p256::FieldBytes = bytes.into();
371        Option::from(Scalar::from_repr(fb)).unwrap_or(s)
372    } else {
373        s
374    }
375}
376
377fn decode_scalar_to_uint(s: Scalar) -> p256::U256 {
378    use elliptic_curve::Curve;
379    use p256::NistP256;
380    let bytes = s.to_bytes();
381    <NistP256 as Curve>::Uint::from_be_slice(bytes.as_slice())
382}
383
384fn decode_affine(bytes: &[u8]) -> Result<AffinePoint> {
385    use elliptic_curve::sec1::FromSec1Point;
386    if bytes.len() != 33 {
387        return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
388    }
389    let enc = elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(bytes)
390        .map_err(|_| scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE))?;
391    let pt: AffinePoint = Option::from(AffinePoint::from_sec1_point(&enc))
392        .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE))?;
393    let _ = pt.x();
394    Ok(pt)
395}