Skip to main content

confium_tc_gg18/
inprocess.rs

1//! In-process synchronous driver for GG18 DKG and signing.
2//!
3//! Thin wrapper over [`confium_tc::inprocess`] that names the GG18
4//! schemes and pulls the joint public key out of the first share. All
5//! multi-round routing logic lives in the framework driver; this module
6//! is intentionally short.
7//!
8//! ## Output wire format
9//!
10//! [`keygen`] returns `(shares, public_key)` where:
11//!
12//! - `shares[i]` is the opaque `Gg18Share::to_bytes()` encoding for
13//!   party `i` (71 bytes: magic[4] | version[1] | x_i[32] | X[33] | idx[1]).
14//! - `public_key` is the 33-byte SEC1 compressed encoding of the joint
15//!   P-256 point.
16//!
17//! [`sign`] returns a 64-byte `r || s` ECDSA signature. Verify it with
18//! the [`p256::ecdsa`] crate's `VerifyingKey::verify`.
19//!
20//! ## Security note
21//!
22//! The underlying GG18 crate's MtA sub-round is a simplified in-clear
23//! stub. This driver inherits that property. See
24//! [`crate::mta`] for the gap.
25
26use elliptic_curve::sec1::ToSec1Point;
27use p256::AffinePoint;
28
29use confium_tc::Result;
30use confium_tc::inprocess as driver;
31
32use crate::share::Gg18Share;
33
34/// Outcome of a single GG18 DKG run: N share blobs plus the joint
35/// public key.
36#[derive(Debug, Clone)]
37pub struct KeygenOutput {
38    /// One share blob per party, in roster order (0-based index matches
39    /// `party_idx` of the share after subtracting 1).
40    pub shares: Vec<Vec<u8>>,
41    /// 33-byte SEC1 compressed encoding of the joint P-256 public key.
42    pub public_key: Vec<u8>,
43}
44
45/// Drive the GG18 DKG in-process for `party_count` parties at threshold
46/// `threshold`.
47///
48/// `threshold` must be in `1..=party_count`. All parties are in-process
49/// (`Party::inproc`), identified as `p0`, `p1`, … `p{n-1}`.
50pub fn keygen(threshold: u32, party_count: usize) -> Result<KeygenOutput> {
51    let shares = driver::run_dkg(crate::DKG_SCHEME_NAME, threshold, party_count)?;
52    let first = Gg18Share::from_bytes(&shares[0])?;
53    let public_key: Vec<u8> = first.public_key.to_sec1_point(true).as_bytes().to_vec();
54    Ok(KeygenOutput { shares, public_key })
55}
56
57/// Drive a GG18 signing session in-process using `share_blobs` (each a
58/// `Gg18Share::to_bytes()` blob from a previous [`keygen`]) at threshold
59/// `threshold`. Returns the 64-byte `(r, s)` ECDSA signature.
60///
61/// `share_blobs.len()` must be `>= threshold`. The supplied shares must
62/// all share the same joint public key — the protocol will otherwise
63/// silently produce an invalid signature.
64pub fn sign(share_blobs: &[Vec<u8>], threshold: u32, message: &[u8]) -> Result<Vec<u8>> {
65    driver::run_sign(crate::SIGN_SCHEME_NAME, share_blobs, threshold, message)
66}
67
68/// Sign N messages against the same joint key without re-running DKG.
69/// See [`confium_tc_cmp20::inprocess::sign_batch`] for performance notes.
70pub fn sign_batch(
71    share_blobs: &[Vec<u8>],
72    threshold: u32,
73    messages: &[&[u8]],
74) -> Result<Vec<Vec<u8>>> {
75    let mut out = Vec::with_capacity(messages.len());
76    for msg in messages {
77        out.push(sign(share_blobs, threshold, msg)?);
78    }
79    Ok(out)
80}
81
82/// Decode a 33-byte SEC1 compressed P-256 point. Public so bindings can
83/// verify the DKG-produced joint public key out-of-band.
84pub fn decode_public_key(bytes: &[u8]) -> Result<AffinePoint> {
85    use elliptic_curve::sec1::FromSec1Point;
86    if bytes.len() != 33 {
87        return Err(crate::error::scheme_error(
88            crate::error::Gg18ErrorCode::BAD_SHARE,
89        ));
90    }
91    let enc = elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(bytes)
92        .map_err(|_| crate::error::scheme_error(crate::error::Gg18ErrorCode::BAD_SHARE))?;
93    Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&enc))
94        .ok_or_else(|| crate::error::scheme_error(crate::error::Gg18ErrorCode::BAD_SHARE))
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
101
102    #[test]
103    fn keygen_and_sign_round_trip() {
104        let kg = keygen(2, 3).expect("dkg");
105        assert_eq!(kg.shares.len(), 3);
106        assert_eq!(kg.public_key.len(), 33);
107
108        let sig = sign(&kg.shares[..2], 2, b"hello gg18").expect("sign");
109        assert_eq!(sig.len(), 64);
110
111        let pk = decode_public_key(&kg.public_key).expect("pk decode");
112        let vk = VerifyingKey::from_affine(pk).expect("vk");
113        let s = Signature::from_slice(&sig).expect("parse sig");
114        vk.verify(b"hello gg18", &s).expect("verify ok");
115    }
116
117    #[test]
118    fn keygen_below_threshold_sign_errors() {
119        let kg = keygen(3, 5).expect("dkg");
120        let err = sign(&kg.shares[..2], 3, b"msg");
121        assert!(err.is_err());
122    }
123
124    #[test]
125    fn keygen_full_committee_signs_and_verifies() {
126        let kg = keygen(3, 3).expect("dkg");
127        let sig = sign(&kg.shares, 3, b"all-three").expect("sign");
128        let pk = decode_public_key(&kg.public_key).expect("pk decode");
129        let vk = VerifyingKey::from_affine(pk).expect("vk");
130        let s = Signature::from_slice(&sig).expect("parse sig");
131        vk.verify(b"all-three", &s).expect("verify ok");
132    }
133
134    #[test]
135    fn keygen_rejects_corrupt_share_magic() {
136        let kg = keygen(2, 3).expect("dkg");
137        let mut corrupt = kg.shares[0].clone();
138        corrupt[0] = b'X';
139        let err = sign(&[corrupt], 1, b"msg");
140        assert!(err.is_err());
141    }
142
143    #[test]
144    fn sign_batch_produces_one_sig_per_message() {
145        let kg = keygen(2, 3).expect("dkg");
146        let messages: Vec<&[u8]> = vec![b"msg-a", b"msg-b", b"msg-c", b"msg-d"];
147        let sigs = sign_batch(&kg.shares[..2], 2, &messages).expect("batch sign");
148        assert_eq!(sigs.len(), 4);
149        for s in &sigs {
150            assert_eq!(s.len(), 64);
151        }
152    }
153
154    #[test]
155    fn sign_batch_all_verify_under_joint_public_key() {
156        let kg = keygen(2, 3).expect("dkg");
157        let messages: Vec<&[u8]> = vec![b"a", b"b", b"c"];
158        let sigs = sign_batch(&kg.shares[..2], 2, &messages).expect("batch sign");
159        let pk = decode_public_key(&kg.public_key).expect("pk");
160        let vk = VerifyingKey::from_affine(pk).expect("vk");
161        for (msg, sig) in messages.iter().zip(sigs.iter()) {
162            let s = Signature::from_slice(sig).expect("parse sig");
163            vk.verify(msg, &s).expect("verify");
164        }
165    }
166
167    #[test]
168    fn sign_batch_empty_messages_returns_empty() {
169        let kg = keygen(2, 3).expect("dkg");
170        let sigs = sign_batch(&kg.shares[..2], 2, &[]).expect("empty batch");
171        assert!(sigs.is_empty());
172    }
173
174    #[test]
175    fn sign_batch_propagates_signing_error() {
176        let kg = keygen(3, 5).expect("dkg");
177        // threshold is 3 but we only supply 2 shares — each sign call must fail.
178        let messages: Vec<&[u8]> = vec![b"a", b"b"];
179        let err = sign_batch(&kg.shares[..2], 3, &messages);
180        assert!(err.is_err());
181    }
182}