Skip to main content

confium_tc_core/
share.rs

1//! Opaque handle to a party's portion of a distributed secret.
2//!
3//! In a threshold scheme the secret key is never reconstructed in one
4//! place; instead each party holds a [`Share`]. After a DKG run each
5//! party obtains a fresh [`Share`] plus the shared public key. For
6//! signing sessions a pre-existing [`Share`] is loaded as input.
7//!
8//! The framework treats share bytes as opaque — the encoding is defined
9//! by the scheme plugin. [`Share`] carries the scheme name so a share
10//! can only be fed back into a session of the same scheme.
11
12use snafu::ensure;
13
14use crate::Result;
15use crate::error;
16
17/// A party's share of a distributed secret.
18///
19/// `scheme` ties the share to the scheme that produced it (e.g.
20/// `"FROST-ed25519"`). `bytes` is the scheme-specific encoding of the
21/// share; the framework never inspects or reinterprets it.
22///
23/// The secret `bytes` field is zeroized on drop.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Share {
26    scheme: String,
27    bytes: Vec<u8>,
28}
29
30impl Drop for Share {
31    fn drop(&mut self) {
32        use zeroize::Zeroize;
33        self.bytes.zeroize();
34    }
35}
36
37impl Share {
38    pub fn new(scheme: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
39        Share {
40            scheme: scheme.into(),
41            bytes: bytes.into(),
42        }
43    }
44
45    pub fn scheme(&self) -> &str {
46        &self.scheme
47    }
48
49    pub fn bytes(&self) -> &[u8] {
50        &self.bytes
51    }
52
53    pub fn into_bytes(self) -> Vec<u8> {
54        self.bytes.clone()
55    }
56
57    pub fn len(&self) -> usize {
58        self.bytes.len()
59    }
60
61    pub fn is_empty(&self) -> bool {
62        self.bytes.is_empty()
63    }
64
65    /// Reject a share whose scheme doesn't match the session's scheme.
66    /// Used by [`crate::session::Session::create`] when a pre-existing
67    /// share is supplied as input.
68    pub fn assert_scheme(&self, expected: &str) -> Result<()> {
69        ensure!(
70            self.scheme == expected,
71            error::ShareSchemeMismatchSnafu {
72                expected,
73                actual: self.scheme.as_str(),
74            }
75        );
76        Ok(())
77    }
78
79    /// Serialize into the `(scheme_len: u32 BE | scheme_utf8 | bytes)`
80    /// framing used by [`Share::from_bytes`]. The scheme name is the
81    /// canonical identifier; `bytes` is the scheme-specific payload.
82    /// This is the only (de)serialization the framework needs — share
83    /// persistence goes through [`crate::store`] using this encoding.
84    pub fn to_bytes(&self) -> Vec<u8> {
85        let scheme_bytes = self.scheme.as_bytes();
86        let mut out = Vec::with_capacity(4 + scheme_bytes.len() + self.bytes.len());
87        out.extend_from_slice(&(scheme_bytes.len() as u32).to_be_bytes());
88        out.extend_from_slice(scheme_bytes);
89        out.extend_from_slice(&self.bytes);
90        out
91    }
92
93    /// Parse the framing produced by [`Share::to_bytes`].
94    pub fn from_bytes(data: &[u8]) -> Result<Self> {
95        if data.len() < 4 {
96            return Err(error::ShareTruncatedSnafu {
97                needed: 4usize,
98                have: data.len(),
99            }
100            .build());
101        }
102        let scheme_len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
103        let scheme_end = 4usize.checked_add(scheme_len).ok_or_else(|| {
104            error::ShareTruncatedSnafu {
105                needed: 4 + scheme_len,
106                have: data.len(),
107            }
108            .build()
109        })?;
110        if data.len() < scheme_end {
111            return Err(error::ShareTruncatedSnafu {
112                needed: scheme_end,
113                have: data.len(),
114            }
115            .build());
116        }
117        let scheme = std::str::from_utf8(&data[4..scheme_end])
118            .map_err(|_| error::ShareInvalidSchemeSnafu {}.build())?
119            .to_string();
120        let bytes = data[scheme_end..].to_vec();
121        Ok(Share { scheme, bytes })
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn share_round_trip() {
131        let original = Share::new("FROST-ed25519", vec![0xDE, 0xAD, 0xBE, 0xEF]);
132        let encoded = original.to_bytes();
133        let decoded = Share::from_bytes(&encoded).expect("decode succeeds");
134        assert_eq!(original, decoded);
135    }
136
137    #[test]
138    fn share_round_trip_preserves_scheme_name() {
139        let original = Share::new("GG18-ECDSA-P256", vec![1, 2, 3, 4, 5]);
140        let encoded = original.to_bytes();
141        let decoded = Share::from_bytes(&encoded).expect("decode succeeds");
142        assert_eq!(decoded.scheme(), "GG18-ECDSA-P256");
143        assert_eq!(decoded.bytes(), &[1, 2, 3, 4, 5]);
144    }
145
146    #[test]
147    fn share_round_trip_empty_bytes() {
148        let original = Share::new("test-scheme", Vec::new());
149        let encoded = original.to_bytes();
150        let decoded = Share::from_bytes(&encoded).expect("decode succeeds");
151        assert_eq!(original, decoded);
152        assert!(decoded.is_empty());
153    }
154
155    #[test]
156    fn share_from_bytes_rejects_truncated_header() {
157        let err = Share::from_bytes(&[1, 2]).unwrap_err();
158        assert!(matches!(err, error::Error::ShareTruncated { .. }));
159    }
160
161    #[test]
162    fn share_from_bytes_rejects_truncated_scheme() {
163        // scheme_len = 10 but only 2 bytes follow
164        let data = [0, 0, 0, 10, b'a', b'b'];
165        let err = Share::from_bytes(&data).unwrap_err();
166        assert!(matches!(err, error::Error::ShareTruncated { .. }));
167    }
168
169    #[test]
170    fn share_assert_scheme_accepts_match() {
171        let share = Share::new("FROST-ed25519", vec![1]);
172        share.assert_scheme("FROST-ed25519").expect("match is ok");
173    }
174
175    #[test]
176    fn share_assert_scheme_rejects_mismatch() {
177        let share = Share::new("FROST-ed25519", vec![1]);
178        let err = share.assert_scheme("GG18-ECDSA-P256").unwrap_err();
179        assert!(matches!(err, error::Error::ShareSchemeMismatch { .. }));
180    }
181
182    #[test]
183    fn share_into_bytes_consumes_payload() {
184        let share = Share::new("s", vec![9, 9, 9]);
185        let bytes = share.into_bytes();
186        assert_eq!(bytes, vec![9, 9, 9]);
187    }
188}