1use hmac::{Hmac, KeyInit, Mac};
36use sha2::{Digest, Sha256};
37
38use crate::Result;
39use crate::message::Message;
40use crate::registry::{RoundResult, SessionImpl, TcScheme, TcSchemeKind};
41use crate::session::SessionParams;
42
43pub const SCHEME_NAME: &str = "mock-tc-sig";
45
46type HmacSha256 = Hmac<Sha256>;
48
49pub struct MockTcSigScheme;
53
54impl TcScheme for MockTcSigScheme {
55 fn name(&self) -> &'static str {
56 SCHEME_NAME
57 }
58
59 fn kind(&self) -> TcSchemeKind {
60 TcSchemeKind::Signature
61 }
62
63 fn create_session(&self, params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
64 let party_id = params.parties.get(params.this_party_idx)?.id.clone();
65 let threshold = params.threshold;
66 let message = params.message.clone().unwrap_or_default();
67 let roster_ids = params
68 .parties
69 .parties()
70 .iter()
71 .map(|p| p.id.clone())
72 .collect::<Vec<_>>();
73 let shared_key = params
78 .local_share
79 .as_ref()
80 .map(|s| s.bytes().to_vec())
81 .unwrap_or_else(|| DEFAULT_SHARED_KEY.to_vec());
82
83 Ok(Box::new(MockTcSigSession {
84 party_id,
85 threshold,
86 roster_ids,
87 shared_key,
88 message,
89 round_done: 0,
90 collected_tags: 0,
91 signature: Vec::new(),
92 }))
93 }
94}
95
96crate::register_tc_scheme!(MockTcSigScheme);
99
100const DEFAULT_SHARED_KEY: &[u8] = b"confium-mock-tc-sig-shared-key";
104
105struct MockTcSigSession {
107 party_id: String,
109 threshold: u32,
111 roster_ids: Vec<String>,
114 shared_key: Vec<u8>,
116 message: Vec<u8>,
118 round_done: u8,
120 collected_tags: usize,
123 signature: Vec<u8>,
125}
126
127impl MockTcSigSession {
128 fn derive_nonce(party_id: &str, shared_key: &[u8], message: &[u8]) -> Vec<u8> {
132 let mut hasher = Sha256::new();
133 hasher.update(b"mock-tc-sig-nonce");
134 hasher.update(shared_key);
135 hasher.update(party_id.as_bytes());
136 hasher.update(message);
137 hasher.finalize().to_vec()
138 }
139
140 fn canonical_blob(roster_ids: &[String], shared_key: &[u8], message: &[u8]) -> Vec<u8> {
144 let mut entries: Vec<(String, Vec<u8>)> = roster_ids
145 .iter()
146 .map(|id| (id.clone(), Self::derive_nonce(id, shared_key, message)))
147 .collect();
148 entries.sort_by(|a, b| a.0.cmp(&b.0));
149 let mut blob = Vec::new();
150 for (id, nonce) in &entries {
151 blob.extend_from_slice(id.as_bytes());
152 blob.extend_from_slice(nonce);
153 }
154 blob.extend_from_slice(message);
155 blob
156 }
157
158 fn hmac(shared_key: &[u8], data: &[u8]) -> Vec<u8> {
160 let mut mac = HmacSha256::new_from_slice(shared_key).expect("HMAC accepts any key length");
161 mac.update(data);
162 mac.finalize().into_bytes().to_vec()
163 }
164
165 fn round0(&mut self) -> Result<RoundResult> {
168 let nonce = Self::derive_nonce(&self.party_id, &self.shared_key, &self.message);
169 let payload = frame(&self.party_id, &nonce);
170 let msg = Message::broadcast(&self.party_id, 1, payload);
171 Ok(RoundResult::new(vec![msg], false))
172 }
173
174 fn round1(&mut self, _incoming: &[Message]) -> Result<RoundResult> {
179 let blob = Self::canonical_blob(&self.roster_ids, &self.shared_key, &self.message);
180 let tag = Self::hmac(&self.shared_key, &blob);
181 let payload = frame(&self.party_id, &tag);
182 let msg = Message::broadcast(&self.party_id, 2, payload);
183 Ok(RoundResult::new(vec![msg], false))
184 }
185
186 fn round2(&mut self, incoming: &[Message]) -> Result<RoundResult> {
189 let mut seen: std::collections::HashSet<String> = parse_frames(incoming)
192 .into_iter()
193 .map(|(id, _)| id)
194 .collect();
195 seen.insert(self.party_id.clone());
196 self.collected_tags = seen.len();
197
198 if (self.collected_tags as u32) < self.threshold {
199 return Err(crate::error::SchemeInternalSnafu { code: 0x1042u32 }.build());
200 }
201
202 let blob = Self::canonical_blob(&self.roster_ids, &self.shared_key, &self.message);
205 let tag = Self::hmac(&self.shared_key, &blob);
206 let mut sig = Vec::new();
207 sig.extend_from_slice(&tag);
208 self.signature = sig;
209 Ok(RoundResult::done())
210 }
211}
212
213impl SessionImpl for MockTcSigSession {
214 fn round(&mut self, incoming: &[Message]) -> Result<RoundResult> {
215 self.round_done = self.round_done.checked_add(1).ok_or_else(|| {
216 crate::error::RoundOverflowSnafu {
217 round: self.round_done,
218 }
219 .build()
220 })?;
221 match self.round_done {
222 1 => self.round0(),
223 2 => self.round1(incoming),
224 3 => self.round2(incoming),
225 other => Err(crate::error::RoundOverflowSnafu { round: other }.build()),
226 }
227 }
228
229 fn result(&self) -> Result<Vec<u8>> {
230 if self.round_done < 3 {
231 return Err(crate::error::SessionNotCompleteSnafu {}.build());
232 }
233 Ok(self.signature.clone())
234 }
235
236 fn destroy(&mut self) {
237 self.shared_key.fill(0);
238 self.message.fill(0);
239 self.signature.fill(0);
240 }
241}
242
243fn frame(party_id: &str, body: &[u8]) -> Vec<u8> {
250 let id = party_id.as_bytes();
251 debug_assert!(id.len() <= u8::MAX as usize, "party id fits in a byte tag");
252 let mut out = Vec::with_capacity(1 + id.len() + body.len());
253 out.push(id.len() as u8);
254 out.extend_from_slice(id);
255 out.extend_from_slice(body);
256 out
257}
258
259fn parse_frames(msgs: &[Message]) -> Vec<(String, Vec<u8>)> {
263 let mut out = Vec::with_capacity(msgs.len());
264 for m in msgs {
265 let p = &m.payload;
266 if p.is_empty() {
267 continue;
268 }
269 let len = p[0] as usize;
270 if p.len() < 1 + len {
271 continue;
272 }
273 let id = match std::str::from_utf8(&p[1..1 + len]) {
274 Ok(s) => s.to_string(),
275 Err(_) => continue,
276 };
277 out.push((id, p[1 + len..].to_vec()));
278 }
279 out
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::party::{Party, PartyList};
286 use crate::share::Share;
287
288 fn params(idx: usize, threshold: u32, message: &[u8]) -> SessionParams {
289 SessionParams {
290 scheme: SCHEME_NAME.to_string(),
291 parties: PartyList::from_parties(vec![
292 Party::inproc("alice"),
293 Party::inproc("bob"),
294 Party::inproc("carol"),
295 ]),
296 threshold,
297 this_party_idx: idx,
298 local_share: Some(Share::new(SCHEME_NAME, DEFAULT_SHARED_KEY.to_vec())),
299 message: Some(message.to_vec()),
300 }
301 }
302
303 #[test]
304 fn scheme_is_registered() {
305 let s = crate::registry::find(SCHEME_NAME);
306 assert!(s.is_some(), "mock-tc-sig must be registered at link time");
307 assert_eq!(s.unwrap().kind(), TcSchemeKind::Signature);
308 }
309
310 #[test]
311 fn nonce_is_deterministic_per_party() {
312 let a = MockTcSigSession::derive_nonce("alice", DEFAULT_SHARED_KEY, b"msg");
313 let b = MockTcSigSession::derive_nonce("alice", DEFAULT_SHARED_KEY, b"msg");
314 assert_eq!(a, b, "nonce must be deterministic for the same inputs");
315 }
316
317 #[test]
318 fn nonce_differs_per_party() {
319 let a = MockTcSigSession::derive_nonce("alice", DEFAULT_SHARED_KEY, b"msg");
320 let b = MockTcSigSession::derive_nonce("bob", DEFAULT_SHARED_KEY, b"msg");
321 assert_ne!(a, b, "different parties produce different nonces");
322 }
323
324 #[test]
325 fn canonical_blob_is_independent_of_roster_order() {
326 let mut a = vec!["alice".to_string(), "bob".to_string(), "carol".to_string()];
327 let mut b = vec!["carol".to_string(), "alice".to_string(), "bob".to_string()];
328 let blob_a = MockTcSigSession::canonical_blob(&a, DEFAULT_SHARED_KEY, b"msg");
329 let blob_b = MockTcSigSession::canonical_blob(&b, DEFAULT_SHARED_KEY, b"msg");
330 assert_eq!(blob_a, blob_b, "blob must sort the roster canonically");
331 a.sort();
332 b.sort();
333 assert_eq!(a, b);
334 }
335
336 #[test]
337 fn frame_round_trip() {
338 let f = frame("alice", &[1, 2, 3]);
339 let parsed = parse_frames(&[Message::broadcast("alice", 1, f.clone())]);
340 assert_eq!(parsed.len(), 1);
341 assert_eq!(parsed[0].0, "alice");
342 assert_eq!(parsed[0].1, vec![1, 2, 3]);
343 }
344
345 #[test]
346 fn parse_skips_truncated() {
347 let bad = vec![5, b'a'];
348 let parsed = parse_frames(&[Message::broadcast("x", 1, bad)]);
349 assert!(parsed.is_empty(), "truncated frame must be skipped");
350 }
351
352 #[test]
353 fn create_session_uses_default_key_when_no_share() {
354 let mut p = params(0, 2, b"msg");
355 p.local_share = None;
356 let s = MockTcSigScheme.create_session(&p);
357 assert!(s.is_ok(), "session creates without a share");
358 }
359}