1use std::{
4 collections::BTreeMap,
5 fmt::{self, Display},
6 io::Write,
7};
8
9use frost_ristretto255 as frost;
10use serde::{
11 Deserialize, Serialize,
12 de::{DeserializeOwned, MapAccess, SeqAccess, Visitor},
13};
14use zeroize::{Zeroize, Zeroizing};
15
16use crate::{GenesisCeremonyConfig, Result, RootError};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct RootPublicKeyPackage {
21 pub public_key_package: Vec<u8>,
23 pub root_public_key: Vec<u8>,
25 pub verifying_shares: BTreeMap<u16, Vec<u8>>,
27}
28
29#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(from = "ZeroizingRootKeyPackage")]
37pub struct RootKeyPackage {
38 pub frost_identifier: u16,
40 pub key_package: Vec<u8>,
42}
43
44impl fmt::Debug for RootKeyPackage {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 formatter
47 .debug_struct("RootKeyPackage")
48 .field("frost_identifier", &self.frost_identifier)
49 .field("key_package", &"[REDACTED]")
50 .finish()
51 }
52}
53
54impl RootKeyPackage {
55 fn from_zeroizing(frost_identifier: u16, mut key_package: Zeroizing<Vec<u8>>) -> Self {
56 let mut output = Self {
57 frost_identifier,
58 key_package: Vec::new(),
59 };
60 output.key_package = std::mem::take(&mut *key_package);
61 output
62 }
63}
64
65impl Zeroize for RootKeyPackage {
66 fn zeroize(&mut self) {
67 self.key_package.zeroize();
68 }
69}
70
71#[derive(Deserialize)]
72struct ZeroizingRootKeyPackage {
73 frost_identifier: u16,
74 #[serde(deserialize_with = "deserialize_zeroizing_bytes")]
75 key_package: Zeroizing<Vec<u8>>,
76}
77
78impl From<ZeroizingRootKeyPackage> for RootKeyPackage {
79 fn from(value: ZeroizingRootKeyPackage) -> Self {
80 Self::from_zeroizing(value.frost_identifier, value.key_package)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(from = "ZeroizingRootDkgOutput")]
91pub struct RootDkgOutput {
92 pub key_packages: BTreeMap<u16, RootKeyPackage>,
94 pub public_key_package: RootPublicKeyPackage,
96}
97
98#[derive(Deserialize)]
100struct ZeroizingRootDkgOutput {
101 key_packages: BTreeMap<u16, ZeroizingRootKeyPackage>,
102 public_key_package: RootPublicKeyPackage,
103}
104
105impl From<ZeroizingRootDkgOutput> for RootDkgOutput {
106 fn from(value: ZeroizingRootDkgOutput) -> Self {
107 Self {
108 key_packages: value
109 .key_packages
110 .into_iter()
111 .map(|(identifier, package)| (identifier, package.into()))
112 .collect(),
113 public_key_package: value.public_key_package,
114 }
115 }
116}
117
118#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(from = "ZeroizingRootDkgRound1Output")]
125pub struct RootDkgRound1Output {
126 pub frost_identifier: u16,
128 pub round1_secret_package: Vec<u8>,
130 pub round1_package: Vec<u8>,
132}
133
134impl RootDkgRound1Output {
135 fn from_zeroizing(
136 frost_identifier: u16,
137 mut round1_secret_package: Zeroizing<Vec<u8>>,
138 round1_package: Vec<u8>,
139 ) -> Self {
140 let mut output = Self {
141 frost_identifier,
142 round1_secret_package: Vec::new(),
143 round1_package,
144 };
145 output.round1_secret_package = std::mem::take(&mut *round1_secret_package);
146 output
147 }
148}
149
150impl Zeroize for RootDkgRound1Output {
151 fn zeroize(&mut self) {
152 self.round1_secret_package.zeroize();
153 }
154}
155
156#[derive(Deserialize)]
157struct ZeroizingRootDkgRound1Output {
158 frost_identifier: u16,
159 #[serde(deserialize_with = "deserialize_zeroizing_bytes")]
160 round1_secret_package: Zeroizing<Vec<u8>>,
161 round1_package: Vec<u8>,
162}
163
164impl From<ZeroizingRootDkgRound1Output> for RootDkgRound1Output {
165 fn from(value: ZeroizingRootDkgRound1Output) -> Self {
166 Self::from_zeroizing(
167 value.frost_identifier,
168 value.round1_secret_package,
169 value.round1_package,
170 )
171 }
172}
173
174impl fmt::Debug for RootDkgRound1Output {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 formatter
177 .debug_struct("RootDkgRound1Output")
178 .field("frost_identifier", &self.frost_identifier)
179 .field("round1_secret_package", &"[REDACTED]")
180 .field("round1_package", &self.round1_package)
181 .finish()
182 }
183}
184
185#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(from = "ZeroizingRootDkgRound2Output")]
193pub struct RootDkgRound2Output {
194 pub frost_identifier: u16,
196 pub round2_secret_package: Vec<u8>,
198 pub round2_packages: BTreeMap<u16, Vec<u8>>,
200}
201
202type RecipientRound2Packages = BTreeMap<u16, Zeroizing<Vec<u8>>>;
203type ZeroizingRound2Parts = (Zeroizing<Vec<u8>>, RecipientRound2Packages);
204
205impl RootDkgRound2Output {
206 fn from_zeroizing(
207 frost_identifier: u16,
208 mut round2_secret_package: Zeroizing<Vec<u8>>,
209 mut round2_packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
210 ) -> Self {
211 let mut output = Self {
212 frost_identifier,
213 round2_secret_package: Vec::new(),
214 round2_packages: BTreeMap::new(),
215 };
216 output.round2_secret_package = std::mem::take(&mut *round2_secret_package);
217 while let Some((identifier, mut package)) = round2_packages.pop_first() {
218 output
219 .round2_packages
220 .insert(identifier, std::mem::take(&mut *package));
221 }
222 output
223 }
224
225 fn take_zeroizing_parts(&mut self) -> ZeroizingRound2Parts {
226 let secret_package = Zeroizing::new(std::mem::take(&mut self.round2_secret_package));
227 let packages = zeroizing_byte_map(std::mem::take(&mut self.round2_packages));
228 (secret_package, packages)
229 }
230}
231
232impl Zeroize for RootDkgRound2Output {
233 fn zeroize(&mut self) {
234 self.round2_secret_package.zeroize();
235 for package in self.round2_packages.values_mut() {
236 package.zeroize();
237 }
238 self.round2_packages.clear();
239 }
240}
241
242#[derive(Deserialize)]
243struct ZeroizingRootDkgRound2Output {
244 frost_identifier: u16,
245 #[serde(deserialize_with = "deserialize_zeroizing_bytes")]
246 round2_secret_package: Zeroizing<Vec<u8>>,
247 #[serde(deserialize_with = "deserialize_zeroizing_byte_map")]
248 round2_packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
249}
250
251impl From<ZeroizingRootDkgRound2Output> for RootDkgRound2Output {
252 fn from(value: ZeroizingRootDkgRound2Output) -> Self {
253 Self::from_zeroizing(
254 value.frost_identifier,
255 value.round2_secret_package,
256 value.round2_packages,
257 )
258 }
259}
260
261impl fmt::Debug for RootDkgRound2Output {
262 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263 formatter
264 .debug_struct("RootDkgRound2Output")
265 .field("frost_identifier", &self.frost_identifier)
266 .field("round2_secret_package", &"[REDACTED]")
267 .field("round2_packages", &"[REDACTED]")
268 .finish()
269 }
270}
271
272const MAX_INITIAL_SECRET_CAPACITY: usize = 4096;
273const MAX_SECRET_CAPACITY: usize = usize::MAX / 2;
274
275struct ZeroizingByteAccumulator {
276 bytes: Zeroizing<Vec<u8>>,
277 logical_len: usize,
278}
279
280#[derive(Clone, Copy, Debug)]
281struct SecretBufferError(&'static str);
282
283fn next_secret_capacity(current_capacity: usize) -> std::result::Result<usize, SecretBufferError> {
284 let next_capacity = current_capacity
285 .checked_mul(2)
286 .unwrap_or(MAX_SECRET_CAPACITY)
287 .clamp(1, MAX_SECRET_CAPACITY);
288 if next_capacity <= current_capacity {
289 return Err(SecretBufferError(
290 "root secret bytes exceed supported capacity",
291 ));
292 }
293 Ok(next_capacity)
294}
295
296impl ZeroizingByteAccumulator {
297 fn with_size_hint(size_hint: usize) -> std::result::Result<Self, SecretBufferError> {
298 let initial_capacity = size_hint.min(MAX_INITIAL_SECRET_CAPACITY);
299 let mut bytes = Zeroizing::new(Vec::new());
300 bytes
301 .try_reserve_exact(initial_capacity)
302 .map_err(|_| SecretBufferError("unable to allocate root secret bytes"))?;
303 bytes.resize(initial_capacity, 0);
304 Ok(Self {
305 bytes,
306 logical_len: 0,
307 })
308 }
309
310 fn push(&mut self, byte: u8) -> std::result::Result<(), SecretBufferError> {
311 if self.logical_len == self.bytes.len() {
312 self.grow()?;
313 }
314 self.bytes[self.logical_len] = byte;
315 self.logical_len += 1;
316 Ok(())
317 }
318
319 fn grow(&mut self) -> std::result::Result<(), SecretBufferError> {
320 let current_capacity = self.bytes.len();
321 let next_capacity = next_secret_capacity(current_capacity)?;
322 let mut replacement = Zeroizing::new(Vec::new());
323 replacement
324 .try_reserve_exact(next_capacity)
325 .map_err(|_| SecretBufferError("unable to allocate root secret bytes"))?;
326 replacement.resize(next_capacity, 0);
327 replacement[..self.logical_len].copy_from_slice(&self.bytes[..self.logical_len]);
328 self.bytes.zeroize();
329 self.bytes = replacement;
330 Ok(())
331 }
332
333 fn finish(mut self) -> Zeroizing<Vec<u8>> {
334 self.bytes.truncate(self.logical_len);
335 std::mem::take(&mut self.bytes)
336 }
337}
338
339impl Drop for ZeroizingByteAccumulator {
340 fn drop(&mut self) {
341 self.bytes.zeroize();
342 self.logical_len.zeroize();
343 }
344}
345
346impl zeroize::ZeroizeOnDrop for ZeroizingByteAccumulator {}
347
348struct ZeroizingByteWriter {
349 accumulator: ZeroizingByteAccumulator,
350}
351
352impl ZeroizingByteWriter {
353 fn new() -> Self {
354 Self {
355 accumulator: ZeroizingByteAccumulator {
356 bytes: Zeroizing::new(Vec::new()),
357 logical_len: 0,
358 },
359 }
360 }
361
362 fn finish(self) -> Zeroizing<Vec<u8>> {
363 self.accumulator.finish()
364 }
365}
366
367impl Write for ZeroizingByteWriter {
368 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
369 for byte in buffer {
370 self.accumulator
371 .push(*byte)
372 .map_err(|error| std::io::Error::new(std::io::ErrorKind::OutOfMemory, error.0))?;
373 }
374 Ok(buffer.len())
375 }
376
377 fn flush(&mut self) -> std::io::Result<()> {
378 Ok(())
379 }
380}
381
382impl zeroize::ZeroizeOnDrop for ZeroizingByteWriter {}
383
384struct ZeroizingBytesVisitor;
385
386impl<'de> Visitor<'de> for ZeroizingBytesVisitor {
387 type Value = Zeroizing<Vec<u8>>;
388
389 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
390 formatter.write_str("a sequence of root secret bytes")
391 }
392
393 fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
394 where
395 A: SeqAccess<'de>,
396 {
397 let mut accumulator =
398 ZeroizingByteAccumulator::with_size_hint(sequence.size_hint().unwrap_or(0))
399 .map_err(|error| <A::Error as serde::de::Error>::custom(error.0))?;
400 while let Some(byte) = sequence.next_element::<u8>()? {
401 accumulator
402 .push(byte)
403 .map_err(|error| <A::Error as serde::de::Error>::custom(error.0))?;
404 }
405 Ok(accumulator.finish())
406 }
407}
408
409pub(crate) fn deserialize_zeroizing_bytes<'de, D>(
410 deserializer: D,
411) -> std::result::Result<Zeroizing<Vec<u8>>, D::Error>
412where
413 D: serde::Deserializer<'de>,
414{
415 deserializer.deserialize_seq(ZeroizingBytesVisitor)
416}
417
418struct ZeroizingBytes(Zeroizing<Vec<u8>>);
419
420impl<'de> Deserialize<'de> for ZeroizingBytes {
421 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
422 where
423 D: serde::Deserializer<'de>,
424 {
425 deserialize_zeroizing_bytes(deserializer).map(Self)
426 }
427}
428
429struct ZeroizingByteMapVisitor;
430
431impl<'de> Visitor<'de> for ZeroizingByteMapVisitor {
432 type Value = BTreeMap<u16, Zeroizing<Vec<u8>>>;
433
434 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
435 formatter.write_str("a map of recipient-bound root secret byte sequences")
436 }
437
438 fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
439 where
440 A: MapAccess<'de>,
441 {
442 let mut packages = BTreeMap::new();
443 while let Some((identifier, package)) = map.next_entry::<u16, ZeroizingBytes>()? {
444 packages.insert(identifier, package.0);
445 }
446 Ok(packages)
447 }
448}
449
450fn deserialize_zeroizing_byte_map<'de, D>(
451 deserializer: D,
452) -> std::result::Result<BTreeMap<u16, Zeroizing<Vec<u8>>>, D::Error>
453where
454 D: serde::Deserializer<'de>,
455{
456 deserializer.deserialize_map(ZeroizingByteMapVisitor)
457}
458
459fn zeroizing_byte_map(mut packages: BTreeMap<u16, Vec<u8>>) -> BTreeMap<u16, Zeroizing<Vec<u8>>> {
460 let mut zeroizing_packages = BTreeMap::new();
461 while let Some((identifier, package)) = packages.pop_first() {
462 let package = Zeroizing::new(package);
463 zeroizing_packages.insert(identifier, package);
464 }
465 zeroizing_packages
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
474#[serde(from = "ZeroizingRootParticipantDkgOutput")]
475pub struct RootParticipantDkgOutput {
476 pub key_package: RootKeyPackage,
478 pub public_key_package: RootPublicKeyPackage,
480}
481
482impl Zeroize for RootParticipantDkgOutput {
483 fn zeroize(&mut self) {
484 self.key_package.zeroize();
485 }
486}
487
488#[derive(Deserialize)]
489struct ZeroizingRootParticipantDkgOutput {
490 key_package: ZeroizingRootKeyPackage,
491 public_key_package: RootPublicKeyPackage,
492}
493
494impl From<ZeroizingRootParticipantDkgOutput> for RootParticipantDkgOutput {
495 fn from(value: ZeroizingRootParticipantDkgOutput) -> Self {
496 Self {
497 key_package: value.key_package.into(),
498 public_key_package: value.public_key_package,
499 }
500 }
501}
502
503pub(crate) fn frost_identifier(identifier: u16) -> Result<frost::Identifier> {
504 frost::Identifier::try_from(identifier).map_err(frost_error)
505}
506
507fn rostered_frost_identifier(
508 config: &GenesisCeremonyConfig,
509 identifier: u16,
510 operation: &str,
511) -> Result<frost::Identifier> {
512 if config.certifier_by_identifier(identifier).is_none() {
513 return Err(RootError::InvalidConfig {
514 reason: format!("{operation} certifier {identifier} is not rostered"),
515 });
516 }
517 frost_identifier(identifier)
518}
519
520fn frost_error(error: frost::Error) -> RootError {
521 RootError::Frost {
522 detail: error.to_string(),
523 }
524}
525
526fn frost_encoding_error(error: impl Display) -> RootError {
527 RootError::Frost {
528 detail: format!("FROST artifact canonical encoding failed: {error}"),
529 }
530}
531
532pub(crate) fn serialize_frost<T: Serialize>(value: &T) -> Result<Vec<u8>> {
533 let mut bytes = Vec::new();
534 ciborium::into_writer(value, &mut bytes).map_err(frost_encoding_error)?;
535 Ok(bytes)
536}
537
538pub(crate) fn serialize_frost_secret<T: Serialize>(value: &T) -> Result<Zeroizing<Vec<u8>>> {
539 let mut writer = ZeroizingByteWriter::new();
540 ciborium::into_writer(value, &mut writer).map_err(frost_encoding_error)?;
541 Ok(writer.finish())
542}
543
544pub(crate) fn deserialize_frost<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
545 ciborium::from_reader(bytes).map_err(frost_encoding_error)
546}
547
548fn identifier_value(
549 config: &GenesisCeremonyConfig,
550 frost_identifier: frost::Identifier,
551) -> Result<u16> {
552 for certifier in &config.certifiers {
553 if crate::dkg::frost_identifier(certifier.frost_identifier)? == frost_identifier {
554 return Ok(certifier.frost_identifier);
555 }
556 }
557 Err(RootError::Frost {
558 detail: "FROST identifier is not rostered".to_owned(),
559 })
560}
561
562fn peer_packages_except(
563 packages: &BTreeMap<u16, Vec<u8>>,
564 excluded: u16,
565) -> BTreeMap<u16, Vec<u8>> {
566 packages
567 .iter()
568 .filter(|(peer, _)| **peer != excluded)
569 .map(|(peer, package)| (*peer, package.clone()))
570 .collect()
571}
572
573fn frost_dkg_round1<R>(
574 identifier: frost::Identifier,
575 max_signers: u16,
576 threshold: u16,
577 rng: &mut R,
578) -> Result<(
579 frost::keys::dkg::round1::SecretPackage,
580 frost::keys::dkg::round1::Package,
581)>
582where
583 R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
584{
585 frost::keys::dkg::part1(identifier, max_signers, threshold, rng).map_err(frost_error)
586}
587
588pub(crate) fn serialize_public_key_package(
589 config: &GenesisCeremonyConfig,
590 package: &frost::keys::PublicKeyPackage,
591) -> Result<RootPublicKeyPackage> {
592 let public_key_package = serialize_frost(package)?;
593 let root_public_key = serialize_frost(package.verifying_key())?;
594 let mut verifying_shares = BTreeMap::new();
595 for certifier in &config.certifiers {
596 let identifier = frost_identifier(certifier.frost_identifier)?;
597 let share =
598 package
599 .verifying_shares()
600 .get(&identifier)
601 .ok_or_else(|| RootError::Frost {
602 detail: format!(
603 "missing verification share for identifier {}",
604 certifier.frost_identifier
605 ),
606 })?;
607 verifying_shares.insert(certifier.frost_identifier, serialize_frost(share)?);
608 }
609 Ok(RootPublicKeyPackage {
610 public_key_package,
611 root_public_key,
612 verifying_shares,
613 })
614}
615
616pub(crate) fn validate_public_key_package(
617 config: &GenesisCeremonyConfig,
618 package: &RootPublicKeyPackage,
619) -> Result<()> {
620 let frost_package: frost::keys::PublicKeyPackage =
621 deserialize_frost(package.public_key_package.as_slice())?;
622 let expected = serialize_public_key_package(config, &frost_package)?;
623 if &expected != package {
624 return Err(RootError::BundleRejected {
625 reason: "public key package metadata does not match serialized FROST package"
626 .to_owned(),
627 });
628 }
629 Ok(())
630}
631
632pub fn dkg_round1<R>(
634 config: &GenesisCeremonyConfig,
635 frost_identifier_value: u16,
636 rng: &mut R,
637) -> Result<RootDkgRound1Output>
638where
639 R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
640{
641 config.validate()?;
642 let identifier = rostered_frost_identifier(config, frost_identifier_value, "round-one")?;
643 let max_signers = config.max_signers;
644 let threshold = config.threshold;
645 let round1 = frost_dkg_round1(identifier, max_signers, threshold, rng)?;
646 let (secret_package, package) = round1;
647 let secret_package = serialize_frost_secret(&secret_package)?;
648 let public_package = serialize_frost(&package)?;
649 Ok(RootDkgRound1Output::from_zeroizing(
650 frost_identifier_value,
651 secret_package,
652 public_package,
653 ))
654}
655
656pub fn dkg_round2(
659 config: &GenesisCeremonyConfig,
660 frost_identifier_value: u16,
661 round1_secret_package: &[u8],
662 round1_packages: BTreeMap<u16, Vec<u8>>,
663) -> Result<RootDkgRound2Output> {
664 config.validate()?;
665 if round1_packages.len() != usize::from(config.max_signers - 1) {
666 return Err(RootError::Frost {
667 detail: "round two requires all twelve peer round-one packages".to_owned(),
668 });
669 }
670 let participant_identifier =
671 rostered_frost_identifier(config, frost_identifier_value, "round-two")?;
672 let secret_package = deserialize_frost(round1_secret_package)?;
673 let inbound_round1 =
674 deserialize_round1_packages(config, participant_identifier, round1_packages)?;
675 let (round2_secret_package, outbound) =
676 frost::keys::dkg::part2(secret_package, &inbound_round1).map_err(frost_error)?;
677 let mut round2_packages = BTreeMap::new();
678 for (recipient, package) in outbound {
679 let recipient_value = identifier_value(config, recipient)?;
680 round2_packages.insert(recipient_value, serialize_frost_secret(&package)?);
681 }
682 let round2_secret_package = serialize_frost_secret(&round2_secret_package)?;
683 Ok(RootDkgRound2Output::from_zeroizing(
684 frost_identifier_value,
685 round2_secret_package,
686 round2_packages,
687 ))
688}
689
690pub fn dkg_finalize_participant(
693 config: &GenesisCeremonyConfig,
694 frost_identifier_value: u16,
695 round2_secret_package: &[u8],
696 round1_packages: BTreeMap<u16, Vec<u8>>,
697 round2_packages: BTreeMap<u16, Vec<u8>>,
698) -> Result<RootParticipantDkgOutput> {
699 let round2_packages = zeroizing_byte_map(round2_packages);
700 dkg_finalize_participant_zeroizing(
701 config,
702 frost_identifier_value,
703 round2_secret_package,
704 round1_packages,
705 round2_packages,
706 )
707}
708
709pub fn dkg_finalize_participant_zeroizing(
716 config: &GenesisCeremonyConfig,
717 frost_identifier_value: u16,
718 round2_secret_package: &[u8],
719 round1_packages: BTreeMap<u16, Vec<u8>>,
720 round2_packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
721) -> Result<RootParticipantDkgOutput> {
722 config.validate()?;
723 if round1_packages.len() != usize::from(config.max_signers - 1) {
724 return Err(RootError::Frost {
725 detail: "finalize requires all twelve peer round-one packages".to_owned(),
726 });
727 }
728 if round2_packages.len() != usize::from(config.max_signers - 1) {
729 return Err(RootError::Frost {
730 detail: "finalize requires all twelve peer round-two packages".to_owned(),
731 });
732 }
733 let participant_identifier =
734 rostered_frost_identifier(config, frost_identifier_value, "finalize")?;
735 let secret_package = deserialize_frost(round2_secret_package)?;
736 let inbound_round1 =
737 deserialize_round1_packages(config, participant_identifier, round1_packages)?;
738 let inbound_round2 =
739 deserialize_round2_packages(config, participant_identifier, round2_packages)?;
740 let (key_package, public_key_package) =
741 frost::keys::dkg::part3(&secret_package, &inbound_round1, &inbound_round2)
742 .map_err(frost_error)?;
743 let key_package = serialize_frost_secret(&key_package)?;
744 let public_key_package = serialize_public_key_package(config, &public_key_package)?;
745 Ok(RootParticipantDkgOutput {
746 key_package: RootKeyPackage::from_zeroizing(frost_identifier_value, key_package),
747 public_key_package,
748 })
749}
750
751fn deserialize_round1_packages(
752 config: &GenesisCeremonyConfig,
753 participant_identifier: frost::Identifier,
754 packages: BTreeMap<u16, Vec<u8>>,
755) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round1::Package>> {
756 let mut result = BTreeMap::new();
757 for (sender, package_bytes) in packages {
758 if config.certifier_by_identifier(sender).is_none() {
759 return Err(RootError::InvalidConfig {
760 reason: format!("round-one sender {sender} is not rostered"),
761 });
762 }
763 let sender_identifier = frost_identifier(sender)?;
764 if sender_identifier == participant_identifier {
765 return Err(RootError::Frost {
766 detail: "round-one peer packages must not include self".to_owned(),
767 });
768 }
769 let package = deserialize_frost(package_bytes.as_slice())?;
770 result.insert(sender_identifier, package);
771 }
772 Ok(result)
773}
774
775fn deserialize_round2_packages(
776 config: &GenesisCeremonyConfig,
777 participant_identifier: frost::Identifier,
778 packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
779) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round2::Package>> {
780 let mut result = BTreeMap::new();
781 for (sender, package_bytes) in packages {
782 if config.certifier_by_identifier(sender).is_none() {
783 return Err(RootError::InvalidConfig {
784 reason: format!("round-two sender {sender} is not rostered"),
785 });
786 }
787 let sender_identifier = frost_identifier(sender)?;
788 if sender_identifier == participant_identifier {
789 return Err(RootError::Frost {
790 detail: "round-two peer packages must not include self".to_owned(),
791 });
792 }
793 let package = deserialize_frost(package_bytes.as_slice())?;
794 result.insert(sender_identifier, package);
795 }
796 Ok(result)
797}
798
799fn require_recipient_round2_packages(
800 round2_packages: Option<RecipientRound2Packages>,
801 identifier: u16,
802) -> Result<RecipientRound2Packages> {
803 round2_packages.ok_or_else(|| RootError::Frost {
804 detail: format!("missing recipient-bound round-two packages for {identifier}"),
805 })
806}
807
808fn require_first_public_key_package(
809 public_key_package: Option<RootPublicKeyPackage>,
810) -> Result<RootPublicKeyPackage> {
811 public_key_package.ok_or_else(|| RootError::Frost {
812 detail: "missing first participant public key package".to_owned(),
813 })
814}
815
816pub fn run_complete_dkg<R>(config: &GenesisCeremonyConfig, rng: &mut R) -> Result<RootDkgOutput>
822where
823 R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
824{
825 config.validate()?;
826
827 let mut round1_outputs: BTreeMap<u16, Zeroizing<RootDkgRound1Output>> = BTreeMap::new();
828 let mut round1_public = BTreeMap::new();
829 for certifier in &config.certifiers {
830 let output = Zeroizing::new(dkg_round1(config, certifier.frost_identifier, rng)?);
831 round1_public.insert(certifier.frost_identifier, output.round1_package.clone());
832 round1_outputs.insert(certifier.frost_identifier, output);
833 }
834
835 let mut round2_secrets = BTreeMap::new();
836 let mut round2_by_recipient: BTreeMap<u16, RecipientRound2Packages> = BTreeMap::new();
837 for (identifier, round1_output) in round1_outputs {
838 let peer_round1 = peer_packages_except(&round1_public, identifier);
839 let secret = &round1_output.round1_secret_package;
840 let mut round2 = dkg_round2(config, identifier, secret, peer_round1)?;
841 let (round2_secret, round2_packages) = round2.take_zeroizing_parts();
842 for (recipient, package) in round2_packages {
843 let recipient_packages = round2_by_recipient.entry(recipient).or_default();
844 recipient_packages.insert(identifier, package);
845 }
846 round2_secrets.insert(identifier, round2_secret);
847 }
848
849 let mut key_packages: BTreeMap<u16, Zeroizing<RootKeyPackage>> = BTreeMap::new();
850 let first_identifier = config.certifiers[0].frost_identifier;
851 let mut public_key_package = None;
852 for (identifier, round2_secret) in round2_secrets {
853 let peer_round1 = peer_packages_except(&round1_public, identifier);
854 let recipient_packages = round2_by_recipient.remove(&identifier);
855 let round2 = require_recipient_round2_packages(recipient_packages, identifier)?;
856 let participant = dkg_finalize_participant_zeroizing(
857 config,
858 identifier,
859 &round2_secret,
860 peer_round1,
861 round2,
862 )?;
863 let key_package = Zeroizing::new(participant.key_package);
864 if identifier == first_identifier {
865 public_key_package = Some(participant.public_key_package);
866 }
867 key_packages.insert(identifier, key_package);
868 }
869
870 let public_key_package = require_first_public_key_package(public_key_package)?;
871 let key_packages = key_packages
873 .into_iter()
874 .map(|(identifier, mut package)| {
875 let key_package = RootKeyPackage {
876 frost_identifier: package.frost_identifier,
877 key_package: std::mem::take(&mut package.key_package),
878 };
879 (identifier, key_package)
880 })
881 .collect();
882 Ok(RootDkgOutput {
883 key_packages,
884 public_key_package,
885 })
886}
887
888#[cfg(test)]
889mod tests {
890 use exo_core::{Did, Hash256, PublicKey, Timestamp};
891 use rand::{SeedableRng, rngs::StdRng};
892 use serde::{Serialize, ser::SerializeSeq};
893
894 use super::*;
895 use crate::CertifierContact;
896
897 fn test_config() -> GenesisCeremonyConfig {
898 let certifiers = (1..=13)
899 .map(|identifier| {
900 let byte = u8::try_from(identifier).expect("identifier fits");
901 CertifierContact {
902 did: Did::new(&format!("did:exo:unit-{identifier:02}")).expect("valid did"),
903 frost_identifier: identifier,
904 signing_public_key: PublicKey::from_bytes([byte; 32]),
905 transport_public_key: [byte; 32],
906 }
907 })
908 .collect();
909 GenesisCeremonyConfig {
910 ceremony_id: "unit-root".into(),
911 network_id: "unit-net".into(),
912 repo_commit: "d8927686a34bdc28ba36d53938f665685d2c4c04".into(),
913 constitution_hash: Hash256::digest(b"constitution"),
914 threshold: 7,
915 max_signers: 13,
916 created_at: Timestamp::new(1, 0),
917 certifiers,
918 signing_set: (1..=7).collect(),
919 }
920 }
921
922 #[derive(Serialize)]
923 struct LegacyRootKeyPackage<'a> {
924 frost_identifier: u16,
925 key_package: &'a Vec<u8>,
926 }
927
928 #[derive(Serialize)]
929 struct LegacyRound1Output<'a> {
930 frost_identifier: u16,
931 round1_secret_package: &'a Vec<u8>,
932 round1_package: &'a Vec<u8>,
933 }
934
935 #[derive(Serialize)]
936 struct LegacyRound2Output<'a> {
937 frost_identifier: u16,
938 round2_secret_package: &'a Vec<u8>,
939 round2_packages: &'a BTreeMap<u16, Vec<u8>>,
940 }
941
942 #[derive(Serialize)]
943 struct MalformedKeyPackage {
944 frost_identifier: u16,
945 key_package: Vec<serde_json::Value>,
946 }
947
948 #[derive(Serialize)]
949 struct MalformedRound1Output {
950 frost_identifier: u16,
951 round1_secret_package: Vec<serde_json::Value>,
952 round1_package: Vec<u8>,
953 }
954
955 #[derive(Serialize)]
956 struct MalformedRound2SecretOutput {
957 frost_identifier: u16,
958 round2_secret_package: Vec<serde_json::Value>,
959 round2_packages: BTreeMap<u16, Vec<u8>>,
960 }
961
962 #[derive(Serialize)]
963 struct MalformedRound2MapOutput {
964 frost_identifier: u16,
965 round2_secret_package: Vec<u8>,
966 round2_packages: BTreeMap<u16, Vec<serde_json::Value>>,
967 }
968
969 struct FailingSecretSerialize;
970
971 impl Serialize for FailingSecretSerialize {
972 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
973 where
974 S: serde::Serializer,
975 {
976 let mut sequence = serializer.serialize_seq(Some(4))?;
977 sequence.serialize_element(&0xdeu8)?;
978 sequence.serialize_element(&0xadu8)?;
979 Err(<S::Error as serde::ser::Error>::custom(
980 "forced secret serialization failure",
981 ))
982 }
983 }
984
985 struct NoSizeHintByteSequence {
986 bytes: std::vec::IntoIter<u8>,
987 }
988
989 impl<'de> serde::de::SeqAccess<'de> for NoSizeHintByteSequence {
990 type Error = serde::de::value::Error;
991
992 fn next_element_seed<T>(
993 &mut self,
994 seed: T,
995 ) -> std::result::Result<Option<T::Value>, Self::Error>
996 where
997 T: serde::de::DeserializeSeed<'de>,
998 {
999 match self.bytes.next() {
1000 Some(byte) => seed
1001 .deserialize(serde::de::value::U8Deserializer::new(byte))
1002 .map(Some),
1003 None => Ok(None),
1004 }
1005 }
1006
1007 fn size_hint(&self) -> Option<usize> {
1008 None
1009 }
1010 }
1011
1012 fn cbor_bytes(value: &impl Serialize) -> Vec<u8> {
1013 let mut bytes = Vec::new();
1014 ciborium::into_writer(value, &mut bytes).expect("CBOR encoding");
1015 bytes
1016 }
1017
1018 fn assert_explicitly_zeroizable<T: zeroize::Zeroize>(_: &T) {}
1019
1020 #[test]
1021 fn legacy_secret_dkg_byte_carriers_support_explicit_zeroize() {
1022 let mut key_package = RootKeyPackage {
1023 frost_identifier: 7,
1024 key_package: vec![0xde, 0xad, 0xbe, 0xef],
1025 };
1026 let mut round1 = RootDkgRound1Output {
1027 frost_identifier: 7,
1028 round1_secret_package: vec![0xca, 0xfe, 0xba, 0xbe],
1029 round1_package: vec![1, 2, 3],
1030 };
1031 let mut round2 = RootDkgRound2Output {
1032 frost_identifier: 7,
1033 round2_secret_package: vec![0x12, 0x34, 0x56, 0x78],
1034 round2_packages: BTreeMap::from([(8, vec![4, 5, 6])]),
1035 };
1036
1037 assert_explicitly_zeroizable(&key_package);
1038 assert_explicitly_zeroizable(&round1);
1039 assert_explicitly_zeroizable(&round2);
1040
1041 key_package.zeroize();
1042 round1.zeroize();
1043 round2.zeroize();
1044 assert!(key_package.key_package.is_empty());
1045 assert!(round1.round1_secret_package.is_empty());
1046 assert!(round2.round2_secret_package.is_empty());
1047 assert!(round2.round2_packages.is_empty());
1048 let public_key_package = RootPublicKeyPackage {
1049 public_key_package: vec![1],
1050 root_public_key: vec![2],
1051 verifying_shares: BTreeMap::from([(7, vec![3])]),
1052 };
1053 let mut participant = RootParticipantDkgOutput {
1054 key_package: RootKeyPackage {
1055 frost_identifier: 7,
1056 key_package: vec![4, 5, 6],
1057 },
1058 public_key_package: public_key_package.clone(),
1059 };
1060 assert_explicitly_zeroizable(&participant);
1061 participant.zeroize();
1062 assert!(participant.key_package.key_package.is_empty());
1063 assert_eq!(participant.key_package.frost_identifier, 7);
1064 assert_eq!(participant.public_key_package, public_key_package);
1065 }
1066
1067 #[test]
1068 fn secret_dkg_debug_redacts_every_private_package() {
1069 let key_secret = vec![0xde, 0xad, 0xbe, 0xef];
1070 let round1_secret = vec![0xca, 0xfe, 0xba, 0xbe];
1071 let round2_secret = vec![0x12, 0x34, 0x56, 0x78];
1072 let key_package = RootKeyPackage {
1073 frost_identifier: 7,
1074 key_package: key_secret.clone(),
1075 };
1076 let round1 = RootDkgRound1Output {
1077 frost_identifier: 7,
1078 round1_secret_package: round1_secret.clone(),
1079 round1_package: vec![1, 2, 3],
1080 };
1081 let round2 = RootDkgRound2Output {
1082 frost_identifier: 7,
1083 round2_secret_package: round2_secret.clone(),
1084 round2_packages: BTreeMap::from([(8, vec![4, 5, 6])]),
1085 };
1086
1087 for (rendered, secret) in [
1088 (format!("{key_package:?}"), format!("{key_secret:?}")),
1089 (format!("{round1:?}"), format!("{round1_secret:?}")),
1090 (format!("{round2:?}"), format!("{round2_secret:?}")),
1091 ] {
1092 assert!(
1093 rendered.contains("[REDACTED]"),
1094 "secret-bearing DKG debug output must visibly redact: {rendered}"
1095 );
1096 assert!(
1097 !rendered.contains(&secret),
1098 "secret-bearing DKG debug output exposed fixture bytes: {rendered}"
1099 );
1100 }
1101 }
1102
1103 #[test]
1104 fn secret_round_two_recipient_packages_are_fully_redacted() {
1105 let recipient_secret = vec![0x99, 0x88, 0x77, 0x66];
1106 let output = RootDkgRound2Output {
1107 frost_identifier: 7,
1108 round2_secret_package: vec![0x12, 0x34, 0x56, 0x78],
1109 round2_packages: BTreeMap::from([(8, recipient_secret.clone())]),
1110 };
1111
1112 let rendered = format!("{output:?}");
1113 assert!(
1114 rendered.matches("[REDACTED]").count() >= 2,
1115 "both round-two secret fields must be visibly redacted: {rendered}"
1116 );
1117 assert!(
1118 !rendered.contains(&format!("{recipient_secret:?}")),
1119 "recipient-bound round-two package leaked through Debug: {rendered}"
1120 );
1121 assert_explicitly_zeroizable(&output);
1122 }
1123
1124 #[test]
1125 fn secret_byte_deserializer_clamps_hints_and_grows_from_fully_initialized_storage() {
1126 let mut accumulator = ZeroizingByteAccumulator::with_size_hint(usize::MAX)
1127 .expect("bounded initial allocation");
1128 assert_eq!(accumulator.bytes.len(), accumulator.bytes.capacity());
1129 assert!(accumulator.bytes.capacity() <= 4096);
1130
1131 let bytes_to_add = accumulator.bytes.capacity() + 1;
1132 for index in 0..bytes_to_add {
1133 accumulator
1134 .push(u8::try_from(index % 251).expect("fixture byte"))
1135 .expect("zeroizing growth");
1136 }
1137 assert_eq!(accumulator.bytes.len(), accumulator.bytes.capacity());
1138 let finished = accumulator.finish();
1139 assert_eq!(finished.len(), bytes_to_add);
1140 assert_explicitly_zeroizable(&finished);
1141 }
1142
1143 #[test]
1144 fn secret_byte_deserializer_accepts_sequences_without_size_hints() {
1145 let sequence = NoSizeHintByteSequence {
1146 bytes: vec![0xde, 0xad, 0xbe, 0xef].into_iter(),
1147 };
1148 let deserializer = serde::de::value::SeqAccessDeserializer::new(sequence);
1149
1150 let mut decoded =
1151 deserialize_zeroizing_bytes(deserializer).expect("sequence without a size hint");
1152 assert_eq!(decoded.as_slice(), &[0xde, 0xad, 0xbe, 0xef]);
1153 decoded.zeroize();
1154 assert!(decoded.is_empty());
1155 }
1156
1157 #[test]
1158 fn secret_byte_capacity_guard_rejects_terminal_capacity() {
1159 let error = next_secret_capacity(MAX_SECRET_CAPACITY)
1160 .expect_err("the bounded secret buffer must never grow past its terminal capacity");
1161 assert_eq!(
1162 error.0, "root secret bytes exceed supported capacity",
1163 "capacity exhaustion must remain a typed, diagnostic failure"
1164 );
1165 }
1166
1167 #[test]
1168 fn secret_byte_writer_wipes_before_growth_and_on_serialization_error() {
1169 fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
1170 assert_zeroize_on_drop::<ZeroizingByteAccumulator>();
1171 assert_zeroize_on_drop::<ZeroizingByteWriter>();
1172
1173 let fixture: Vec<u8> = (0..=u16::try_from(MAX_INITIAL_SECRET_CAPACITY).unwrap_or(u16::MAX))
1174 .map(|index| u8::try_from(index % 251).expect("fixture byte"))
1175 .collect();
1176 let mut writer = ZeroizingByteWriter::new();
1177 writer.write_all(&fixture).expect("forced-growth write");
1178 writer
1179 .flush()
1180 .expect("the unbuffered writer flushes safely");
1181 assert_eq!(
1182 writer.accumulator.bytes.len(),
1183 writer.accumulator.bytes.capacity()
1184 );
1185 assert_eq!(writer.finish().as_slice(), fixture.as_slice());
1186
1187 let error = serialize_frost_secret(&FailingSecretSerialize)
1188 .expect_err("serialization error must not release a secret buffer");
1189 assert!(
1190 error
1191 .to_string()
1192 .contains("forced secret serialization failure")
1193 );
1194 }
1195
1196 fn derive_before<'a>(source: &'a str, declaration: &str) -> &'a str {
1197 let declaration_offset = source
1198 .find(declaration)
1199 .expect("secret carrier declaration");
1200 let prefix = &source[..declaration_offset];
1201 let derive_offset = prefix.rfind("#[derive(").expect("secret carrier derive");
1202 &prefix[derive_offset..]
1203 }
1204
1205 #[test]
1206 fn complete_dkg_moves_internal_zeroizing_secret_packages_without_clone() {
1207 let dkg_source = include_str!("dkg.rs").split("#[cfg(test)]").next().unwrap();
1208 let signing_source = include_str!("signing.rs")
1209 .split("#[cfg(test)]")
1210 .next()
1211 .unwrap();
1212 for (declaration, intermediate) in [
1213 ("pub struct RootDkgOutput", "ZeroizingRootDkgOutput"),
1214 (
1215 "pub struct RootParticipantDkgOutput",
1216 "ZeroizingRootParticipantDkgOutput",
1217 ),
1218 ] {
1219 assert!(
1220 derive_before(dkg_source, declaration)
1221 .contains(&format!("#[serde(from = \"{intermediate}\")]")),
1222 "{declaration} must guard private fields before later composite decoding fails"
1223 );
1224 }
1225 assert!(dkg_source.contains("key_packages: BTreeMap<u16, ZeroizingRootKeyPackage>"));
1228 assert!(dkg_source.contains("key_package: ZeroizingRootKeyPackage"));
1229 let signing_production = signing_source.split("#[cfg(test)]").next().unwrap();
1230 let legacy_signing = signing_production
1231 .split("pub fn threshold_sign<R>")
1232 .nth(1)
1233 .unwrap()
1234 .split("pub fn threshold_sign_zeroizing<R>")
1235 .next()
1236 .unwrap();
1237 assert!(legacy_signing.contains("Zeroizing::new(share)"));
1238 assert!(legacy_signing.contains("threshold_sign_zeroizing("));
1239 assert!(
1240 !legacy_signing.contains('?'),
1241 "all consumed legacy shares must be guarded before any fallible signing work"
1242 );
1243 assert!(
1244 !derive_before(signing_source, "pub struct RootSigningNonces").contains("Clone"),
1245 "RootSigningNonces must not regain secret duplication through Clone"
1246 );
1247 assert_eq!(
1248 dkg_source
1249 .matches("#[serde(deserialize_with = \"deserialize_zeroizing_bytes\")]")
1250 .count(),
1251 3,
1252 "all three direct DKG secret-byte fields must use the shared visitor"
1253 );
1254 assert_eq!(
1255 dkg_source
1256 .matches("#[serde(deserialize_with = \"deserialize_zeroizing_byte_map\")]")
1257 .count(),
1258 1,
1259 "the recipient-bound round-two map must use the shared byte visitor"
1260 );
1261 assert_eq!(
1262 signing_source
1263 .matches("#[serde(deserialize_with = \"deserialize_zeroizing_bytes\")]")
1264 .count(),
1265 1,
1266 "signing nonces must use the shared visitor"
1267 );
1268 let complete_dkg = dkg_source
1269 .split("pub fn run_complete_dkg")
1270 .nth(1)
1271 .expect("complete DKG implementation")
1272 .split("#[cfg(test)]")
1273 .next()
1274 .expect("complete DKG ends before tests");
1275 assert!(complete_dkg.contains("BTreeMap<u16, Zeroizing<RootDkgRound1Output>>"));
1276 assert!(complete_dkg.contains("BTreeMap<u16, Zeroizing<RootKeyPackage>>"));
1277 let public_ready = complete_dkg
1278 .find("require_first_public_key_package(public_key_package)?")
1279 .expect("public result validated before releasing guarded private map");
1280 let private_handoff = complete_dkg
1281 .find("let key_packages = key_packages")
1282 .expect("final legacy private-map handoff");
1283 assert!(public_ready < private_handoff);
1284 let finalize = dkg_source
1285 .split("pub fn dkg_finalize_participant_zeroizing")
1286 .nth(1)
1287 .unwrap()
1288 .split("fn deserialize_round1_packages")
1289 .next()
1290 .unwrap();
1291 assert!(
1292 finalize.find("serialize_public_key_package(").unwrap()
1293 < finalize.find("RootKeyPackage::from_zeroizing(").unwrap(),
1294 "serialized private key must stay guarded through fallible public serialization"
1295 );
1296 for forbidden in [
1297 "recipient_packages.insert(*identifier, package.clone())",
1298 "round2_by_recipient[&first_identifier].clone()",
1299 "round2_by_recipient[&identifier].clone()",
1300 ] {
1301 assert!(
1302 !complete_dkg.contains(forbidden),
1303 "complete DKG must move each recipient secret exactly once: {forbidden}"
1304 );
1305 }
1306 assert!(
1307 complete_dkg.contains("for (recipient, package) in round2_packages"),
1308 "complete DKG must consume outbound recipient packages"
1309 );
1310 assert!(
1311 complete_dkg.contains("round2_by_recipient.remove(&identifier)"),
1312 "complete DKG must remove each recipient map for finalization"
1313 );
1314 }
1315
1316 #[test]
1317 fn secret_dkg_deserializers_reject_partially_decoded_json_and_cbor() {
1318 #[derive(Serialize)]
1319 struct LateInvalidParticipant {
1320 key_package: RootKeyPackage,
1321 public_key_package: bool,
1322 }
1323 #[derive(Serialize)]
1324 struct LateInvalidDkg {
1325 key_packages: BTreeMap<u16, RootKeyPackage>,
1326 public_key_package: bool,
1327 }
1328 #[derive(Serialize)]
1329 struct LateInvalidDkgMap {
1330 key_packages: BTreeMap<u16, MalformedKeyPackage>,
1331 public_key_package: bool,
1332 }
1333 let key_fixture = RootKeyPackage {
1334 frost_identifier: 7,
1335 key_package: vec![1, 2, 3],
1336 };
1337 let participant = LateInvalidParticipant {
1338 key_package: key_fixture.clone(),
1339 public_key_package: false,
1340 };
1341 let dkg = LateInvalidDkg {
1342 key_packages: BTreeMap::from([(7, key_fixture)]),
1343 public_key_package: false,
1344 };
1345 assert!(
1346 serde_json::from_slice::<RootParticipantDkgOutput>(
1347 &serde_json::to_vec(&participant).expect("late invalid participant JSON")
1348 )
1349 .is_err()
1350 );
1351 assert!(
1352 ciborium::from_reader::<RootParticipantDkgOutput, _>(
1353 cbor_bytes(&participant).as_slice()
1354 )
1355 .is_err()
1356 );
1357 assert!(
1358 serde_json::from_slice::<RootDkgOutput>(
1359 &serde_json::to_vec(&dkg).expect("late invalid DKG JSON")
1360 )
1361 .is_err()
1362 );
1363 assert!(ciborium::from_reader::<RootDkgOutput, _>(cbor_bytes(&dkg).as_slice()).is_err());
1364 let partial_map = LateInvalidDkgMap {
1365 key_packages: BTreeMap::from([
1366 (
1367 7,
1368 MalformedKeyPackage {
1369 frost_identifier: 7,
1370 key_package: vec![serde_json::json!(1), serde_json::json!(2)],
1371 },
1372 ),
1373 (
1374 8,
1375 MalformedKeyPackage {
1376 frost_identifier: 8,
1377 key_package: vec![serde_json::json!(3), serde_json::json!("invalid")],
1378 },
1379 ),
1380 ]),
1381 public_key_package: false,
1382 };
1383 assert!(
1384 serde_json::from_slice::<RootDkgOutput>(
1385 &serde_json::to_vec(&partial_map).expect("partial map JSON")
1386 )
1387 .is_err()
1388 );
1389 assert!(
1390 ciborium::from_reader::<RootDkgOutput, _>(cbor_bytes(&partial_map).as_slice()).is_err()
1391 );
1392 let invalid_bytes = vec![
1393 serde_json::json!(1),
1394 serde_json::json!(2),
1395 serde_json::json!("bad"),
1396 serde_json::json!(3),
1397 ];
1398 let key = MalformedKeyPackage {
1399 frost_identifier: 7,
1400 key_package: invalid_bytes.clone(),
1401 };
1402 let round1 = MalformedRound1Output {
1403 frost_identifier: 7,
1404 round1_secret_package: invalid_bytes.clone(),
1405 round1_package: vec![3],
1406 };
1407 let round2_secret = MalformedRound2SecretOutput {
1408 frost_identifier: 7,
1409 round2_secret_package: invalid_bytes.clone(),
1410 round2_packages: BTreeMap::from([(8, vec![3])]),
1411 };
1412 let round2_map = MalformedRound2MapOutput {
1413 frost_identifier: 7,
1414 round2_secret_package: vec![1, 2, 3],
1415 round2_packages: BTreeMap::from([(8, invalid_bytes)]),
1416 };
1417
1418 let key_json = serde_json::to_vec(&key).expect("key malformed JSON fixture");
1419 let round1_json = serde_json::to_vec(&round1).expect("round-one malformed JSON fixture");
1420 let round2_secret_json =
1421 serde_json::to_vec(&round2_secret).expect("round-two secret malformed JSON fixture");
1422 let round2_map_json =
1423 serde_json::to_vec(&round2_map).expect("round-two map malformed JSON fixture");
1424 assert!(serde_json::from_slice::<RootKeyPackage>(&key_json).is_err());
1425 assert!(serde_json::from_slice::<RootDkgRound1Output>(&round1_json).is_err());
1426 assert!(serde_json::from_slice::<RootDkgRound2Output>(&round2_secret_json).is_err());
1427 assert!(serde_json::from_slice::<RootDkgRound2Output>(&round2_map_json).is_err());
1428
1429 let key_cbor = cbor_bytes(&key);
1430 let round1_cbor = cbor_bytes(&round1);
1431 let round2_secret_cbor = cbor_bytes(&round2_secret);
1432 let round2_map_cbor = cbor_bytes(&round2_map);
1433 assert!(ciborium::from_reader::<RootKeyPackage, _>(key_cbor.as_slice()).is_err());
1434 assert!(ciborium::from_reader::<RootDkgRound1Output, _>(round1_cbor.as_slice()).is_err());
1435 assert!(
1436 ciborium::from_reader::<RootDkgRound2Output, _>(round2_secret_cbor.as_slice()).is_err()
1437 );
1438 assert!(
1439 ciborium::from_reader::<RootDkgRound2Output, _>(round2_map_cbor.as_slice()).is_err()
1440 );
1441 }
1442
1443 #[test]
1444 fn secret_dkg_deserializers_round_trip_all_secret_carriers() {
1445 let mut key_fixture = RootKeyPackage {
1446 frost_identifier: 7,
1447 key_package: vec![0xde, 0xad, 0xbe, 0xef],
1448 };
1449 let public_key_package = RootPublicKeyPackage {
1450 public_key_package: vec![1],
1451 root_public_key: vec![2],
1452 verifying_shares: BTreeMap::from([(7, vec![3])]),
1453 };
1454 let participant = RootParticipantDkgOutput {
1455 key_package: key_fixture.clone(),
1456 public_key_package: public_key_package.clone(),
1457 };
1458 let participant_json = serde_json::to_vec(&participant).expect("participant JSON");
1459 let participant_cbor = cbor_bytes(&participant);
1460 let guarded: Zeroizing<RootParticipantDkgOutput> =
1461 serde_json::from_slice(&participant_json).expect("guarded participant JSON");
1462 assert_eq!(*guarded, participant);
1463 assert_eq!(
1464 serde_json::to_vec(&guarded).expect("guarded JSON"),
1465 participant_json
1466 );
1467 assert_eq!(cbor_bytes(&guarded), participant_cbor);
1468 let guarded_cbor: Zeroizing<RootParticipantDkgOutput> =
1469 ciborium::from_reader(participant_cbor.as_slice()).expect("guarded participant CBOR");
1470 assert_eq!(*guarded_cbor, participant);
1471 let dkg = RootDkgOutput {
1472 key_packages: BTreeMap::from([(7, key_fixture.clone())]),
1473 public_key_package,
1474 };
1475 assert_eq!(
1476 serde_json::from_slice::<RootDkgOutput>(
1477 &serde_json::to_vec(&dkg).expect("complete DKG JSON")
1478 )
1479 .expect("complete DKG JSON round trip"),
1480 dkg
1481 );
1482 assert_eq!(
1483 ciborium::from_reader::<RootDkgOutput, _>(cbor_bytes(&dkg).as_slice())
1484 .expect("complete DKG CBOR round trip"),
1485 dkg
1486 );
1487 fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>(_: &T) {}
1488 assert_zeroize_on_drop(&guarded);
1489 let key_json =
1490 Zeroizing::new(serde_json::to_vec(&key_fixture).expect("key-package JSON fixture"));
1491 let mut key_json_round_trip = serde_json::from_slice::<RootKeyPackage>(&key_json)
1492 .expect("key-package JSON secret bytes");
1493 assert_eq!(key_json_round_trip, key_fixture);
1494 let key_cbor = Zeroizing::new(cbor_bytes(&key_fixture));
1495 let mut key_cbor_round_trip =
1496 ciborium::from_reader::<RootKeyPackage, _>(key_cbor.as_slice())
1497 .expect("key-package CBOR secret bytes");
1498 assert_eq!(key_cbor_round_trip, key_fixture);
1499
1500 let mut round1_fixture = RootDkgRound1Output {
1501 frost_identifier: 7,
1502 round1_secret_package: vec![0xca, 0xfe, 0xba, 0xbe],
1503 round1_package: vec![1, 2, 3],
1504 };
1505 let round1_json =
1506 Zeroizing::new(serde_json::to_vec(&round1_fixture).expect("round-one JSON fixture"));
1507 let mut round1_json_round_trip =
1508 serde_json::from_slice::<RootDkgRound1Output>(&round1_json)
1509 .expect("round-one JSON secret bytes");
1510 assert_eq!(round1_json_round_trip, round1_fixture);
1511 let round1_cbor = Zeroizing::new(cbor_bytes(&round1_fixture));
1512 let mut round1_cbor_round_trip =
1513 ciborium::from_reader::<RootDkgRound1Output, _>(round1_cbor.as_slice())
1514 .expect("round-one CBOR secret bytes");
1515 assert_eq!(round1_cbor_round_trip, round1_fixture);
1516
1517 let mut round2_fixture = RootDkgRound2Output {
1518 frost_identifier: 7,
1519 round2_secret_package: vec![1, 2, 3],
1520 round2_packages: BTreeMap::from([(8, vec![4, 5, 6]), (9, vec![7, 8, 9])]),
1521 };
1522
1523 let json =
1524 Zeroizing::new(serde_json::to_vec(&round2_fixture).expect("round-two JSON fixture"));
1525 let mut json_round_trip = serde_json::from_slice::<RootDkgRound2Output>(&json)
1526 .expect("round-two JSON secret map");
1527 assert_eq!(json_round_trip, round2_fixture);
1528
1529 let cbor = Zeroizing::new(cbor_bytes(&round2_fixture));
1530 let mut cbor_round_trip = ciborium::from_reader::<RootDkgRound2Output, _>(cbor.as_slice())
1531 .expect("round-two CBOR secret map");
1532 assert_eq!(cbor_round_trip, round2_fixture);
1533
1534 for secret_carrier in [
1535 &mut key_fixture,
1536 &mut key_json_round_trip,
1537 &mut key_cbor_round_trip,
1538 ] {
1539 secret_carrier.zeroize();
1540 assert!(secret_carrier.key_package.is_empty());
1541 }
1542 for secret_carrier in [
1543 &mut round1_fixture,
1544 &mut round1_json_round_trip,
1545 &mut round1_cbor_round_trip,
1546 ] {
1547 secret_carrier.zeroize();
1548 assert!(secret_carrier.round1_secret_package.is_empty());
1549 }
1550 for secret_carrier in [
1551 &mut round2_fixture,
1552 &mut json_round_trip,
1553 &mut cbor_round_trip,
1554 ] {
1555 secret_carrier.zeroize();
1556 assert!(secret_carrier.round2_secret_package.is_empty());
1557 assert!(secret_carrier.round2_packages.is_empty());
1558 }
1559 }
1560
1561 #[test]
1562 fn secret_dkg_deserializer_reports_expected_container_shapes() {
1563 let byte_sequence_error = serde_json::from_str::<RootKeyPackage>(
1564 r#"{"frost_identifier":7,"key_package":"not-a-sequence"}"#,
1565 )
1566 .expect_err("a root key secret must be encoded as a byte sequence");
1567 assert!(
1568 byte_sequence_error
1569 .to_string()
1570 .contains("a sequence of root secret bytes"),
1571 "unexpected byte-sequence diagnostic: {byte_sequence_error}"
1572 );
1573
1574 let recipient_map_error = serde_json::from_str::<RootDkgRound2Output>(
1575 r#"{"frost_identifier":7,"round2_secret_package":[1,2,3],"round2_packages":[]}"#,
1576 )
1577 .expect_err("recipient-bound secrets must be encoded as a map");
1578 assert!(
1579 recipient_map_error
1580 .to_string()
1581 .contains("a map of recipient-bound root secret byte sequences"),
1582 "unexpected recipient-map diagnostic: {recipient_map_error}"
1583 );
1584 }
1585
1586 #[test]
1587 fn secret_dkg_json_and_cbor_match_legacy_vec_wire_layout() {
1588 let key_secret = vec![0xde, 0xad, 0xbe, 0xef];
1589 let key_package = RootKeyPackage {
1590 frost_identifier: 7,
1591 key_package: key_secret.clone(),
1592 };
1593 let legacy_key = LegacyRootKeyPackage {
1594 frost_identifier: 7,
1595 key_package: &key_secret,
1596 };
1597 assert_eq!(
1598 serde_json::to_vec(&key_package).expect("key JSON"),
1599 serde_json::to_vec(&legacy_key).expect("legacy key JSON")
1600 );
1601 assert_eq!(cbor_bytes(&key_package), cbor_bytes(&legacy_key));
1602
1603 let round1_secret = vec![0xca, 0xfe, 0xba, 0xbe];
1604 let round1_public = vec![1, 2, 3];
1605 let round1 = RootDkgRound1Output {
1606 frost_identifier: 7,
1607 round1_secret_package: round1_secret.clone(),
1608 round1_package: round1_public.clone(),
1609 };
1610 let legacy_round1 = LegacyRound1Output {
1611 frost_identifier: 7,
1612 round1_secret_package: &round1_secret,
1613 round1_package: &round1_public,
1614 };
1615 assert_eq!(
1616 serde_json::to_vec(&round1).expect("round-one JSON"),
1617 serde_json::to_vec(&legacy_round1).expect("legacy round-one JSON")
1618 );
1619 assert_eq!(cbor_bytes(&round1), cbor_bytes(&legacy_round1));
1620
1621 let round2_secret = vec![0x12, 0x34, 0x56, 0x78];
1622 let round2_public = BTreeMap::from([(8, vec![4, 5, 6])]);
1623 let round2 = RootDkgRound2Output {
1624 frost_identifier: 7,
1625 round2_secret_package: round2_secret.clone(),
1626 round2_packages: round2_public.clone(),
1627 };
1628 let legacy_round2 = LegacyRound2Output {
1629 frost_identifier: 7,
1630 round2_secret_package: &round2_secret,
1631 round2_packages: &round2_public,
1632 };
1633 assert_eq!(
1634 serde_json::to_vec(&round2).expect("round-two JSON"),
1635 serde_json::to_vec(&legacy_round2).expect("legacy round-two JSON")
1636 );
1637 assert_eq!(cbor_bytes(&round2), cbor_bytes(&legacy_round2));
1638 }
1639
1640 #[test]
1641 fn identifier_value_rejects_unrostered_identifier() {
1642 let config = test_config();
1643 let identifier = frost_identifier(14).expect("identifier");
1644 assert!(identifier_value(&config, identifier).is_err());
1645 }
1646
1647 #[test]
1648 fn frost_identifier_rejects_zero_identifier() {
1649 let error = frost_identifier(0).expect_err("zero identifier");
1650 assert!(error.to_string().contains("frost operation failed"));
1651 }
1652
1653 #[test]
1654 fn rostered_identifier_and_encoding_helpers_are_diagnostic() {
1655 let config = test_config();
1656 let identifier = rostered_frost_identifier(&config, 1, "unit").expect("rostered");
1657 assert_eq!(identifier, frost_identifier(1).expect("identifier"));
1658
1659 let error =
1660 rostered_frost_identifier(&config, 14, "unit").expect_err("unrostered certifier");
1661 assert!(
1662 error
1663 .to_string()
1664 .contains("unit certifier 14 is not rostered")
1665 );
1666
1667 let encoding_error = frost_encoding_error("unit failure");
1668 assert!(
1669 encoding_error
1670 .to_string()
1671 .contains("FROST artifact canonical encoding failed")
1672 );
1673
1674 let encoded = serialize_frost(&7u16).expect("serialize");
1675 let decoded: u16 = deserialize_frost(encoded.as_slice()).expect("deserialize");
1676 assert_eq!(decoded, 7);
1677 assert!(deserialize_frost::<u16>(b"not cbor").is_err());
1678 }
1679
1680 #[test]
1681 fn peer_package_filter_retains_every_non_excluded_package() {
1682 let mut packages = BTreeMap::new();
1683 packages.insert(1, b"one".to_vec());
1684 packages.insert(2, b"two".to_vec());
1685 packages.insert(3, b"three".to_vec());
1686
1687 let peers = peer_packages_except(&packages, 2);
1688
1689 assert_eq!(peers.len(), 2);
1690 assert_eq!(peers.get(&1).expect("peer one"), b"one");
1691 assert_eq!(peers.get(&3).expect("peer three"), b"three");
1692 assert!(!peers.contains_key(&2));
1693 }
1694
1695 #[test]
1696 fn complete_dkg_fail_closed_helpers_reject_missing_assembly_state() {
1697 let recipient_error = require_recipient_round2_packages(None, 7)
1698 .expect_err("a missing recipient package set must abort local DKG assembly");
1699 assert_eq!(
1700 recipient_error,
1701 RootError::Frost {
1702 detail: "missing recipient-bound round-two packages for 7".to_owned()
1703 }
1704 );
1705
1706 let public_error = require_first_public_key_package(None)
1707 .expect_err("a missing first-participant public package must abort assembly");
1708 assert_eq!(
1709 public_error,
1710 RootError::Frost {
1711 detail: "missing first participant public key package".to_owned()
1712 }
1713 );
1714 }
1715
1716 #[test]
1717 fn round_one_and_complete_dkg_success_paths_are_diagnostic() {
1718 let config = test_config();
1719 let mut rng = StdRng::seed_from_u64(11);
1720
1721 let round1 = dkg_round1(&config, 1, &mut rng).expect("round one");
1722 assert_eq!(round1.frost_identifier, 1);
1723 assert!(!round1.round1_secret_package.is_empty());
1724 assert!(!round1.round1_package.is_empty());
1725
1726 let dkg = run_complete_dkg(&config, &mut rng).expect("complete dkg");
1727 assert_eq!(dkg.key_packages.len(), usize::from(config.max_signers));
1728 assert_eq!(
1729 dkg.public_key_package.verifying_shares.len(),
1730 usize::from(config.max_signers)
1731 );
1732 }
1733
1734 #[test]
1735 fn deserialize_peer_package_helpers_reject_bad_sender_sets() {
1736 let config = test_config();
1737 let participant = frost_identifier(1).expect("participant");
1738
1739 assert!(
1740 deserialize_round1_packages(&config, participant, BTreeMap::new())
1741 .expect("empty round-one helper input")
1742 .is_empty()
1743 );
1744 assert!(
1745 deserialize_round2_packages(&config, participant, BTreeMap::new())
1746 .expect("empty round-two helper input")
1747 .is_empty()
1748 );
1749
1750 let mut nonrostered_round1 = BTreeMap::new();
1751 nonrostered_round1.insert(14, Vec::new());
1752 assert!(deserialize_round1_packages(&config, participant, nonrostered_round1).is_err());
1753
1754 let mut self_round1 = BTreeMap::new();
1755 self_round1.insert(1, Vec::new());
1756 assert!(deserialize_round1_packages(&config, participant, self_round1).is_err());
1757
1758 let mut malformed_round1 = BTreeMap::new();
1759 malformed_round1.insert(2, b"not a round-one package".to_vec());
1760 assert!(deserialize_round1_packages(&config, participant, malformed_round1).is_err());
1761
1762 let mut nonrostered_round2 = BTreeMap::new();
1763 nonrostered_round2.insert(14, Zeroizing::new(Vec::new()));
1764 assert!(deserialize_round2_packages(&config, participant, nonrostered_round2).is_err());
1765
1766 let mut self_round2 = BTreeMap::new();
1767 self_round2.insert(1, Zeroizing::new(Vec::new()));
1768 assert!(deserialize_round2_packages(&config, participant, self_round2).is_err());
1769
1770 let mut malformed_round2 = BTreeMap::new();
1771 malformed_round2.insert(2, Zeroizing::new(b"not a round-two package".to_vec()));
1772 assert!(deserialize_round2_packages(&config, participant, malformed_round2).is_err());
1773 }
1774
1775 #[test]
1776 fn serialize_public_key_package_rejects_missing_verification_share() {
1777 let config = test_config();
1778 let mut rng = StdRng::seed_from_u64(7);
1779 let dkg = run_complete_dkg(&config, &mut rng).expect("dkg");
1780 let public: frost::keys::PublicKeyPackage =
1781 deserialize_frost(dkg.public_key_package.public_key_package.as_slice())
1782 .expect("public package");
1783 let mut changed_config = config;
1784 changed_config.certifiers[0].frost_identifier = 14;
1785 assert!(serialize_public_key_package(&changed_config, &public).is_err());
1786 }
1787}