1use hmac::{Hmac, KeyInit, Mac};
14use serde::{Deserialize, Serialize};
15use sha2::Sha256;
16use subtle::ConstantTimeEq;
17
18type HmacSha256 = Hmac<Sha256>;
19
20pub const ENVELOPE_VERSION: u8 = 1;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ShareEnvelope {
26 pub version: u8,
28 pub scheme: String,
30 pub quorum_id: String,
32 pub party_idx: u32,
34 pub threshold: u32,
36 pub party_count: u32,
38 pub share_data: Vec<u8>,
40 #[serde(default)]
42 pub integrity_tag: [u8; 32],
43}
44
45#[derive(Debug, thiserror::Error)]
47pub enum EnvelopeError {
48 #[error("unsupported envelope version: {0}")]
50 UnsupportedVersion(u8),
51 #[error("integrity check failed")]
53 IntegrityFailed,
54 #[error("serialization error: {0}")]
56 Serialization(String),
57}
58
59impl ShareEnvelope {
63 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 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 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 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 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}