Skip to main content

confium_tc_core/
share_envelope.rs

1//! Versioned share envelope — canonical wire format for threshold shares.
2//!
3//! Provides:
4//! - Version field for forward/backward compatibility
5//! - Scheme identifier (CMP20, FROST-P256, FROST-ed25519, GG18)
6//! - Quorum association
7//! - Tamper detection via HMAC-SHA256 integrity tag
8//!
9//! The envelope wraps scheme-specific share bytes without interpreting
10//! them. Each scheme (CMP20, FROST, etc.) serializes its own share type
11//! to bytes, then the envelope adds the versioning and integrity layer.
12
13use hmac::{Hmac, KeyInit, Mac};
14use serde::{Deserialize, Serialize};
15use sha2::Sha256;
16use subtle::ConstantTimeEq;
17
18type HmacSha256 = Hmac<Sha256>;
19
20/// Current envelope format version.
21pub const ENVELOPE_VERSION: u8 = 1;
22
23/// A versioned, integrity-protected threshold share envelope.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ShareEnvelope {
26    /// Envelope format version (currently 1).
27    pub version: u8,
28    /// Threshold scheme (e.g., "CMP20", "FROST-P256", "GG18").
29    pub scheme: String,
30    /// Quorum identifier this share belongs to.
31    pub quorum_id: String,
32    /// Party index (1-based, per DKG convention).
33    pub party_idx: u32,
34    /// Threshold T.
35    pub threshold: u32,
36    /// Total party count N.
37    pub party_count: u32,
38    /// Scheme-specific share bytes (opaque to the envelope).
39    pub share_data: Vec<u8>,
40    /// HMAC-SHA256 of all the above fields (keyed with the master key).
41    #[serde(default)]
42    pub integrity_tag: [u8; 32],
43}
44
45/// Errors during envelope operations.
46#[derive(Debug, thiserror::Error)]
47pub enum EnvelopeError {
48    /// Version mismatch.
49    #[error("unsupported envelope version: {0}")]
50    UnsupportedVersion(u8),
51    /// Integrity check failed.
52    #[error("integrity check failed")]
53    IntegrityFailed,
54    /// Serialization error.
55    #[error("serialization error: {0}")]
56    Serialization(String),
57}
58
59/// Create a new share envelope wrapping scheme-specific share bytes.
60/// The integrity tag is computed as HMAC-SHA256 over the envelope
61/// fields using `integrity_key` as the HMAC key.
62impl ShareEnvelope {
63    /// Wrap scheme-specific share bytes into a versioned envelope.
64    pub fn wrap(
65        scheme: &str,
66        quorum_id: &str,
67        party_idx: u32,
68        threshold: u32,
69        party_count: u32,
70        share_data: Vec<u8>,
71        integrity_key: &[u8],
72    ) -> Result<Self, EnvelopeError> {
73        let mut envelope = Self {
74            version: ENVELOPE_VERSION,
75            scheme: scheme.to_string(),
76            quorum_id: quorum_id.to_string(),
77            party_idx,
78            threshold,
79            party_count,
80            share_data,
81            integrity_tag: [0u8; 32],
82        };
83        envelope.integrity_tag = envelope.compute_tag(integrity_key)?;
84        Ok(envelope)
85    }
86
87    /// Verify the integrity tag matches the envelope contents.
88    /// Uses constant-time comparison to prevent timing side-channels.
89    pub fn verify(&self, integrity_key: &[u8]) -> Result<(), EnvelopeError> {
90        if self.version != ENVELOPE_VERSION {
91            return Err(EnvelopeError::UnsupportedVersion(self.version));
92        }
93        let expected = self.compute_tag(integrity_key)?;
94        if self.integrity_tag.ct_eq(&expected).into() {
95            Ok(())
96        } else {
97            Err(EnvelopeError::IntegrityFailed)
98        }
99    }
100
101    /// Extract the share data (after verifying integrity).
102    pub fn unwrap(&self, integrity_key: &[u8]) -> Result<Vec<u8>, EnvelopeError> {
103        self.verify(integrity_key)?;
104        Ok(self.share_data.clone())
105    }
106
107    /// Serialize to JSON bytes.
108    pub fn to_bytes(&self) -> Result<Vec<u8>, EnvelopeError> {
109        serde_json::to_vec(self).map_err(|e| EnvelopeError::Serialization(e.to_string()))
110    }
111
112    /// Deserialize from JSON bytes.
113    pub fn from_bytes(data: &[u8]) -> Result<Self, EnvelopeError> {
114        serde_json::from_slice(data).map_err(|e| EnvelopeError::Serialization(e.to_string()))
115    }
116
117    fn compute_tag(&self, key: &[u8]) -> Result<[u8; 32], EnvelopeError> {
118        let mut mac = HmacSha256::new_from_slice(key)
119            .map_err(|e| EnvelopeError::Serialization(e.to_string()))?;
120        mac.update(&[self.version]);
121        mac.update(self.scheme.as_bytes());
122        mac.update(self.quorum_id.as_bytes());
123        mac.update(&self.party_idx.to_be_bytes());
124        mac.update(&self.threshold.to_be_bytes());
125        mac.update(&self.party_count.to_be_bytes());
126        mac.update(&self.share_data);
127        let result = mac.finalize().into_bytes();
128        let mut tag = [0u8; 32];
129        tag.copy_from_slice(&result);
130        Ok(tag)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn round_trip_wrap_unwrap() {
140        let key = b"test-integrity-key-12345678901234";
141        let share = vec![0xAA; 32];
142        let envelope =
143            ShareEnvelope::wrap("CMP20", "quorum-alpha", 3, 2, 5, share.clone(), key).unwrap();
144
145        let recovered = envelope.unwrap(key).unwrap();
146        assert_eq!(recovered, share);
147    }
148
149    #[test]
150    fn tampered_share_data_detected() {
151        let key = b"test-integrity-key-12345678901234";
152        let mut envelope =
153            ShareEnvelope::wrap("FROST-P256", "quorum-beta", 1, 3, 5, vec![0x11; 32], key).unwrap();
154
155        envelope.share_data[0] ^= 0xFF;
156        assert!(matches!(
157            envelope.verify(key),
158            Err(EnvelopeError::IntegrityFailed)
159        ));
160    }
161
162    #[test]
163    fn tampered_threshold_detected() {
164        let key = b"test-integrity-key-12345678901234";
165        let mut envelope =
166            ShareEnvelope::wrap("CMP20", "quorum-gamma", 2, 3, 5, vec![0x22; 32], key).unwrap();
167
168        envelope.threshold = 2;
169        assert!(matches!(
170            envelope.verify(key),
171            Err(EnvelopeError::IntegrityFailed)
172        ));
173    }
174
175    #[test]
176    fn wrong_key_fails() {
177        let key = b"correct-integrity-key-12345678901";
178        let wrong_key = b"wrong-integrity-key-123456789012";
179        let envelope =
180            ShareEnvelope::wrap("GG18", "quorum-delta", 1, 2, 3, vec![0x33; 32], key).unwrap();
181
182        assert!(envelope.verify(wrong_key).is_err());
183        assert!(envelope.verify(key).is_ok());
184    }
185
186    #[test]
187    fn json_serialization_round_trip() {
188        let key = b"json-test-key-123456789012345678";
189        let envelope =
190            ShareEnvelope::wrap("CMP20", "quorum-json", 5, 3, 7, vec![0x44; 32], key).unwrap();
191
192        let json = envelope.to_bytes().unwrap();
193        let recovered = ShareEnvelope::from_bytes(&json).unwrap();
194        assert_eq!(recovered.version, envelope.version);
195        assert_eq!(recovered.scheme, envelope.scheme);
196        assert_eq!(recovered.quorum_id, envelope.quorum_id);
197        assert_eq!(recovered.party_idx, envelope.party_idx);
198        assert_eq!(recovered.threshold, envelope.threshold);
199        assert_eq!(recovered.party_count, envelope.party_count);
200        assert_eq!(recovered.share_data, envelope.share_data);
201        assert_eq!(recovered.integrity_tag, envelope.integrity_tag);
202
203        assert!(recovered.verify(key).is_ok());
204    }
205
206    #[test]
207    fn version_mismatch_rejected() {
208        let key = b"version-test-key-12345678901234";
209        let mut envelope =
210            ShareEnvelope::wrap("CMP20", "quorum-version", 1, 2, 3, vec![0x55; 32], key).unwrap();
211        envelope.version = 99;
212        assert!(matches!(
213            envelope.verify(key),
214            Err(EnvelopeError::UnsupportedVersion(99))
215        ));
216    }
217}