Skip to main content

confium_tc_cmp20/
share.rs

1//! Per-party share material produced by CMP20 DKG and consumed by signing.
2
3use elliptic_curve::sec1::ToSec1Point;
4use p256::{AffinePoint, FieldBytes, NonZeroScalar, Scalar};
5use zeroize::Zeroize;
6
7use crate::error::{Cmp20ErrorCode, Result, scheme_error};
8
9const SHARE_MAGIC: [u8; 4] = *b"CMP2";
10const SHARE_VERSION: u8 = 1;
11/// Wire length: magic[4] | version[1] | x_i[32] | X[33] | idx[1].
12pub const SHARE_BYTES: usize = 4 + 1 + 32 + 33 + 1;
13
14/// One party's durable CMP20 secret material.
15#[derive(Clone)]
16pub struct Cmp20Share {
17    /// This party's Shamir share of the joint secret.
18    pub x_i: NonZeroScalar,
19    /// The shared public key `X = g^x` (affine).
20    pub public_key: AffinePoint,
21    /// 1-based DKG roster index of this party.
22    pub party_idx: u32,
23}
24
25impl std::fmt::Debug for Cmp20Share {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("Cmp20Share")
28            .field("x_i", &"<redacted>")
29            .field("party_idx", &self.party_idx)
30            .finish_non_exhaustive()
31    }
32}
33
34impl Drop for Cmp20Share {
35    fn drop(&mut self) {
36        let mut bytes = self.x_i.to_bytes();
37        bytes.zeroize();
38    }
39}
40
41impl Cmp20Share {
42    pub fn to_bytes(&self) -> Vec<u8> {
43        let mut out = Vec::with_capacity(SHARE_BYTES);
44        out.extend_from_slice(&SHARE_MAGIC);
45        out.push(SHARE_VERSION);
46        out.extend_from_slice(&self.x_i.to_bytes());
47        out.extend_from_slice(self.public_key.to_sec1_point(true).as_bytes());
48        out.push(self.party_idx as u8);
49        out
50    }
51
52    pub fn from_bytes(data: &[u8]) -> Result<Self> {
53        if data.len() != SHARE_BYTES {
54            return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
55        }
56        if data[0..4] != SHARE_MAGIC {
57            return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
58        }
59        if data[4] != SHARE_VERSION {
60            return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
61        }
62        let mut x_i_bytes = [0u8; 32];
63        x_i_bytes.copy_from_slice(&data[5..37]);
64        let fb: FieldBytes = x_i_bytes.into();
65        let x_i: NonZeroScalar = Option::from(NonZeroScalar::from_repr(fb))
66            .ok_or_else(|| scheme_error(Cmp20ErrorCode::BAD_SHARE))?;
67        let pk_bytes = &data[37..70];
68        let public_key = decode_affine(pk_bytes)?;
69        let party_idx = data[70] as u32;
70        Ok(Cmp20Share {
71            x_i,
72            public_key,
73            party_idx,
74        })
75    }
76
77    pub fn from_parts(x_i: NonZeroScalar, public_key: AffinePoint, party_idx: u32) -> Self {
78        Cmp20Share {
79            x_i,
80            public_key,
81            party_idx,
82        }
83    }
84
85    pub fn scalar(&self) -> Scalar {
86        *self.x_i
87    }
88}
89
90/// Decode a 33-byte SEC1 compressed point into an [`AffinePoint`].
91pub(crate) fn decode_affine(bytes: &[u8]) -> Result<AffinePoint> {
92    use elliptic_curve::point::AffineCoordinates;
93    use elliptic_curve::sec1::FromSec1Point;
94    if bytes.len() != 33 {
95        return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
96    }
97    let enc = elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(bytes)
98        .map_err(|_| scheme_error(Cmp20ErrorCode::BAD_SHARE))?;
99    let pt: AffinePoint = Option::from(AffinePoint::from_sec1_point(&enc))
100        .ok_or_else(|| scheme_error(Cmp20ErrorCode::BAD_SHARE))?;
101    let _ = pt.x();
102    Ok(pt)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    use elliptic_curve::Generate;
110
111    fn random_share(idx: u32) -> Cmp20Share {
112        let x_i = NonZeroScalar::generate();
113        let g = p256::ProjectivePoint::GENERATOR;
114        let pk = (g * *x_i).to_affine();
115        Cmp20Share::from_parts(x_i, pk, idx)
116    }
117
118    #[test]
119    fn share_round_trip() {
120        let s = random_share(1);
121        let bytes = s.to_bytes();
122        assert_eq!(bytes.len(), SHARE_BYTES);
123        let s2 = Cmp20Share::from_bytes(&bytes).expect("decode");
124        assert_eq!(s2.party_idx, 1);
125        assert_eq!(s2.x_i.to_bytes(), s.x_i.to_bytes());
126    }
127
128    #[test]
129    fn share_rejects_bad_magic() {
130        let mut bytes = random_share(0).to_bytes();
131        bytes[0] = b'X';
132        assert!(Cmp20Share::from_bytes(&bytes).is_err());
133    }
134
135    #[test]
136    fn share_rejects_truncated() {
137        let bytes = random_share(0).to_bytes();
138        assert!(Cmp20Share::from_bytes(&bytes[..10]).is_err());
139    }
140
141    #[test]
142    fn share_rejects_zero_scalar() {
143        let mut bytes = random_share(0).to_bytes();
144        for b in &mut bytes[5..37] {
145            *b = 0;
146        }
147        assert!(Cmp20Share::from_bytes(&bytes).is_err());
148    }
149
150    #[test]
151    fn debug_redacts_secret() {
152        let s = random_share(0);
153        let dbg = format!("{:?}", s);
154        assert!(dbg.contains("<redacted>"));
155    }
156}