1use std::{
4 collections::{BTreeMap, BTreeSet},
5 fmt,
6};
7
8use exo_core::Hash256;
9use frost_ristretto255 as frost;
10use serde::{Deserialize, Serialize};
11use zeroize::Zeroizing;
12
13use crate::{
14 GenesisCeremonyConfig, Result, RootError, RootKeyPackage, RootPublicKeyPackage,
15 dkg::{
16 deserialize_frost, deserialize_zeroizing_bytes, serialize_frost, serialize_frost_secret,
17 },
18};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct RootSignature {
23 pub signature: Vec<u8>,
25 pub signer_ids: Vec<u16>,
27}
28
29fn frost_error(error: frost::Error) -> RootError {
30 RootError::Frost {
31 detail: error.to_string(),
32 }
33}
34
35fn frost_sign_share(
36 signing_package: &frost::SigningPackage,
37 nonces: &frost::round1::SigningNonces,
38 key_package: &frost::keys::KeyPackage,
39) -> Result<frost::round2::SignatureShare> {
40 frost::round2::sign(signing_package, nonces, key_package).map_err(frost_error)
41}
42
43fn frost_aggregate_signature(
44 signing_package: &frost::SigningPackage,
45 signature_shares: &BTreeMap<frost::Identifier, frost::round2::SignatureShare>,
46 public: &frost::keys::PublicKeyPackage,
47) -> Result<frost::Signature> {
48 frost::aggregate(signing_package, signature_shares, public).map_err(frost_error)
49}
50
51pub fn threshold_sign<R>(
53 config: &GenesisCeremonyConfig,
54 public_key_package: &RootPublicKeyPackage,
55 shares: BTreeMap<u16, RootKeyPackage>,
56 message: &[u8],
57 rng: &mut R,
58) -> Result<RootSignature>
59where
60 R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
61{
62 let shares = shares
63 .into_iter()
64 .map(|(identifier, share)| (identifier, Zeroizing::new(share)))
65 .collect();
66 threshold_sign_zeroizing(config, public_key_package, shares, message, rng)
67}
68
69pub fn threshold_sign_zeroizing<R>(
76 config: &GenesisCeremonyConfig,
77 public_key_package: &RootPublicKeyPackage,
78 shares: BTreeMap<u16, Zeroizing<RootKeyPackage>>,
79 message: &[u8],
80 rng: &mut R,
81) -> Result<RootSignature>
82where
83 R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
84{
85 config.validate()?;
86 if shares.len() < usize::from(config.threshold) {
87 let supplied = shares
88 .keys()
89 .take(usize::from(u16::MAX))
90 .fold(0u16, |count, _| count.saturating_add(1));
91 let error = RootError::ThresholdNotMet {
92 required: config.threshold,
93 supplied,
94 };
95 return Err(error);
96 }
97 let signer_ids: Vec<u16> = shares.keys().copied().collect();
98 validate_root_signer_ids(config, signer_ids.as_slice())?;
99
100 let public = deserialize_frost(public_key_package.public_key_package.as_slice())?;
101
102 let mut key_packages = BTreeMap::new();
103 let mut signing_nonces = BTreeMap::new();
104 let mut signing_commitments = BTreeMap::new();
105
106 for (identifier, share) in shares {
107 if share.frost_identifier != identifier {
108 let share_id = share.frost_identifier;
109 let detail = format!("share id {share_id} mismatches key {identifier}");
110 return Err(RootError::Frost { detail });
111 }
112 let frost_identifier = crate::dkg::frost_identifier(identifier)?;
113 let key_package: frost::keys::KeyPackage = deserialize_frost(share.key_package.as_slice())?;
114 if *key_package.identifier() != frost_identifier {
115 return Err(RootError::Frost {
116 detail: "deserialized key package identifier mismatch".to_owned(),
117 });
118 }
119 let (nonces, commitments) = frost::round1::commit(key_package.signing_share(), rng);
120 signing_nonces.insert(frost_identifier, nonces);
121 signing_commitments.insert(frost_identifier, commitments);
122 key_packages.insert(frost_identifier, key_package);
123 }
124
125 let signing_package = frost::SigningPackage::new(signing_commitments, message);
126 let mut signature_shares = BTreeMap::new();
127 for (identifier, key_package) in &key_packages {
128 let nonces = &signing_nonces[identifier];
129 let share = frost_sign_share(&signing_package, nonces, key_package)?;
130 signature_shares.insert(*identifier, share);
131 }
132
133 let sig = frost_aggregate_signature(&signing_package, &signature_shares, &public)?;
134 let signature = serialize_frost(&sig)?;
135
136 verify_root_signature(&public_key_package.root_public_key, message, &signature)?;
137
138 Ok(RootSignature {
139 signature,
140 signer_ids,
141 })
142}
143
144pub fn verify_root_signature(
146 root_public_key: &[u8],
147 message: &[u8],
148 signature: &[u8],
149) -> Result<()> {
150 let verifying_key: frost::VerifyingKey = deserialize_frost(root_public_key)?;
151 let signature: frost::Signature = deserialize_frost(signature)?;
152 verifying_key
153 .verify(message, &signature)
154 .map_err(signature_rejected)
155}
156
157fn signature_rejected(error: frost::Error) -> RootError {
158 RootError::SignatureRejected {
159 reason: error.to_string(),
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct RootSigningCommitment {
181 pub frost_identifier: u16,
183 pub commitments: Vec<u8>,
185}
186
187#[derive(PartialEq, Eq, Serialize, Deserialize)]
195pub struct RootSigningNonces {
196 pub frost_identifier: u16,
198 pub ceremony_id: String,
201 pub artifact_hash: Hash256,
207 pub commitment_hash: Hash256,
211 #[serde(deserialize_with = "deserialize_zeroizing_bytes")]
213 pub nonces: Zeroizing<Vec<u8>>,
214}
215
216impl fmt::Debug for RootSigningNonces {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 formatter
219 .debug_struct("RootSigningNonces")
220 .field("frost_identifier", &self.frost_identifier)
221 .field("ceremony_id", &self.ceremony_id)
222 .field("artifact_hash", &self.artifact_hash)
223 .field("commitment_hash", &self.commitment_hash)
224 .field("nonces", &"[REDACTED]")
225 .finish()
226 }
227}
228
229fn commitment_hash(commitment_bytes: &[u8]) -> Hash256 {
232 Hash256::digest(commitment_bytes)
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238pub struct RootSigningPackage {
239 pub signing_package: Vec<u8>,
241 pub signer_ids: Vec<u16>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct RootSignatureShareOutput {
249 pub frost_identifier: u16,
251 pub signature_share: Vec<u8>,
253}
254
255fn ensure_rostered(config: &GenesisCeremonyConfig, identifier: u16, role: &str) -> Result<()> {
256 if config.certifier_by_identifier(identifier).is_none() {
257 return Err(RootError::InvalidConfig {
258 reason: format!("{role} {identifier} is not rostered"),
259 });
260 }
261 Ok(())
262}
263
264pub(crate) fn validate_root_signer_ids(
265 config: &GenesisCeremonyConfig,
266 signer_ids: &[u16],
267) -> Result<()> {
268 if signer_ids.len() != usize::from(config.threshold) {
269 return Err(RootError::InvalidConfig {
270 reason: format!(
271 "signing selection must contain exactly {} signers",
272 config.threshold
273 ),
274 });
275 }
276 let mut selection = BTreeSet::new();
277 for identifier in signer_ids {
278 if config.certifier_by_identifier(*identifier).is_none() {
279 return Err(RootError::InvalidConfig {
280 reason: format!("signer {identifier} is not rostered"),
281 });
282 }
283 if !selection.insert(*identifier) {
284 return Err(RootError::InvalidConfig {
285 reason: format!("duplicate signer {identifier}"),
286 });
287 }
288 }
289 config.validate_signing_selection(&selection)
290}
291
292pub fn sign_commit<R>(
300 config: &GenesisCeremonyConfig,
301 key_package: &RootKeyPackage,
302 artifact: &[u8],
303 rng: &mut R,
304) -> Result<(RootSigningCommitment, RootSigningNonces)>
305where
306 R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
307{
308 config.validate()?;
309 ensure_rostered(config, key_package.frost_identifier, "signer")?;
310 let parsed: frost::keys::KeyPackage = deserialize_frost(key_package.key_package.as_slice())?;
311 let (nonces, commitments) = frost::round1::commit(parsed.signing_share(), rng);
312 let commitment_bytes = serialize_frost(&commitments)?;
313 let commitment = RootSigningCommitment {
314 frost_identifier: key_package.frost_identifier,
315 commitments: commitment_bytes.clone(),
316 };
317 let signing_nonces = RootSigningNonces {
318 frost_identifier: key_package.frost_identifier,
319 ceremony_id: config.ceremony_id.clone(),
320 artifact_hash: Hash256::digest(artifact),
321 commitment_hash: commitment_hash(commitment_bytes.as_slice()),
322 nonces: serialize_frost_secret(&nonces)?,
323 };
324 Ok((commitment, signing_nonces))
325}
326
327pub fn build_signing_package(
330 config: &GenesisCeremonyConfig,
331 commitments: BTreeMap<u16, Vec<u8>>,
332 message: &[u8],
333) -> Result<RootSigningPackage> {
334 config.validate()?;
335 if commitments.len() < usize::from(config.threshold) {
336 return Err(RootError::ThresholdNotMet {
337 required: config.threshold,
338 supplied: u16::try_from(commitments.len()).unwrap_or(u16::MAX),
339 });
340 }
341 let signer_ids: Vec<u16> = commitments.keys().copied().collect();
345 validate_root_signer_ids(config, signer_ids.as_slice())?;
346 let mut parsed = BTreeMap::new();
347 for (identifier, bytes) in commitments {
348 ensure_rostered(config, identifier, "signer")?;
349 let frost_id = crate::dkg::frost_identifier(identifier)?;
350 let commitment: frost::round1::SigningCommitments = deserialize_frost(bytes.as_slice())?;
351 parsed.insert(frost_id, commitment);
352 }
353 let signing_package = frost::SigningPackage::new(parsed, message);
354 Ok(RootSigningPackage {
355 signing_package: serialize_frost(&signing_package)?,
356 signer_ids,
357 })
358}
359
360pub fn sign_share(
373 config: &GenesisCeremonyConfig,
374 key_package: &RootKeyPackage,
375 nonces: &RootSigningNonces,
376 signing_package: &RootSigningPackage,
377 message: &[u8],
378) -> Result<RootSignatureShareOutput> {
379 config.validate()?;
380 ensure_rostered(config, key_package.frost_identifier, "signer")?;
381 if nonces.frost_identifier != key_package.frost_identifier {
382 let nonce_id = nonces.frost_identifier;
383 let key_id = key_package.frost_identifier;
384 return Err(RootError::Frost {
385 detail: format!("nonces id {nonce_id} mismatches key {key_id}"),
386 });
387 }
388 if nonces.ceremony_id != config.ceremony_id {
389 return Err(RootError::Frost {
390 detail: "nonces were generated for a different ceremony".to_owned(),
391 });
392 }
393 if Hash256::digest(message) != nonces.artifact_hash {
396 return Err(RootError::Frost {
397 detail: "nonces are bound to a different artifact than the message".to_owned(),
398 });
399 }
400 validate_root_signer_ids(config, signing_package.signer_ids.as_slice())?;
405 let parsed_key: frost::keys::KeyPackage =
406 deserialize_frost(key_package.key_package.as_slice())?;
407 let parsed_nonces: frost::round1::SigningNonces = deserialize_frost(nonces.nonces.as_slice())?;
408 let parsed_package: frost::SigningPackage =
409 deserialize_frost(signing_package.signing_package.as_slice())?;
410 let mut commitments = BTreeMap::new();
414 for identifier in &signing_package.signer_ids {
415 let signer_frost_id = crate::dkg::frost_identifier(*identifier)?;
416 let commitment = parsed_package
417 .signing_commitment(&signer_frost_id)
418 .ok_or_else(|| RootError::Frost {
419 detail: format!("signing package is missing commitment for signer {identifier}"),
420 })?;
421 commitments.insert(signer_frost_id, commitment);
422 }
423 let frost_id = crate::dkg::frost_identifier(key_package.frost_identifier)?;
425 let package_commitment = commitments.get(&frost_id).ok_or_else(|| RootError::Frost {
426 detail: "signer is not in the signing package's signer set".to_owned(),
427 })?;
428 if commitment_hash(serialize_frost(package_commitment)?.as_slice()) != nonces.commitment_hash {
429 return Err(RootError::Frost {
430 detail: "nonces are not bound to this signing package's commitment".to_owned(),
431 });
432 }
433 let rebuilt_package = frost::SigningPackage::new(commitments, message);
434 let share =
435 frost::round2::sign(&rebuilt_package, &parsed_nonces, &parsed_key).map_err(frost_error)?;
436 Ok(RootSignatureShareOutput {
437 frost_identifier: key_package.frost_identifier,
438 signature_share: serialize_frost(&share)?,
439 })
440}
441
442pub fn aggregate_signature(
445 config: &GenesisCeremonyConfig,
446 public_key_package: &RootPublicKeyPackage,
447 signing_package: &[u8],
448 shares: BTreeMap<u16, Vec<u8>>,
449 message: &[u8],
450) -> Result<RootSignature> {
451 config.validate()?;
452 if shares.len() < usize::from(config.threshold) {
453 return Err(RootError::ThresholdNotMet {
454 required: config.threshold,
455 supplied: u16::try_from(shares.len()).unwrap_or(u16::MAX),
456 });
457 }
458 let signer_ids: Vec<u16> = shares.keys().copied().collect();
459 validate_root_signer_ids(config, signer_ids.as_slice())?;
460 let public = deserialize_frost(public_key_package.public_key_package.as_slice())?;
461 let parsed_package: frost::SigningPackage = deserialize_frost(signing_package)?;
462 let mut parsed_shares = BTreeMap::new();
463 for (identifier, bytes) in shares {
464 ensure_rostered(config, identifier, "signer")?;
465 let frost_id = crate::dkg::frost_identifier(identifier)?;
466 let share: frost::round2::SignatureShare = deserialize_frost(bytes.as_slice())?;
467 parsed_shares.insert(frost_id, share);
468 }
469 let aggregated =
470 frost::aggregate(&parsed_package, &parsed_shares, &public).map_err(frost_error)?;
471 let signature = serialize_frost(&aggregated)?;
472 verify_root_signature(&public_key_package.root_public_key, message, &signature)?;
473 Ok(RootSignature {
474 signature,
475 signer_ids,
476 })
477}
478
479#[cfg(test)]
480mod tests {
481 use exo_core::{Did, Hash256, PublicKey, Timestamp};
482 use rand::{SeedableRng, rngs::StdRng};
483 use serde::Serialize;
484
485 use super::*;
486 use crate::CertifierContact;
487
488 #[derive(Serialize)]
489 struct LegacySigningNonces<'a> {
490 frost_identifier: u16,
491 ceremony_id: &'a String,
492 artifact_hash: Hash256,
493 commitment_hash: Hash256,
494 nonces: &'a Vec<u8>,
495 }
496
497 fn cbor_bytes(value: &impl Serialize) -> Vec<u8> {
498 let mut bytes = Vec::new();
499 ciborium::into_writer(value, &mut bytes).expect("CBOR encoding");
500 bytes
501 }
502
503 fn assert_zeroizing_carrier<T: zeroize::Zeroize + zeroize::ZeroizeOnDrop>(_: &T) {}
504
505 #[test]
506 fn secret_signing_nonce_bytes_zeroize_on_drop() {
507 let nonces = RootSigningNonces {
508 frost_identifier: 7,
509 ceremony_id: "root-secret-zeroize-v1".to_owned(),
510 artifact_hash: Hash256::digest(b"artifact"),
511 commitment_hash: Hash256::digest(b"commitment"),
512 nonces: Zeroizing::new(vec![0xde, 0xad, 0xfa, 0xce]),
513 };
514
515 assert_zeroizing_carrier(&nonces.nonces);
516 }
517
518 #[test]
519 fn secret_signing_nonce_debug_is_redacted_and_wire_compatible() {
520 let secret = vec![0xde, 0xad, 0xfa, 0xce];
521 let ceremony_id = "root-secret-wire-v1".to_owned();
522 let artifact_hash = Hash256::digest(b"artifact");
523 let commitment_hash = Hash256::digest(b"commitment");
524 let nonces = RootSigningNonces {
525 frost_identifier: 7,
526 ceremony_id: ceremony_id.clone(),
527 artifact_hash,
528 commitment_hash,
529 nonces: Zeroizing::new(secret.clone()),
530 };
531 let rendered = format!("{nonces:?}");
532 assert!(
533 rendered.contains("[REDACTED]"),
534 "signing nonce debug output must visibly redact: {rendered}"
535 );
536 assert!(
537 !rendered.contains(&format!("{secret:?}")),
538 "signing nonce debug output exposed fixture bytes: {rendered}"
539 );
540
541 let legacy = LegacySigningNonces {
542 frost_identifier: 7,
543 ceremony_id: &ceremony_id,
544 artifact_hash,
545 commitment_hash,
546 nonces: &secret,
547 };
548 assert_eq!(
549 serde_json::to_vec(&nonces).expect("nonce JSON"),
550 serde_json::to_vec(&legacy).expect("legacy nonce JSON")
551 );
552 assert_eq!(cbor_bytes(&nonces), cbor_bytes(&legacy));
553 }
554
555 #[test]
556 fn secret_signing_nonce_deserializer_rejects_partial_json_and_cbor() {
557 let malformed = serde_json::json!({
558 "frost_identifier": 7,
559 "ceremony_id": "root-secret-malformed-v1",
560 "artifact_hash": Hash256::digest(b"artifact"),
561 "commitment_hash": Hash256::digest(b"commitment"),
562 "nonces": [1, 2, "bad"]
563 });
564 let json = serde_json::to_vec(&malformed).expect("malformed JSON fixture");
565 assert!(serde_json::from_slice::<RootSigningNonces>(&json).is_err());
566 let encoded = cbor_bytes(&malformed);
567 assert!(ciborium::from_reader::<RootSigningNonces, _>(encoded.as_slice()).is_err());
568 }
569
570 #[test]
571 fn public_signing_artifacts_keep_debug_and_wire_round_trips() {
572 let commitment = RootSigningCommitment {
573 frost_identifier: 7,
574 commitments: vec![1, 2, 3, 4],
575 };
576 let share = RootSignatureShareOutput {
577 frost_identifier: 7,
578 signature_share: vec![5, 6, 7, 8],
579 };
580 let commitment_json = serde_json::to_vec(&commitment).expect("commitment JSON");
581 let share_json = serde_json::to_vec(&share).expect("share JSON");
582 assert!(format!("{commitment:?}").contains("commitments"));
583 assert!(format!("{share:?}").contains("signature_share"));
584 assert_eq!(
585 serde_json::from_slice::<RootSigningCommitment>(&commitment_json)
586 .expect("commitment JSON round trip"),
587 commitment
588 );
589 assert_eq!(
590 serde_json::from_slice::<RootSignatureShareOutput>(&share_json)
591 .expect("share JSON round trip"),
592 share
593 );
594 assert_eq!(
595 ciborium::from_reader::<RootSigningCommitment, _>(cbor_bytes(&commitment).as_slice())
596 .expect("commitment CBOR round trip"),
597 commitment
598 );
599 assert_eq!(
600 ciborium::from_reader::<RootSignatureShareOutput, _>(cbor_bytes(&share).as_slice())
601 .expect("share CBOR round trip"),
602 share
603 );
604 }
605
606 fn test_config() -> GenesisCeremonyConfig {
607 let certifiers = (1..=13)
608 .map(|identifier| {
609 let byte = u8::try_from(identifier).expect("identifier fits");
610 CertifierContact {
611 did: Did::new(&format!("did:exo:signing-unit-{identifier:02}"))
612 .expect("valid did"),
613 frost_identifier: identifier,
614 signing_public_key: PublicKey::from_bytes([byte; 32]),
615 transport_public_key: [byte; 32],
616 }
617 })
618 .collect();
619 GenesisCeremonyConfig {
620 ceremony_id: "unit-root".into(),
621 network_id: "unit-net".into(),
622 repo_commit: "d8927686a34bdc28ba36d53938f665685d2c4c04".into(),
623 constitution_hash: Hash256::digest(b"constitution"),
624 threshold: 7,
625 max_signers: 13,
626 created_at: Timestamp::new(1, 0),
627 certifiers,
628 signing_set: (1..=7).collect(),
629 }
630 }
631
632 #[test]
633 fn frost_error_conversion_is_diagnostic() {
634 let error = frost::Identifier::try_from(0).expect_err("zero identifier");
635 let converted = frost_error(error);
636 assert!(converted.to_string().contains("frost operation failed"));
637 }
638
639 #[test]
640 fn signature_rejection_conversion_is_diagnostic() {
641 let error = frost::Identifier::try_from(0).expect_err("zero identifier");
642 let converted = signature_rejected(error);
643 assert!(
644 converted
645 .to_string()
646 .contains("signature verification failed")
647 );
648 }
649
650 #[test]
651 fn threshold_sign_rejects_too_few_shares_before_public_deserialization() {
652 let config = test_config();
653 let public_key_package = RootPublicKeyPackage {
654 public_key_package: b"not a public package".to_vec(),
655 root_public_key: b"not a verifying key".to_vec(),
656 verifying_shares: BTreeMap::new(),
657 };
658 let mut rng = StdRng::seed_from_u64(7);
659 let error = threshold_sign(
660 &config,
661 &public_key_package,
662 BTreeMap::new(),
663 b"root artifact",
664 &mut rng,
665 )
666 .expect_err("empty signer set");
667 assert_eq!(
668 error,
669 RootError::ThresholdNotMet {
670 required: 7,
671 supplied: 0
672 }
673 );
674 }
675
676 #[test]
677 fn signer_id_validation_rejects_wrong_count_and_duplicates() {
678 let config = test_config();
679 let wrong_count = validate_root_signer_ids(&config, &[1, 2, 3, 4, 5, 6])
680 .expect_err("signer set must match threshold count");
681 assert!(wrong_count.to_string().contains("exactly 7 signers"));
682
683 let duplicate = validate_root_signer_ids(&config, &[1, 2, 3, 4, 5, 6, 6])
684 .expect_err("signer set must reject duplicates");
685 assert!(duplicate.to_string().contains("duplicate signer 6"));
686 }
687
688 #[test]
689 fn threshold_sign_covers_success_and_share_mismatch_paths() {
690 let config = test_config();
691 let mut rng = StdRng::seed_from_u64(71);
692 let mut dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
693 let selected: BTreeMap<u16, _> = (1..=7)
694 .map(|identifier| {
695 let share = dkg.key_packages.remove(&identifier).expect("key package");
696 (identifier, share)
697 })
698 .collect();
699 let message = b"unit root signing artifact";
700 let guarded = selected
701 .clone()
702 .into_iter()
703 .map(|(identifier, share)| (identifier, Zeroizing::new(share)))
704 .collect();
705 let mut guarded_rng = rng.clone();
706 let signature = threshold_sign(
707 &config,
708 &dkg.public_key_package,
709 selected,
710 message,
711 &mut rng,
712 )
713 .expect("signature");
714 let guarded_signature = threshold_sign_zeroizing(
715 &config,
716 &dkg.public_key_package,
717 guarded,
718 message,
719 &mut guarded_rng,
720 )
721 .expect("guarded signature");
722 assert_eq!(
723 guarded_signature, signature,
724 "guarded entrypoint preserves exact signature"
725 );
726
727 verify_root_signature(
728 &dkg.public_key_package.root_public_key,
729 message,
730 &signature.signature,
731 )
732 .expect("signature verifies");
733
734 let mut second_rng = StdRng::seed_from_u64(71);
735 let mut second_dkg = crate::run_complete_dkg(&config, &mut second_rng).expect("second dkg");
736 let mut mismatched: BTreeMap<u16, _> = (1..=7)
737 .map(|identifier| {
738 let share = second_dkg
739 .key_packages
740 .remove(&identifier)
741 .expect("key package");
742 (identifier, share)
743 })
744 .collect();
745 let mut share = mismatched.remove(&1).expect("share one");
746 share.frost_identifier = 2;
747 mismatched.insert(1, share);
748 let error = threshold_sign(
749 &config,
750 &second_dkg.public_key_package,
751 mismatched,
752 message,
753 &mut rng,
754 )
755 .expect_err("share identifier mismatch");
756 assert!(error.to_string().contains("mismatches key 1"));
757 }
758
759 #[test]
760 fn threshold_sign_rejects_non_declared_signer_set_before_public_deserialization() {
761 let config = test_config();
762 let public_key_package = RootPublicKeyPackage {
763 public_key_package: b"not a public package".to_vec(),
764 root_public_key: b"not a verifying key".to_vec(),
765 verifying_shares: BTreeMap::new(),
766 };
767 let shares = [1, 2, 3, 4, 5, 6, 9]
768 .into_iter()
769 .map(|identifier| {
770 (
771 identifier,
772 RootKeyPackage {
773 frost_identifier: identifier,
774 key_package: Vec::new(),
775 },
776 )
777 })
778 .collect();
779 let mut rng = StdRng::seed_from_u64(72);
780 let error = threshold_sign(&config, &public_key_package, shares, b"artifact", &mut rng)
781 .expect_err("non-declared signer set");
782 assert!(
783 error.to_string().contains("predeclared signing_set"),
784 "expected signer-set rejection before public package decoding, got: {error}"
785 );
786 }
787
788 #[test]
789 fn distributed_signing_matches_one_shot_and_verifies() {
790 let config = test_config();
791 let mut rng = StdRng::seed_from_u64(99);
792 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
793 let message = b"distributed root signing artifact";
794
795 let signers: Vec<(u16, _)> = dkg
797 .key_packages
798 .iter()
799 .take(7)
800 .map(|(id, kp)| (*id, kp))
801 .collect();
802
803 let mut commitments = BTreeMap::new();
805 let mut nonces = BTreeMap::new();
806 for (id, kp) in &signers {
807 let (commitment, signer_nonces) =
808 sign_commit(&config, kp, message, &mut rng).expect("commit");
809 nonces.insert(*id, signer_nonces);
810 commitments.insert(*id, commitment.commitments);
811 }
812
813 let package = build_signing_package(&config, commitments, message).expect("package");
815 assert_eq!(package.signer_ids.len(), 7);
816
817 let mut shares = BTreeMap::new();
820 for (id, kp) in &signers {
821 let share = sign_share(&config, kp, &nonces[id], &package, message).expect("share");
822 shares.insert(*id, share.signature_share);
823 }
824
825 let signature = aggregate_signature(
828 &config,
829 &dkg.public_key_package,
830 &package.signing_package,
831 shares,
832 message,
833 )
834 .expect("aggregate");
835 verify_root_signature(
836 &dkg.public_key_package.root_public_key,
837 message,
838 &signature.signature,
839 )
840 .expect("distributed signature verifies");
841 assert_eq!(signature.signer_ids.len(), 7);
842 }
843
844 #[test]
845 fn build_signing_package_rejects_sub_threshold_commitment_set() {
846 let config = test_config();
847 let mut rng = StdRng::seed_from_u64(100);
848 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
849 let mut commitments = BTreeMap::new();
850 for (id, kp) in dkg.key_packages.iter().take(3) {
851 let (commitment, _nonces) = sign_commit(&config, kp, b"msg", &mut rng).expect("commit");
852 commitments.insert(*id, commitment.commitments);
853 }
854 let error = build_signing_package(&config, commitments, b"msg")
855 .expect_err("sub-threshold commitments");
856 assert!(matches!(
857 error,
858 RootError::ThresholdNotMet {
859 required: 7,
860 supplied: 3
861 }
862 ));
863 }
864
865 #[test]
866 fn distributed_signing_rejects_unrostered_and_sub_threshold() {
867 let config = test_config();
868 let mut rng = StdRng::seed_from_u64(123);
869 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
870
871 let mut stranger_rng = StdRng::seed_from_u64(123);
873 let mut stranger_dkg =
874 crate::run_complete_dkg(&config, &mut stranger_rng).expect("stranger dkg");
875 let mut stranger = stranger_dkg
876 .key_packages
877 .remove(&1)
878 .expect("key package one");
879 stranger.frost_identifier = 99;
880 assert!(matches!(
881 sign_commit(&config, &stranger, b"msg", &mut rng).expect_err("unrostered commit"),
882 RootError::InvalidConfig { .. }
883 ));
884
885 let mut commitments = BTreeMap::new();
887 for (id, kp) in dkg.key_packages.iter().take(7) {
888 let (commitment, _nonces) = sign_commit(&config, kp, b"msg", &mut rng).expect("commit");
889 commitments.insert(*id, commitment.commitments);
890 }
891 let mut unrostered = commitments.clone();
892 let stolen = unrostered.remove(&1).expect("commitment one");
893 unrostered.insert(99, stolen);
894 assert!(matches!(
895 build_signing_package(&config, unrostered, b"msg").expect_err("unrostered commitment"),
896 RootError::InvalidConfig { .. }
897 ));
898
899 let package = build_signing_package(&config, commitments, b"msg").expect("package");
901 let (_commitment, commit_nonces) =
902 sign_commit(&config, &dkg.key_packages[&1], b"msg", &mut rng).expect("commit");
903 assert!(matches!(
904 sign_share(&config, &stranger, &commit_nonces, &package, b"msg")
905 .expect_err("unrostered share"),
906 RootError::InvalidConfig { .. }
907 ));
908
909 assert!(matches!(
911 aggregate_signature(
912 &config,
913 &dkg.public_key_package,
914 &package.signing_package,
915 BTreeMap::new(),
916 b"msg",
917 )
918 .expect_err("sub-threshold aggregate"),
919 RootError::ThresholdNotMet { required: 7, .. }
920 ));
921 }
922
923 #[test]
924 fn aggregate_signature_rejects_non_declared_signer_set_before_deserialization() {
925 let config = test_config();
926 let public_key_package = RootPublicKeyPackage {
927 public_key_package: b"not a public package".to_vec(),
928 root_public_key: b"not a verifying key".to_vec(),
929 verifying_shares: BTreeMap::new(),
930 };
931 let shares = [1, 2, 3, 4, 5, 6, 9]
932 .into_iter()
933 .map(|identifier| (identifier, vec![u8::try_from(identifier).expect("id fits")]))
934 .collect();
935 let error = aggregate_signature(
936 &config,
937 &public_key_package,
938 b"not a signing package",
939 shares,
940 b"artifact",
941 )
942 .expect_err("non-declared aggregate signer set");
943 assert!(
944 error.to_string().contains("predeclared signing_set"),
945 "expected signer-set rejection before signature artifacts decode, got: {error}"
946 );
947 }
948
949 #[test]
950 fn sign_share_rejects_nonces_bound_to_a_different_signer() {
951 let config = test_config();
955 let key_package = RootKeyPackage {
956 frost_identifier: 1,
957 key_package: Vec::new(),
958 };
959 let foreign_nonces = RootSigningNonces {
960 frost_identifier: 2,
961 ceremony_id: config.ceremony_id.clone(),
962 artifact_hash: Hash256::digest(b"artifact"),
963 commitment_hash: Hash256::digest(b"unrelated commitment"),
964 nonces: Vec::new().into(),
965 };
966 let empty_package = RootSigningPackage {
967 signing_package: Vec::new(),
968 signer_ids: Vec::new(),
969 };
970 let error = sign_share(
971 &config,
972 &key_package,
973 &foreign_nonces,
974 &empty_package,
975 b"artifact",
976 )
977 .expect_err("nonces bound to a different signer must be rejected");
978 assert!(error.to_string().contains("mismatches key 1"));
979 }
980
981 #[test]
982 fn sign_share_rejects_nonces_from_a_different_ceremony() {
983 let config = test_config();
986 let key_package = RootKeyPackage {
987 frost_identifier: 1,
988 key_package: Vec::new(),
989 };
990 let foreign_nonces = RootSigningNonces {
991 frost_identifier: 1,
992 ceremony_id: "some-other-ceremony".to_owned(),
993 artifact_hash: Hash256::digest(b"artifact"),
994 commitment_hash: Hash256::digest(b"unrelated commitment"),
995 nonces: Vec::new().into(),
996 };
997 let empty_package = RootSigningPackage {
998 signing_package: Vec::new(),
999 signer_ids: Vec::new(),
1000 };
1001 let error = sign_share(
1002 &config,
1003 &key_package,
1004 &foreign_nonces,
1005 &empty_package,
1006 b"artifact",
1007 )
1008 .expect_err("nonces from a different ceremony must be rejected");
1009 assert!(error.to_string().contains("different ceremony"));
1010 }
1011
1012 #[test]
1013 fn sign_share_rejects_nonces_bound_to_a_different_signing_instance() {
1014 let config = test_config();
1018 let mut rng = StdRng::seed_from_u64(404);
1019 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
1020 let mut commitments = BTreeMap::new();
1021 for (id, kp) in dkg.key_packages.iter().take(7) {
1022 let (commitment, _nonces) =
1023 sign_commit(&config, kp, b"artifact", &mut rng).expect("commit");
1024 commitments.insert(*id, commitment.commitments);
1025 }
1026 let package = build_signing_package(&config, commitments, b"artifact").expect("package");
1027 let (_other_commitment, stale_nonces) =
1030 sign_commit(&config, &dkg.key_packages[&1], b"artifact", &mut rng).expect("commit");
1031 let error = sign_share(
1032 &config,
1033 &dkg.key_packages[&1],
1034 &stale_nonces,
1035 &package,
1036 b"artifact",
1037 )
1038 .expect_err("nonces not bound to the package commitment must be rejected");
1039 assert!(
1040 error
1041 .to_string()
1042 .contains("not bound to this signing package")
1043 );
1044 }
1045
1046 #[test]
1047 fn sign_share_rejects_signer_absent_from_signing_package() {
1048 let config = test_config();
1051 let mut rng = StdRng::seed_from_u64(606);
1052 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
1053 let mut commitments = BTreeMap::new();
1054 for (id, kp) in dkg.key_packages.iter().take(7) {
1055 let (commitment, _nonces) =
1056 sign_commit(&config, kp, b"artifact", &mut rng).expect("commit");
1057 commitments.insert(*id, commitment.commitments);
1058 }
1059 let package = build_signing_package(&config, commitments, b"artifact").expect("package");
1060 let (_commitment8, nonces8) =
1061 sign_commit(&config, &dkg.key_packages[&8], b"artifact", &mut rng).expect("commit 8");
1062 let error = sign_share(
1063 &config,
1064 &dkg.key_packages[&8],
1065 &nonces8,
1066 &package,
1067 b"artifact",
1068 )
1069 .expect_err("a signer absent from the set must be rejected");
1070 assert!(
1071 error
1072 .to_string()
1073 .contains("signer is not in the signing package's signer set")
1074 );
1075 }
1076
1077 #[test]
1078 fn sign_share_rejects_non_canonical_signer_set() {
1079 let config = test_config();
1085 let mut rng = StdRng::seed_from_u64(909);
1086 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
1087 let mut commitments = BTreeMap::new();
1088 let mut signer_one_nonces = None;
1089 for (id, kp) in dkg.key_packages.iter().take(7) {
1090 let (commitment, nonces) =
1091 sign_commit(&config, kp, b"artifact", &mut rng).expect("commit");
1092 if *id == 1 {
1093 signer_one_nonces = Some(nonces);
1094 }
1095 commitments.insert(*id, commitment.commitments);
1096 }
1097 let mut package =
1098 build_signing_package(&config, commitments, b"artifact").expect("package");
1099 package.signer_ids = vec![1, 2, 3, 4, 5, 6, 9];
1101 let error = sign_share(
1102 &config,
1103 &dkg.key_packages[&1],
1104 &signer_one_nonces.expect("signer one nonces"),
1105 &package,
1106 b"artifact",
1107 )
1108 .expect_err("a non-canonical signer set must be rejected");
1109 assert!(
1110 error.to_string().contains("predeclared signing_set"),
1111 "expected a signing-selection-policy rejection, got: {error}"
1112 );
1113 }
1114
1115 #[test]
1116 fn sign_share_refuses_to_sign_a_second_different_message_with_one_nonce_set() {
1117 let config = test_config();
1122 let mut rng = StdRng::seed_from_u64(707);
1123 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
1124 let artifact = b"the one true root artifact";
1125 let mut commitments = BTreeMap::new();
1126 let mut nonces = BTreeMap::new();
1127 for (id, kp) in dkg.key_packages.iter().take(7) {
1128 let (commitment, signer_nonces) =
1129 sign_commit(&config, kp, artifact, &mut rng).expect("commit");
1130 commitments.insert(*id, commitment.commitments);
1131 nonces.insert(*id, signer_nonces);
1132 }
1133 let package = build_signing_package(&config, commitments, artifact).expect("package");
1134
1135 sign_share(
1137 &config,
1138 &dkg.key_packages[&1],
1139 &nonces[&1],
1140 &package,
1141 artifact,
1142 )
1143 .expect("first share over the bound artifact");
1144
1145 let error = sign_share(
1147 &config,
1148 &dkg.key_packages[&1],
1149 &nonces[&1],
1150 &package,
1151 b"a different message the coordinator wants signed",
1152 )
1153 .expect_err("a second, different message under the same nonces must be rejected");
1154 assert!(error.to_string().contains("bound to a different artifact"));
1155 }
1156
1157 #[test]
1158 fn sign_share_rejects_a_signing_package_missing_a_declared_signer() {
1159 let config = test_config();
1162 let mut rng = StdRng::seed_from_u64(808);
1163 let dkg = crate::run_complete_dkg(&config, &mut rng).expect("dkg");
1164 let mut commitments = BTreeMap::new();
1165 let mut signer_one_nonces = None;
1166 for (id, kp) in dkg.key_packages.iter().take(7) {
1167 let (commitment, nonces) =
1168 sign_commit(&config, kp, b"artifact", &mut rng).expect("commit");
1169 if *id == 1 {
1170 signer_one_nonces = Some(nonces);
1171 }
1172 commitments.insert(*id, commitment.commitments);
1173 }
1174 let mut package =
1175 build_signing_package(&config, commitments, b"artifact").expect("package");
1176 let parsed_package: frost::SigningPackage =
1177 deserialize_frost(package.signing_package.as_slice()).expect("parsed package");
1178 let mut embedded_commitments = BTreeMap::new();
1179 for id in 1..=6u16 {
1180 let signer_frost_id = crate::dkg::frost_identifier(id).expect("frost id");
1181 let commitment = parsed_package
1182 .signing_commitment(&signer_frost_id)
1183 .expect("commitment");
1184 embedded_commitments.insert(signer_frost_id, commitment);
1185 }
1186 let (commitment8, _nonces8) =
1187 sign_commit(&config, &dkg.key_packages[&8], b"artifact", &mut rng).expect("commit 8");
1188 embedded_commitments.insert(
1189 crate::dkg::frost_identifier(8).expect("frost id 8"),
1190 deserialize_frost(commitment8.commitments.as_slice()).expect("commitment 8"),
1191 );
1192 let malformed_package = frost::SigningPackage::new(embedded_commitments, b"artifact");
1193 package.signing_package = serialize_frost(&malformed_package).expect("serialize package");
1194 package.signer_ids = vec![1, 2, 3, 4, 5, 6, 7];
1195 let error = sign_share(
1196 &config,
1197 &dkg.key_packages[&1],
1198 &signer_one_nonces.expect("signer one nonces"),
1199 &package,
1200 b"artifact",
1201 )
1202 .expect_err("a signing package missing a declared signer must be rejected");
1203 assert!(
1204 error
1205 .to_string()
1206 .contains("missing commitment for signer 7")
1207 );
1208 }
1209}