Skip to main content

confium_tc_gg18/
keygen.rs

1//! GG18 distributed key generation over P-256.
2//!
3//! Each party acts as a Feldman-VSS dealer for a fresh random secret.
4//! After both rounds every party holds a combined share
5//! `x_i = sum_d f_d(i)` and the joint public key
6//! `X = prod_d g^{secret_d} = g^{sum secret_d}`. The combined secret is
7//! never reconstructed.
8//!
9//! ## Rounds
10//!
11//! - **Round 1 — deal.** Broadcast Feldman commitments + direct-send
12//!   each peer its polynomial evaluation.
13//! - **Round 2 — verify and assemble.** Verify every received share
14//!   against the broadcast commitments, sum per-dealer shares into
15//!   `x_i`, compute the joint public key. Complete.
16
17use elliptic_curve::PrimeField;
18use elliptic_curve::rand_core::UnwrapErr;
19use getrandom::SysRng;
20use p256::{AffinePoint, ProjectivePoint, Scalar};
21
22use confium_tc::Result;
23use confium_tc::message::Message;
24use confium_tc::registry::{RoundResult, SessionImpl};
25use confium_tc::session::SessionParams;
26
27use crate::error::{Gg18ErrorCode, scheme_error};
28use crate::share::{Gg18Share, SHARE_BYTES};
29use crate::vss::FeldmanVss;
30
31/// GG18 DKG scheme over P-256. Registered as `GG18-ECDSA-P256`.
32pub struct Gg18DkgP256;
33
34impl Gg18DkgP256 {
35    pub fn build_session(params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
36        let party_id = params.parties.get(params.this_party_idx)?.id.clone();
37        let n = params.parties.len();
38        let t = params.threshold as usize;
39        let party_idx_1based = (params.this_party_idx + 1) as u32;
40        let party_ids: Vec<String> = params
41            .parties
42            .parties()
43            .iter()
44            .map(|p| p.id.clone())
45            .collect();
46
47        let vss = FeldmanVss::deal(&mut UnwrapErr(SysRng), n, t);
48
49        Ok(Box::new(Gg18DkgSession {
50            party_id,
51            party_idx_1based,
52            party_ids,
53            n,
54            t,
55            our_vss: vss,
56            received_shares: Vec::new(),
57            joint_public_key: None,
58            our_combined_share: None,
59            round_done: 0,
60        }))
61    }
62}
63
64pub struct Gg18DkgSession {
65    party_id: String,
66    party_idx_1based: u32,
67    party_ids: Vec<String>,
68    n: usize,
69    t: usize,
70    our_vss: FeldmanVss,
71    received_shares: Vec<(u64, Scalar)>,
72    joint_public_key: Option<AffinePoint>,
73    our_combined_share: Option<Scalar>,
74    round_done: u8,
75}
76
77const TAG_COMMITMENTS: u8 = 0xCC;
78const TAG_SHARE: u8 = 0xCE;
79
80impl Gg18DkgSession {
81    fn round1_deal(&mut self) -> Result<RoundResult> {
82        let mut outgoing = Vec::with_capacity(1 + self.n);
83        let commitments_bytes = FeldmanVss::encode_commitments(&self.our_vss.commitments);
84        let mut bc_payload = Vec::with_capacity(2 + commitments_bytes.len());
85        bc_payload.push(TAG_COMMITMENTS);
86        bc_payload.push(self.party_idx_1based as u8);
87        bc_payload.push(self.our_vss.commitments.len() as u8);
88        bc_payload.extend_from_slice(&commitments_bytes);
89        outgoing.push(Message::broadcast(&self.party_id, 1, bc_payload));
90
91        for (peer_pos, peer_id) in self.party_ids.iter().enumerate() {
92            if peer_id == &self.party_id {
93                continue;
94            }
95            let eval = self.our_vss.shares[peer_pos];
96            let mut payload = Vec::with_capacity(2 + 32);
97            payload.push(TAG_SHARE);
98            payload.push(self.party_idx_1based as u8);
99            payload.extend_from_slice(&eval.to_bytes());
100            outgoing.push(Message::directed(&self.party_id, peer_id, 1, payload));
101        }
102        Ok(RoundResult::new(outgoing, false))
103    }
104
105    fn round2_assemble(&mut self, incoming: &[Message]) -> Result<RoundResult> {
106        let mut commitments_by_dealer: Vec<(u64, Vec<AffinePoint>)> = Vec::new();
107        let mut own_evaluations: Vec<(u64, Scalar)> = Vec::new();
108
109        for msg in incoming {
110            if msg.payload.is_empty() {
111                continue;
112            }
113            let tag = msg.payload[0];
114            match tag {
115                TAG_COMMITMENTS => {
116                    if msg.payload.len() < 3 {
117                        return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
118                    }
119                    let dealer_idx = msg.payload[1] as u64;
120                    let num_c = msg.payload[2] as usize;
121                    let expected = 3 + num_c * 33;
122                    if msg.payload.len() != expected {
123                        return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
124                    }
125                    let cs = FeldmanVss::decode_commitments(&msg.payload[3..expected])
126                        .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE))?;
127                    if cs.len() != num_c || cs.len() < self.t {
128                        return Err(scheme_error(Gg18ErrorCode::VSS_VERIFY_FAILED));
129                    }
130                    commitments_by_dealer.push((dealer_idx, cs));
131                }
132                TAG_SHARE => {
133                    if msg.payload.len() != 2 + 32 {
134                        return Err(scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE));
135                    }
136                    if !msg.is_for(&self.party_id) {
137                        continue;
138                    }
139                    let dealer_idx = msg.payload[1] as u64;
140                    let mut eval_bytes = [0u8; 32];
141                    eval_bytes.copy_from_slice(&msg.payload[2..34]);
142                    let fb: p256::FieldBytes = eval_bytes.into();
143                    let eval: Scalar = Option::from(Scalar::from_repr(fb))
144                        .ok_or_else(|| scheme_error(Gg18ErrorCode::BAD_ROUND_MESSAGE))?;
145                    own_evaluations.push((dealer_idx, eval));
146                }
147                _ => continue,
148            }
149        }
150
151        // Include our own self-evaluation. We did not receive our own
152        // broadcast, so fold our own commitments into the dealer map so
153        // the verification lookup below succeeds for the self-share.
154        own_evaluations.push((
155            self.party_idx_1based as u64,
156            self.our_vss.shares[self.party_idx_1based as usize - 1],
157        ));
158        let self_idx = self.party_idx_1based as u64;
159        if !commitments_by_dealer.iter().any(|(d, _)| *d == self_idx) {
160            commitments_by_dealer.push((self_idx, self.our_vss.commitments.clone()));
161        }
162
163        let mut verified_shares: Vec<(u64, Scalar)> = Vec::new();
164        for (dealer_idx, eval) in &own_evaluations {
165            let commitments = commitments_by_dealer
166                .iter()
167                .find(|(d, _)| d == dealer_idx)
168                .map(|(_, c)| c.as_slice())
169                .ok_or_else(|| scheme_error(Gg18ErrorCode::VSS_VERIFY_FAILED))?;
170            if !FeldmanVss::verify_share(commitments, self.party_idx_1based as u64, *eval) {
171                return Err(scheme_error(Gg18ErrorCode::VSS_VERIFY_FAILED));
172            }
173            verified_shares.push((*dealer_idx, *eval));
174        }
175
176        let distinct_dealers: std::collections::HashSet<u64> =
177            verified_shares.iter().map(|(d, _)| *d).collect();
178        if distinct_dealers.len() < self.t {
179            return Err(scheme_error(Gg18ErrorCode::BELOW_THRESHOLD));
180        }
181
182        let combined: Scalar = verified_shares
183            .iter()
184            .fold(Scalar::ZERO, |acc, &(_, ev)| acc + ev);
185        self.received_shares = verified_shares;
186        self.our_combined_share = Some(combined);
187
188        // joint public key X = product over all dealers of C_0^{(d)}.
189        // Our own commitment was folded into commitments_by_dealer above.
190        let mut joint = ProjectivePoint::IDENTITY;
191        for (_, cs) in &commitments_by_dealer {
192            joint += ProjectivePoint::from(cs[0]);
193        }
194        self.joint_public_key = Some(joint.to_affine());
195
196        Ok(RoundResult::done())
197    }
198}
199
200impl SessionImpl for Gg18DkgSession {
201    fn round(&mut self, incoming: &[Message]) -> Result<RoundResult> {
202        self.round_done = self.round_done.checked_add(1).ok_or_else(|| {
203            confium_tc::error::RoundOverflowSnafu {
204                round: self.round_done,
205            }
206            .build()
207        })?;
208        match self.round_done {
209            1 => self.round1_deal(),
210            2 => self.round2_assemble(incoming),
211            other => Err(confium_tc::error::RoundOverflowSnafu { round: other }.build()),
212        }
213    }
214
215    fn result(&self) -> Result<Vec<u8>> {
216        if self.round_done < 2 {
217            return Err(confium_tc::error::SessionNotCompleteSnafu {}.build());
218        }
219        let combined = self
220            .our_combined_share
221            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
222        let pk = self
223            .joint_public_key
224            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
225        let x_i: p256::NonZeroScalar = Option::from(p256::NonZeroScalar::new(combined))
226            .ok_or_else(|| scheme_error(Gg18ErrorCode::INTERNAL))?;
227        let share = Gg18Share::from_parts(x_i, pk, self.party_idx_1based);
228        Ok(share.to_bytes())
229    }
230
231    fn destroy(&mut self) {
232        if let Some(s) = self.our_combined_share.take() {
233            let _ = s;
234        }
235        for (_, s) in self.received_shares.drain(..) {
236            let _ = s;
237        }
238        self.our_vss.shares.fill(Scalar::ZERO);
239    }
240}
241
242/// Parse a DKG-produced share blob.
243pub fn parse_share(bytes: &[u8]) -> Result<Gg18Share> {
244    if bytes.len() != SHARE_BYTES {
245        return Err(scheme_error(Gg18ErrorCode::BAD_SHARE));
246    }
247    Gg18Share::from_bytes(bytes)
248}
249
250#[cfg(test)]
251pub(crate) fn reconstruct_secret_for_test(shares: &[Gg18Share]) -> Scalar {
252    use crate::lagrange;
253    let pairs: Vec<(Scalar, Scalar)> = shares
254        .iter()
255        .map(|s| (Scalar::from(s.party_idx), s.scalar()))
256        .collect();
257    lagrange::lagrange_weighted_sum(&pairs)
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use confium_tc::party::{Party, PartyList};
264    use confium_tc::share::Share;
265    use elliptic_curve::sec1::ToSec1Point;
266
267    fn params(n: usize, t: u32, idx: usize) -> SessionParams {
268        let roster: Vec<Party> = (0..n).map(|i| Party::inproc(format!("p{}", i))).collect();
269        SessionParams {
270            scheme: crate::DKG_SCHEME_NAME.to_string(),
271            parties: PartyList::from_parties(roster),
272            threshold: t,
273            this_party_idx: idx,
274            local_share: None,
275            message: None,
276        }
277    }
278
279    fn run_dkg(n: usize, t: u32) -> Vec<Gg18Share> {
280        let party_ids: Vec<String> = (0..n).map(|i| format!("p{}", i)).collect();
281        let mut sessions: Vec<Box<dyn SessionImpl>> = (0..n)
282            .map(|i| {
283                let p = params(n, t, i);
284                Gg18DkgP256::build_session(&p).expect("session")
285            })
286            .collect();
287
288        let mut outgoing_r1: Vec<Vec<Message>> = Vec::new();
289        for sess in sessions.iter_mut() {
290            let r = sess.round(&[]).expect("round 1");
291            outgoing_r1.push(r.outgoing);
292        }
293
294        let mut incoming_r2: Vec<Vec<Message>> = vec![Vec::new(); n];
295        for (sender_pos, outs) in outgoing_r1.iter().enumerate() {
296            for m in outs {
297                for (recv_pos, pid) in party_ids.iter().enumerate() {
298                    if recv_pos == sender_pos {
299                        continue;
300                    }
301                    if m.is_for(pid) {
302                        incoming_r2[recv_pos].push(m.clone());
303                    }
304                }
305            }
306        }
307
308        for (i, sess) in sessions.iter_mut().enumerate() {
309            let r = sess.round(&incoming_r2[i]).expect("round 2");
310            assert!(r.complete, "DKG must complete in round 2");
311        }
312
313        sessions
314            .iter()
315            .map(|s| {
316                let bytes = s.result().expect("result");
317                Gg18Share::from_bytes(&bytes).expect("share decodes")
318            })
319            .collect()
320    }
321
322    #[test]
323    fn dkg_two_of_three_produces_consistent_shares() {
324        let shares = run_dkg(3, 2);
325        assert_eq!(shares.len(), 3);
326        let pk0 = shares[0].public_key;
327        for s in &shares[1..] {
328            let a = pk0.to_sec1_point(true);
329            let b = s.public_key.to_sec1_point(true);
330            assert_eq!(a.as_bytes(), b.as_bytes(), "joint public key must match");
331        }
332        let secret_01 = reconstruct_secret_for_test(&shares[0..2]);
333        let secret_02 = reconstruct_secret_for_test(&[shares[0].clone(), shares[2].clone()]);
334        let secret_12 = reconstruct_secret_for_test(&shares[1..3]);
335        assert_eq!(secret_01, secret_02);
336        assert_eq!(secret_02, secret_12);
337        let g = ProjectivePoint::GENERATOR;
338        let expected_pk = (g * secret_01).to_affine();
339        let got_pk = shares[0].public_key.to_sec1_point(true);
340        let want_pk = expected_pk.to_sec1_point(true);
341        assert_eq!(got_pk.as_bytes(), want_pk.as_bytes());
342    }
343
344    #[test]
345    fn dkg_three_of_three_produces_consistent_shares() {
346        let shares = run_dkg(3, 3);
347        let secret = reconstruct_secret_for_test(&shares);
348        let g = ProjectivePoint::GENERATOR;
349        let pk = (g * secret).to_affine().to_sec1_point(true);
350        assert_eq!(
351            pk.as_bytes(),
352            shares[0].public_key.to_sec1_point(true).as_bytes()
353        );
354    }
355
356    #[test]
357    fn dkg_share_is_loadable_as_framework_share() {
358        let shares = run_dkg(3, 2);
359        let bytes = shares[0].to_bytes();
360        let fw = Share::new(crate::DKG_SCHEME_NAME, bytes);
361        assert_eq!(fw.scheme(), crate::DKG_SCHEME_NAME);
362        let rt = Share::from_bytes(&fw.to_bytes()).expect("framework decode");
363        assert_eq!(rt.scheme(), crate::DKG_SCHEME_NAME);
364        let inner = Gg18Share::from_bytes(rt.bytes()).expect("inner decode");
365        assert_eq!(inner.party_idx, shares[0].party_idx);
366    }
367}