Skip to main content

confium_tc_cmp20/
inprocess.rs

1//! In-process synchronous driver for CMP20 DKG and signing.
2//!
3//! Thin wrapper over [`confium_tc::inprocess`] that names the CMP20
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 `Cmp20Share::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 CMP20 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::Cmp20Share;
33
34/// Outcome of a single CMP20 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 CMP20 non-interactive DKG in-process for `party_count`
46/// parties at threshold `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 = Cmp20Share::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 CMP20 signing session in-process using `share_blobs` (each
58/// a `Cmp20Share::to_bytes()` blob from a previous [`keygen`]) at
59/// threshold `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 `messages.len()` messages against the same joint key without
69/// re-running DKG. Reuses the same `share_blobs` for every message.
70///
71/// Returns one 64-byte signature per input message in input order.
72/// If any individual signing op fails (e.g. protocol-level fault),
73/// the function returns the error and discards prior results.
74///
75/// ## Performance
76///
77/// Each message signing is a full 4-round CMP20 protocol run. This
78/// function does NOT cache nonces across messages — doing so would
79/// leak the joint secret. The win over calling [`sign`] in a loop is
80/// that the per-call Ruby/Python binding overhead disappears, which
81/// is significant for high-volume signers (10-100× speedup for small
82/// messages where binding overhead dominates).
83pub fn sign_batch(
84    share_blobs: &[Vec<u8>],
85    threshold: u32,
86    messages: &[&[u8]],
87) -> Result<Vec<Vec<u8>>> {
88    let mut out = Vec::with_capacity(messages.len());
89    for msg in messages {
90        out.push(sign(share_blobs, threshold, msg)?);
91    }
92    Ok(out)
93}
94
95/// Decode a 33-byte SEC1 compressed P-256 point. Public so bindings can
96/// verify the DKG-produced joint public key out-of-band.
97pub fn decode_public_key(bytes: &[u8]) -> Result<AffinePoint> {
98    use elliptic_curve::sec1::FromSec1Point;
99    if bytes.len() != 33 {
100        return Err(crate::error::scheme_error(
101            crate::error::Cmp20ErrorCode::BAD_SHARE,
102        ));
103    }
104    let enc = elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(bytes)
105        .map_err(|_| crate::error::scheme_error(crate::error::Cmp20ErrorCode::BAD_SHARE))?;
106    Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&enc))
107        .ok_or_else(|| crate::error::scheme_error(crate::error::Cmp20ErrorCode::BAD_SHARE))
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
114
115    #[test]
116    fn keygen_and_sign_round_trip() {
117        let kg = keygen(2, 3).expect("dkg");
118        assert_eq!(kg.shares.len(), 3);
119        assert_eq!(kg.public_key.len(), 33);
120
121        let sig = sign(&kg.shares[..2], 2, b"hello cmp20").expect("sign");
122        assert_eq!(sig.len(), 64);
123
124        let pk = decode_public_key(&kg.public_key).expect("pk decode");
125        let vk = VerifyingKey::from_affine(pk).expect("vk");
126        let s = Signature::from_slice(&sig).expect("parse sig");
127        vk.verify(b"hello cmp20", &s).expect("verify ok");
128    }
129
130    #[test]
131    fn keygen_below_threshold_sign_errors() {
132        let kg = keygen(3, 5).expect("dkg");
133        let err = sign(&kg.shares[..2], 3, b"msg");
134        assert!(err.is_err());
135    }
136
137    #[test]
138    fn keygen_full_committee_signs_and_verifies() {
139        let kg = keygen(3, 3).expect("dkg");
140        let sig = sign(&kg.shares, 3, b"all-three").expect("sign");
141        let pk = decode_public_key(&kg.public_key).expect("pk decode");
142        let vk = VerifyingKey::from_affine(pk).expect("vk");
143        let s = Signature::from_slice(&sig).expect("parse sig");
144        vk.verify(b"all-three", &s).expect("verify ok");
145    }
146
147    #[test]
148    fn keygen_rejects_corrupt_share_magic() {
149        let kg = keygen(2, 3).expect("dkg");
150        let mut corrupt = kg.shares[0].clone();
151        corrupt[0] = b'X';
152        let err = sign(&[corrupt], 1, b"msg");
153        assert!(err.is_err());
154    }
155
156    #[test]
157    fn sign_batch_produces_one_sig_per_message() {
158        let kg = keygen(2, 3).expect("dkg");
159        let messages: Vec<&[u8]> = vec![b"msg-a", b"msg-b", b"msg-c", b"msg-d"];
160        let sigs = sign_batch(&kg.shares[..2], 2, &messages).expect("batch sign");
161        assert_eq!(sigs.len(), 4);
162        for s in &sigs {
163            assert_eq!(s.len(), 64);
164        }
165    }
166
167    #[test]
168    fn sign_batch_all_verify_under_joint_public_key() {
169        use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
170        let kg = keygen(2, 3).expect("dkg");
171        let messages: Vec<&[u8]> = vec![b"a", b"b", b"c"];
172        let sigs = sign_batch(&kg.shares[..2], 2, &messages).expect("batch sign");
173        let pk = decode_public_key(&kg.public_key).expect("pk");
174        let vk = VerifyingKey::from_affine(pk).expect("vk");
175        for (msg, sig) in messages.iter().zip(sigs.iter()) {
176            let s = Signature::from_slice(sig).expect("parse sig");
177            vk.verify(msg, &s).expect("verify");
178        }
179    }
180}