//! BLS12-381 threshold signature scheme implementation. //! //! This module provides both the generic BLS12-381 threshold implementation and a macro to generate //! protocol-specific wrappers. //! //! Unlike multi-signature schemes, threshold signatures: //! - Use partial signatures that can be combined to form a threshold signature //! - Require a quorum of signatures to recover the full signature //! - Are **non-attributable**: partial signatures can be forged by holders of enough other partials //! //! For a fixed, securely generated sharing, threshold unforgeability implies that, except with //! negligible probability, a coalition producing a fresh certificate in the standard corruption and //! partial-signing model has a transcript containing exposed shares or valid responses for the exact //! subject covering a quorum of distinct participants. Those shares and responses suffice to compute //! valid partials for that quorum. This is not a public proof of knowledge or a guarantee about a //! later recipient. //! //! Certificate verification does not validate the partials supplied to assembly. Invalid partials //! can have cancelling interpolation errors, even with exactly a quorum of inputs. A valid //! certificate therefore authenticates quorum authorization without proving input validity or //! identifying which participants signed. #[cfg(feature = "mocks")] pub mod mocks; use crate::{ PublicKey, bls12381::primitives::{ group::Share, ops::{self, batch, threshold}, sharing::Sharing, variant::{PartialSignature, Variant}, }, certificate::{AssemblyError, Attestation, Namespace, Scheme, Signers, Subject, Verification}, }; #[cfg(not(feature = "std"))] use alloc::{collections::BTreeSet, vec::Vec}; use bytes::BufMut; use commonware_codec::{Buf, Error, FixedSize, Read, ReadExt, Write, types::lazy::Lazy}; use commonware_parallel::Strategy; use commonware_utils::{ Faults, Participant, iter::NonEmpty, non_empty, ordered::{Quorum, Set}, }; use core::fmt::Debug; use rand_core::CryptoRng; #[cfg(feature = "std")] use std::collections::BTreeSet; /// Generic BLS12-381 threshold signature implementation. /// /// This enum contains the core cryptographic operations without protocol-specific /// context types. It can be reused across different protocols (simplex, aggregation, etc.) /// by wrapping it with protocol-specific trait implementations via the macro. /// /// An instance plays one of the following roles: a signer (with its share), /// a verifier (with evaluated public polynomial), or an external verifier that /// only checks recovered certificates. #[derive(Clone, Debug)] pub enum Generic { Signer { /// Participants in the committee. participants: Set

, /// The public polynomial, used for the group identity, and partial signatures. polynomial: Sharing, /// Local share used to generate partial signatures. share: Share, /// Pre-computed namespace(s) for this subject type. namespace: N, }, Verifier { /// Participants in the committee. participants: Set

, /// The public polynomial, used for the group identity, and partial signatures. polynomial: Sharing, /// Pre-computed namespace(s) for this subject type. namespace: N, }, CertificateVerifier { /// Public identity of the committee (constant across reshares). identity: V::Public, /// Pre-computed namespace(s) for this subject type. namespace: N, }, } impl Generic { /// Constructs a signer instance with a private share and evaluated public polynomial /// compatible with the specified fault model. /// /// The participant identity keys are used for committee ordering and indexing. /// The polynomial can be evaluated to obtain public verification keys for partial /// signatures produced by committee members. /// /// * `namespace`: base namespace for domain separation /// * `participants`: ordered set of participant identity keys /// * `polynomial`: public polynomial for threshold verification /// * `share`: local threshold share for signing /// /// Returns `None` if the share's public key does not match any participant. /// /// # Panics /// /// Panics if the polynomial's participant count or degree does not match the /// committee and fault model, or if the share index does not identify a committee /// participant. pub fn signer( namespace: &[u8], participants: Set

, polynomial: Sharing, share: Share, ) -> Option { assert_eq!( polynomial.total().get() as usize, participants.len(), "polynomial total must equal participant len" ); assert_eq!( polynomial.required(), participants.quorum::(), "polynomial threshold must equal quorum" ); #[cfg(feature = "std")] polynomial.precompute_partial_publics(); let partial_public = polynomial .partial_public(share.index) .expect("share index must match participant indices"); if partial_public == share.public::() { Some(Self::Signer { participants, polynomial, share, namespace: N::derive(namespace), }) } else { None } } /// Produces a verifier for the specified fault model that can authenticate signatures /// but does not hold signing state. /// /// The participant identity keys are used for committee ordering and indexing. /// The polynomial can be evaluated to obtain public verification keys for partial /// signatures produced by committee members. /// /// * `namespace`: base namespace for domain separation /// * `participants`: ordered set of participant identity keys /// * `polynomial`: public polynomial for threshold verification /// /// # Panics /// /// Panics if the polynomial's participant count or degree does not match the committee /// and fault model. pub fn verifier( namespace: &[u8], participants: Set

, polynomial: Sharing, ) -> Self { assert_eq!( polynomial.total().get() as usize, participants.len(), "polynomial total must equal participant len" ); assert_eq!( polynomial.required(), participants.quorum::(), "polynomial threshold must equal quorum" ); #[cfg(feature = "std")] polynomial.precompute_partial_publics(); Self::Verifier { participants, polynomial, namespace: N::derive(namespace), } } /// Creates a verifier that only checks recovered certificates. /// /// This lightweight verifier can authenticate recovered threshold certificates but cannot /// verify individual signatures or partial signatures. /// /// * `namespace`: base namespace for domain separation /// * `identity`: public identity of the committee (constant across reshares) pub fn certificate_verifier(namespace: &[u8], identity: V::Public) -> Self { Self::CertificateVerifier { identity, namespace: N::derive(namespace), } } /// Returns the ordered set of participant public identity keys in the committee. pub fn participants(&self) -> &Set

{ match self { Self::Signer { participants, .. } => participants, Self::Verifier { participants, .. } => participants, _ => panic!("can only be called for signer and verifier"), } } /// Returns the public identity of the committee (constant across reshares). pub fn identity(&self) -> &V::Public { match self { Self::Signer { polynomial, .. } => polynomial.public(), Self::Verifier { polynomial, .. } => polynomial.public(), Self::CertificateVerifier { identity, .. } => identity, } } /// Returns the local share if this instance can generate partial signatures. pub const fn share(&self) -> Option<&Share> { match self { Self::Signer { share, .. } => Some(share), _ => None, } } /// Returns the evaluated public polynomial for validating partial signatures produced by committee members. fn polynomial(&self) -> &Sharing { match self { Self::Signer { polynomial, .. } => polynomial, Self::Verifier { polynomial, .. } => polynomial, _ => panic!("can only be called for signer and verifier"), } } /// Returns the pre-computed namespace. const fn namespace(&self) -> &N { match self { Self::Signer { namespace, .. } => namespace, Self::Verifier { namespace, .. } => namespace, Self::CertificateVerifier { namespace, .. } => namespace, } } /// Returns the index this instance signs at, or `None` if it cannot sign. pub const fn signer_index(&self) -> Option { match self { Self::Signer { share, .. } => Some(share.index), _ => None, } } /// Signs a subject and returns the attestation. pub fn sign(&self, subject: impl Subject) -> Option> where S: Scheme, { let share = self.share()?; let signature = threshold::sign_message::( share, subject.namespace(self.namespace()), &subject.message(), ) .value; Some(Attestation { signer: share.index, signature: signature.into(), }) } /// Verifies a single attestation from a signer. pub fn verify_attestation( &self, subject: impl Subject, attestation: &Attestation, ) -> bool where S: Scheme, { let Ok(evaluated) = self.polynomial().partial_public(attestation.signer) else { return false; }; let Some(signature) = attestation.signature.get() else { return false; }; ops::verify_message::( &evaluated, subject.namespace(self.namespace()), &subject.message(), signature, ) .is_ok() } /// Batch-verifies attestations and returns verified attestations and invalid signers. pub fn verify_attestations( &self, rng: &mut R, subject: impl Subject, attestations: I, strategy: &impl Strategy, ) -> Verification where S: Scheme, R: CryptoRng, I: IntoIterator>, I::IntoIter: Send, { let (partials, failures) = strategy.map_partition_collect_vec(attestations.into_iter(), |attestation| { let index = attestation.signer; let partial = attestation .signature .get() .map(|&value| PartialSignature:: { index, value }); (index, partial) }); let mut invalid: BTreeSet<_> = failures.into_iter().collect(); let polynomial = self.polynomial(); if let Some(partials) = NonEmpty::try_new(partials.iter()) && let Err(errs) = threshold::batch_verify_same_message::<_, V, _>( rng, polynomial, subject.namespace(self.namespace()), &subject.message(), partials, strategy, ) { for partial in errs { invalid.insert(partial.index); } } let verified = partials .into_iter() .filter(|partial| !invalid.contains(&partial.index)) .map(|partial| Attestation { signer: partial.index, signature: partial.value.into(), }) .collect(); Verification::new(verified, invalid.into_iter().collect()) } /// Assembles a candidate certificate from a non-empty collection of attestations. /// /// Checks encoding, signer bounds, uniqueness, and quorum, but does not verify partials. /// Call [`Self::verify_certificate`] before accepting a result assembled from unverified inputs. pub fn assemble( &self, attestations: NonEmpty, strategy: &impl Strategy, ) -> Result, AssemblyError> where S: Scheme, I: Iterator> + Send, { // Decode each partial signature under its attestation's claimed signer. let partials = strategy.try_map_collect_vec(attestations, |attestation| { let index = attestation.signer; attestation .signature .get() .map(|&value| PartialSignature:: { index, value }) .ok_or(AssemblyError::MalformedSignature(index)) })?; // Enforce signer bounds, uniqueness, and quorum before recovering the certificate. let quorum = self.polynomial(); Signers::try_from((quorum, partials.iter().map(|partial| partial.index)))?; // Recover the certificate only after these structural preconditions hold. let signature = threshold::recover(quorum, partials.iter(), strategy) .map_err(|_| AssemblyError::RecoveryFailed)?; Ok(Certificate::new(signature)) } /// Verifies a certificate. pub fn verify_certificate( &self, _rng: &mut R, subject: impl Subject, certificate: &Certificate, ) -> bool where R: CryptoRng, { let Some(signature) = certificate.get() else { return false; }; ops::verify_message::( self.identity(), subject.namespace(self.namespace()), &subject.message(), signature, ) .is_ok() } /// Verifies multiple certificates in a batch. pub fn verify_certificates<'a, R, T, I>( &self, rng: &mut R, certificates: NonEmpty, strategy: &impl Strategy, ) -> bool where R: CryptoRng, T: Subject, I: Iterator)>, { let mut entries: Vec<_> = Vec::new(); for (subject, certificate) in certificates { let Some(signature) = certificate.get() else { return false; }; let namespace = subject.namespace(self.namespace()); let message = subject.message(); entries.push((namespace, message, *signature)); } batch::verify_same_signer::<_, V, _>( rng, self.identity(), non_empty![@entries .iter() .map(|(ns, msg, sig)| (*ns, msg.as_ref(), *sig))], strategy, ) .is_ok() } pub const fn is_attributable() -> bool { false } pub const fn is_batchable() -> bool { true } pub const fn certificate_codec_config(&self) {} pub const fn certificate_codec_config_unbounded() {} } /// Certificate for BLS12-381 threshold signatures. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Certificate { /// The recovered threshold signature. pub signature: Lazy, } impl Certificate { /// Creates a new certificate from a recovered signature. pub fn new(signature: V::Signature) -> Self { Self { signature: Lazy::from(signature), } } /// Attempts to get the decoded signature. /// /// Returns `None` if the signature fails to decode. // `Lazy::get` is only `const` under some feature combinations, so this // cannot be `const` without breaking the default build. #[allow(clippy::missing_const_for_fn)] pub fn get(&self) -> Option<&V::Signature> { self.signature.get() } } impl Write for Certificate { fn write(&self, writer: &mut impl BufMut) { self.signature.write(writer); } } impl Read for Certificate { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let signature = Lazy::::read(reader)?; Ok(Self { signature }) } } impl FixedSize for Certificate { const SIZE: usize = V::Signature::SIZE; } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Certificate where V::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self { signature: Lazy::from(u.arbitrary::()?), }) } } /// Generates a BLS12-381 threshold signing scheme wrapper for a specific protocol. /// /// This macro creates a complete wrapper struct with constructors, `Scheme` trait /// implementation, and a `fixture` function for testing. /// /// # Parameters /// /// - `$subject`: The subject type used as `Scheme::Subject<'a, D>`. Use `'a` and `D` /// in the subject type to bind to the GAT lifetime and digest type parameters. /// /// - `$namespace`: The namespace type that implements [`Namespace`]. /// This type pre-computes and stores any protocol-specific namespace bytes derived from /// a base namespace. The scheme calls `$namespace::derive(base)` at construction time /// to create the namespace, then passes it to `Subject::namespace()` during signing /// and verification. For simple protocols with only a base namespace, `Vec` can be used directly. /// For protocols with multiple message types, a custom struct can pre-compute all variants. /// /// - `$faults`: The [`Faults`] implementation used to compute certificate quorums. /// /// # Example /// ```ignore /// // For non-generic subject types with a single namespace: /// impl_certificate_bls12381_threshold!(MySubject, Vec, commonware_utils::N3f1); /// /// // For protocols with generic subject types: /// impl_certificate_bls12381_threshold!( /// Subject<'a, D>, /// Namespace, /// commonware_utils::N3f1, /// ); /// ``` #[macro_export] macro_rules! impl_certificate_bls12381_threshold { ($subject:ty, $namespace:ty, $faults:ty) => { /// Generates a test fixture with Ed25519 identities and BLS12-381 threshold schemes. /// /// Returns a [`commonware_cryptography::certificate::mocks::Fixture`] whose keys and /// scheme instances share a consistent ordering. #[cfg(feature = "mocks")] #[allow(dead_code)] pub fn fixture( rng: &mut R, namespace: &[u8], n: u32, ) -> $crate::certificate::mocks::Fixture> where V: $crate::bls12381::primitives::variant::Variant, R: rand_core::CryptoRng, { $crate::bls12381::certificate::threshold::mocks::fixture::<_, V, _>( rng, namespace, n, Scheme::signer, Scheme::verifier, ) } /// BLS12-381 threshold signature scheme wrapper. #[derive(Clone, Debug)] pub struct Scheme< P: $crate::PublicKey, V: $crate::bls12381::primitives::variant::Variant, > { /// Verification state, plus the signing material of a signer. generic: $crate::bls12381::certificate::threshold::Generic, /// Index of the local identity key for an instance built with [`Self::member`]. member: Option, } impl< P: $crate::PublicKey, V: $crate::bls12381::primitives::variant::Variant, > Scheme { /// Creates a new signer instance with a private share and evaluated public polynomial /// compatible with the configured fault model. pub fn signer( namespace: &[u8], participants: commonware_utils::ordered::Set

, polynomial: $crate::bls12381::primitives::sharing::Sharing, share: $crate::bls12381::primitives::group::Share, ) -> Option { Some(Self { generic: $crate::bls12381::certificate::threshold::Generic::signer::<$faults>( namespace, participants, polynomial, share, )?, member: None, }) } /// Creates a verifier for the configured fault model that can authenticate partial /// signatures. /// /// A participant without signing material should use [`Self::member`] instead, so the /// scheme reports its membership. pub fn verifier( namespace: &[u8], participants: commonware_utils::ordered::Set

, polynomial: $crate::bls12381::primitives::sharing::Sharing, ) -> Self { Self { generic: $crate::bls12381::certificate::threshold::Generic::verifier::<$faults>( namespace, participants, polynomial, ), member: None, } } /// Creates a committee member that verifies like [`Self::verifier`] but also knows /// its own place in the participant set. /// /// The instance cannot sign, and [`Scheme::me`] returns [`Membership::Member`] at /// the position of `public_key` in the participant set. Returns `None` if /// `public_key` is not a participant. /// /// # Panics /// /// Panics if `public_key` is a participant and the polynomial's participant count or /// degree does not match the committee and the configured fault model. /// /// [`Scheme::me`]: commonware_cryptography::certificate::Scheme::me /// [`Membership::Member`]: commonware_cryptography::certificate::Membership::Member pub fn member( namespace: &[u8], participants: commonware_utils::ordered::Set

, polynomial: $crate::bls12381::primitives::sharing::Sharing, public_key: &P, ) -> Option { let member = commonware_utils::ordered::Quorum::index(&participants, public_key)?; Some(Self { generic: $crate::bls12381::certificate::threshold::Generic::verifier::<$faults>( namespace, participants, polynomial, ), member: Some(member), }) } /// Creates a lightweight verifier that only checks recovered certificates. pub fn certificate_verifier(namespace: &[u8], identity: V::Public) -> Self { Self { generic: $crate::bls12381::certificate::threshold::Generic::certificate_verifier( namespace, identity, ), member: None, } } /// Returns the public identity of the committee (constant across reshares). pub fn identity(&self) -> &V::Public { self.generic.identity() } /// Returns the local share if this instance can generate partial signatures. pub const fn share(&self) -> Option<&$crate::bls12381::primitives::group::Share> { self.generic.share() } } impl< P: $crate::PublicKey, V: $crate::bls12381::primitives::variant::Variant, > $crate::certificate::Verifier for Scheme { type Subject<'a, D: $crate::Digest> = $subject; type Faults = $faults; type PublicKey = P; type Certificate = $crate::bls12381::certificate::threshold::Certificate; fn verify_certificate( &self, rng: &mut R, subject: Self::Subject<'_, D>, certificate: &Self::Certificate, _strategy: &impl commonware_parallel::Strategy, ) -> bool where R: rand_core::CryptoRng, D: $crate::Digest, { self.generic .verify_certificate(rng, subject, certificate) } fn verify_certificates<'a, R, D, I>( &self, rng: &mut R, certificates: commonware_utils::iter::NonEmpty, strategy: &impl commonware_parallel::Strategy, ) -> bool where R: rand_core::CryptoRng, D: $crate::Digest, I: Iterator, &'a Self::Certificate)>, { self.generic .verify_certificates(rng, certificates, strategy) } fn is_batchable() -> bool { $crate::bls12381::certificate::threshold::Generic::::is_batchable() } fn max_certificate_size(_participants: u32) -> usize { ::SIZE } fn certificate_codec_config( &self, ) -> ::Cfg { self.generic.certificate_codec_config() } fn certificate_codec_config_unbounded( ) -> ::Cfg { $crate::bls12381::certificate::threshold::Generic::::certificate_codec_config_unbounded() } } impl< P: $crate::PublicKey, V: $crate::bls12381::primitives::variant::Variant, > $crate::certificate::Scheme for Scheme { type Signature = V::Signature; fn me(&self) -> Option<$crate::certificate::Membership> { self.generic .signer_index() .map($crate::certificate::Membership::Signer) .or(self.member.map($crate::certificate::Membership::Member)) } fn participants(&self) -> &commonware_utils::ordered::Set { self.generic.participants() } fn sign( &self, subject: Self::Subject<'_, D>, ) -> Option<$crate::certificate::Attestation> { self.generic.sign(subject) } fn verify_attestation( &self, _rng: &mut R, subject: Self::Subject<'_, D>, attestation: &$crate::certificate::Attestation, _strategy: &impl commonware_parallel::Strategy, ) -> bool where R: rand_core::CryptoRng, D: $crate::Digest, { self.generic .verify_attestation(subject, attestation) } fn verify_attestations( &self, rng: &mut R, subject: Self::Subject<'_, D>, attestations: I, strategy: &impl commonware_parallel::Strategy, ) -> $crate::certificate::Verification where R: rand_core::CryptoRng, D: $crate::Digest, I: IntoIterator>, I::IntoIter: Send { self.generic .verify_attestations(rng, subject, attestations, strategy) } fn optimistic_assemble<'a, R, D, I, J>( &self, rng: &mut R, subject: Self::Subject<'_, D>, pending: I, verified: J, strategy: &impl commonware_parallel::Strategy, ) -> Result> where R: rand_core::CryptoRng, D: $crate::Digest, I: IntoIterator>, I::IntoIter: ExactSizeIterator + Send, J: IntoIterator>, J::IntoIter: Send, { $crate::certificate::optimistic_assemble::( self, rng, subject, pending, verified, strategy, ) } fn assemble( &self, attestations: commonware_utils::iter::NonEmpty, strategy: &impl commonware_parallel::Strategy, ) -> Result where I: Iterator> + Send, { self.generic.assemble::(attestations, strategy) } fn is_attributable() -> bool { $crate::bls12381::certificate::threshold::Generic::::is_attributable() } } }; } #[cfg(test)] mod tests { use super::*; use crate::{ Signer as _, bls12381::{ dkg::feldman_desmedt as dkg, primitives::{ ops::threshold::sign_message, sharing::Mode, variant::{MinPk, MinSig, Variant}, }, }, certificate::{Membership, Scheme as _, Verifier as _}, ed25519::{self, PrivateKey as Ed25519PrivateKey}, sha256::Digest as Sha256Digest, }; use bytes::Bytes; use commonware_codec::{DecodeExt, Encode, types::lazy::Lazy}; use commonware_math::algebra::{Additive, Random}; use commonware_parallel::Sequential; use commonware_utils::{ Faults, N3f1, N5f1, NZU32, Participant, TryCollect, Widen, ordered::Set, test_rng, }; const NAMESPACE: &[u8] = b"test-bls12381-threshold"; const MESSAGE: &[u8] = b"test message"; /// Test context type for generic scheme tests. #[derive(Clone, Debug)] pub struct TestSubject { pub message: Bytes, } impl Subject for TestSubject { type Namespace = Vec; fn namespace<'a>(&self, derived: &'a Self::Namespace) -> &'a [u8] { derived } fn message(&self) -> Bytes { self.message.clone() } } // Use the macro to generate the test scheme impl_certificate_bls12381_threshold!(TestSubject, Vec, N3f1); #[allow(clippy::type_complexity)] fn setup_signers( rng: &mut impl CryptoRng, n: u32, ) -> ( Vec>, Scheme, Sharing, ) { // Generate identity keys (ed25519) let identity_keys: Vec<_> = (0..n) .map(|_| Ed25519PrivateKey::random(&mut *rng)) .collect(); let participants: Set = identity_keys .iter() .map(|sk| sk.public_key()) .try_collect() .unwrap(); // Generate threshold polynomial and shares using DKG let (polynomial, shares) = dkg::deal_anonymous::(&mut *rng, Mode::NonZeroCounter, NZU32!(n)); let signers = shares .into_iter() .map(|share| { Scheme::signer(NAMESPACE, participants.clone(), polynomial.clone(), share).unwrap() }) .collect(); let verifier = Scheme::verifier(NAMESPACE, participants, polynomial.clone()); (signers, verifier, polynomial) } fn test_sign_vote_roundtrip() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let scheme = &schemes[0]; let attestation = scheme .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(); assert!(scheme.verify_attestation::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &attestation, &Sequential, )); } #[test] fn test_sign_vote_roundtrip_variants() { test_sign_vote_roundtrip::(); test_sign_vote_roundtrip::(); } fn test_verifier_cannot_sign() { let mut rng = test_rng(); let (_, verifier, _) = setup_signers::(&mut rng, 4); assert!( verifier .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .is_none() ); } #[test] fn test_verifier_cannot_sign_variants() { test_verifier_cannot_sign::(); test_verifier_cannot_sign::(); } /// A member reports its membership, cannot sign, and verifies a quorum certificate, while a /// key outside the committee yields no member. fn test_member_reports_membership_without_signing() { let mut rng = test_rng(); let (schemes, verifier, polynomial) = setup_signers::(&mut rng, 4); let participants = verifier.participants().clone(); let subject = TestSubject { message: Bytes::from_static(MESSAGE), }; // A key outside the committee yields no member. let outsider = Ed25519PrivateKey::random(&mut rng).public_key(); assert!( Scheme::member( NAMESPACE, participants.clone(), polynomial.clone(), &outsider ) .is_none() ); // A member reports its membership but cannot sign. Verifiers report no membership, and // each signer reports the index it signs at. let index = Participant::new(2); let key = participants.key(index).unwrap().clone(); let member = Scheme::member(NAMESPACE, participants, polynomial, &key).unwrap(); assert_eq!(member.me(), Some(Membership::Member(index))); assert_eq!(member.me().unwrap().signer(), None); assert!(member.sign::(subject.clone()).is_none()); assert_eq!(verifier.me(), None); let certificate_verifier = Scheme::::certificate_verifier(NAMESPACE, *verifier.identity()); assert_eq!(certificate_verifier.me(), None); for scheme in &schemes { let Some(Membership::Signer(signer)) = scheme.me() else { panic!("signer must report signing membership"); }; let attestation = scheme.sign::(subject.clone()).unwrap(); assert_eq!(attestation.signer, signer); } // A member verifies a certificate assembled by a quorum of signers. let attestations: Vec<_> = schemes .iter() .take(Widen::widen(N3f1::quorum(schemes.len()))) .map(|s| s.sign::(subject.clone()).unwrap()) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); assert!(member.verify_certificate::<_, Sha256Digest>( &mut rng, subject, &certificate, &Sequential, )); } #[test] fn test_member_reports_membership_without_signing_variants() { test_member_reports_membership_without_signing::(); test_member_reports_membership_without_signing::(); } fn test_verify_attestations_filters_invalid() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 5); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let result = schemes[0].verify_attestations::<_, Sha256Digest, _>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, attestations.clone(), &Sequential, ); assert!(result.invalid.is_empty()); assert_eq!(result.verified.len(), quorum); // Corrupt one attestation's signer index. let mut attestations_corrupted = attestations.clone(); attestations_corrupted[0].signer = Participant::new(999); let result = schemes[0].verify_attestations::<_, Sha256Digest, _>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, attestations_corrupted, &Sequential, ); assert_eq!(result.invalid, vec![Participant::new(999)]); assert_eq!(result.verified.len(), quorum - 1); // Corrupt one attestation's signature. let mut attestations_corrupted = attestations; attestations_corrupted[0].signature = attestations_corrupted[1].signature.clone(); let result = schemes[0].verify_attestations::<_, Sha256Digest, _>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, attestations_corrupted, &Sequential, ); assert_eq!(result.invalid.len(), 1); assert_eq!(result.verified.len(), quorum - 1); } #[test] fn test_verify_attestations_filters_invalid_variants() { test_verify_attestations_filters_invalid::(); test_verify_attestations_filters_invalid::(); } fn test_assemble_certificate() { let mut rng = test_rng(); let (schemes, verifier, _) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Verify the assembled certificate assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &certificate, &Sequential, )); } #[test] fn test_assemble_certificate_variants() { test_assemble_certificate::(); test_assemble_certificate::(); } fn test_verify_certificate() { let mut rng = test_rng(); let (schemes, verifier, _) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &certificate, &Sequential, )); } #[test] fn test_verify_certificate_variants() { test_verify_certificate::(); test_verify_certificate::(); } fn test_verify_certificate_detects_corruption() { let mut rng = test_rng(); let (schemes, verifier, _) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Valid certificate passes assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &certificate, &Sequential, )); // Corrupted certificate fails let corrupted = Certificate::new(V::Signature::zero()); assert!(!verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &corrupted, &Sequential, )); } #[test] fn test_verify_certificate_detects_corruption_variants() { test_verify_certificate_detects_corruption::(); test_verify_certificate_detects_corruption::(); } fn test_certificate_codec_roundtrip() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); let encoded = certificate.encode(); assert_eq!( encoded.len(), Scheme::::max_certificate_size( u32::try_from(schemes.len()).unwrap() ) ); let decoded = Certificate::::decode(encoded).expect("decode certificate"); assert_eq!(decoded, certificate); } #[test] fn test_certificate_codec_roundtrip_variants() { test_certificate_codec_roundtrip::(); test_certificate_codec_roundtrip::(); } fn test_certificate_rejects_sub_quorum() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let expected = N3f1::quorum(schemes.len()); let found = expected - 1; let found_count = usize::try_from(found).expect("quorum exceeds usize::MAX"); let attestations: Vec<_> = schemes .iter() .take(found_count) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); assert_eq!( schemes[0].assemble(non_empty![@attestations], &Sequential), Err(AssemblyError::InsufficientAttestations(expected, found)) ); } #[test] fn test_certificate_rejects_sub_quorum_variants() { test_certificate_rejects_sub_quorum::(); test_certificate_rejects_sub_quorum::(); } fn test_certificate_rejects_duplicate_signers() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let quorum = usize::try_from(N3f1::quorum(schemes.len())).expect("quorum exceeds usize::MAX"); let mut attestations: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let duplicate = attestations[0].signer; attestations.push(attestations[0].clone()); assert_eq!( schemes[0].assemble(non_empty![@attestations], &Sequential), Err(AssemblyError::DuplicateSigner(duplicate)) ); } #[test] fn test_certificate_rejects_duplicate_signers_variants() { test_certificate_rejects_duplicate_signers::(); test_certificate_rejects_duplicate_signers::(); } fn test_certificate_rejects_unknown_signer() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let quorum = usize::try_from(N3f1::quorum(schemes.len())).expect("quorum exceeds usize::MAX"); let mut attestations: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let unknown_signer = Participant::from_usize(schemes.len()); let mut unknown = attestations[0].clone(); unknown.signer = unknown_signer; attestations.push(unknown); assert_eq!( schemes[0].assemble(non_empty![@attestations], &Sequential), Err(AssemblyError::UnknownSigner(unknown_signer)) ); } #[test] fn test_certificate_rejects_unknown_signer_variants() { test_certificate_rejects_unknown_signer::(); test_certificate_rejects_unknown_signer::(); } fn test_certificate_rejects_malformed_signature() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let quorum = usize::try_from(N3f1::quorum(schemes.len())).expect("quorum exceeds usize::MAX"); let mut attestations: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| { scheme .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let malformed_signer = attestations[0].signer; let mut malformed = Bytes::from_static(&[0u8]); attestations[0].signature = Lazy::deferred(&mut malformed, ()); assert_eq!( schemes[0].assemble(non_empty![@attestations], &Sequential), Err(AssemblyError::MalformedSignature(malformed_signer)) ); } #[test] fn test_certificate_rejects_malformed_signature_variants() { test_certificate_rejects_malformed_signature::(); test_certificate_rejects_malformed_signature::(); } fn test_verify_certificates_batch() { let mut rng = test_rng(); let (schemes, verifier, _) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let messages: [Bytes; 3] = [ Bytes::from_static(b"msg1"), Bytes::from_static(b"msg2"), Bytes::from_static(b"msg3"), ]; let mut certificates = Vec::new(); for msg in &messages { let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: msg.clone(), }) .unwrap() }) .collect(); certificates.push( schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(), ); } let certs_iter = messages.iter().zip(&certificates).map(|(msg, cert)| { ( TestSubject { message: msg.clone(), }, cert, ) }); assert!(verifier.verify_certificates::<_, Sha256Digest, _>( &mut rng, non_empty![@certs_iter], &Sequential )); } #[test] fn test_verify_certificates_batch_variants() { test_verify_certificates_batch::(); test_verify_certificates_batch::(); } fn test_verify_certificates_batch_detects_failure() { let mut rng = test_rng(); let (schemes, verifier, _) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let messages: [Bytes; 2] = [Bytes::from_static(b"msg1"), Bytes::from_static(b"msg2")]; let mut certificates = Vec::new(); for msg in &messages { let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: msg.clone(), }) .unwrap() }) .collect(); certificates.push( schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(), ); } // Corrupt second certificate certificates[1] = Certificate::new(V::Signature::zero()); let certs_iter = messages.iter().zip(&certificates).map(|(msg, cert)| { ( TestSubject { message: msg.clone(), }, cert, ) }); assert!(!verifier.verify_certificates::<_, Sha256Digest, _>( &mut rng, non_empty![@certs_iter], &Sequential )); } #[test] fn test_verify_certificates_batch_detects_failure_variants() { test_verify_certificates_batch_detects_failure::(); test_verify_certificates_batch_detects_failure::(); } fn test_certificate_verifier() { let mut rng = test_rng(); let (schemes, _, polynomial) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Create a certificate-only verifier using the identity from the polynomial let identity = polynomial.public(); let cert_verifier = Scheme::::certificate_verifier(NAMESPACE, *identity); // Should be able to verify certificates assert!(cert_verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &certificate, &Sequential, )); // Should not be able to sign assert!( cert_verifier .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .is_none() ); } #[test] fn test_certificate_verifier_variants() { test_certificate_verifier::(); test_certificate_verifier::(); } #[test] fn test_is_not_attributable() { assert!(!Generic::>::is_attributable()); assert!(!Scheme::::is_attributable()); assert!(!Generic::>::is_attributable()); assert!(!Scheme::::is_attributable()); } #[test] fn test_is_batchable() { assert!(Generic::>::is_batchable()); assert!(Scheme::::is_batchable()); assert!(Generic::>::is_batchable()); assert!(Scheme::::is_batchable()); } fn test_verifier_accepts_votes() { let mut rng = test_rng(); let (schemes, verifier, _) = setup_signers::(&mut rng, 4); let vote = schemes[1] .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(); assert!(verifier.verify_attestation::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &vote, &Sequential, )); } #[test] fn test_verifier_accepts_votes_variants() { test_verifier_accepts_votes::(); test_verifier_accepts_votes::(); } fn test_scheme_clone_and_verifier() { let mut rng = test_rng(); let (schemes, verifier, _) = setup_signers::(&mut rng, 4); // Clone a signer let signer = schemes[0].clone(); assert!( signer .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .is_some(), "signer should produce votes" ); // A verifier cannot produce votes assert!( verifier .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .is_none(), "verifier should not produce votes" ); } #[test] fn test_scheme_clone_and_verifier_variants() { test_scheme_clone_and_verifier::(); test_scheme_clone_and_verifier::(); } fn certificate_verifier_panics_on_vote() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let certificate_verifier = Scheme::::certificate_verifier( NAMESPACE, *schemes[0].identity(), ); let vote = schemes[1] .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(); // CertificateVerifier should panic when trying to verify a vote certificate_verifier.verify_attestation::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &vote, &Sequential, ); } #[test] #[should_panic(expected = "can only be called for signer and verifier")] fn test_certificate_verifier_panics_on_vote_min_pk() { certificate_verifier_panics_on_vote::(); } #[test] #[should_panic(expected = "can only be called for signer and verifier")] fn test_certificate_verifier_panics_on_vote_min_sig() { certificate_verifier_panics_on_vote::(); } fn signer_shares_must_match_participant_indices() { let mut rng = test_rng(); // Generate identity keys (ed25519) let identity_keys: Vec<_> = (0..4) .map(|_| Ed25519PrivateKey::random(&mut rng)) .collect(); let participants: Set = identity_keys .iter() .map(|sk| sk.public_key()) .try_collect() .unwrap(); let (polynomial, mut shares) = dkg::deal_anonymous::(&mut rng, Mode::NonZeroCounter, NZU32!(4)); shares[0].index = Participant::new(999); Scheme::::signer( NAMESPACE, participants, polynomial, shares[0].clone(), ); } #[test] #[should_panic(expected = "share index must match participant indices")] fn test_signer_shares_must_match_participant_indices_min_pk() { signer_shares_must_match_participant_indices::(); } #[test] #[should_panic(expected = "share index must match participant indices")] fn test_signer_shares_must_match_participant_indices_min_sig() { signer_shares_must_match_participant_indices::(); } fn make_participants(rng: &mut R, n: u32) -> Set { (0..n) .map(|_| Ed25519PrivateKey::random(&mut *rng).public_key()) .try_collect() .expect("participants are unique") } fn signer_validates_polynomial_degree() { let mut rng = test_rng(); let participants = make_participants(&mut rng, 4); // For four participants, N5f1 produces a degree 3 polynomial while this // N3f1 scheme requires degree 2. let (polynomial, shares) = dkg::deal_anonymous::(&mut rng, Mode::NonZeroCounter, NZU32!(4)); Scheme::::signer( NAMESPACE, participants, polynomial, shares[0].clone(), ); } #[test] #[should_panic(expected = "polynomial threshold must equal quorum")] fn test_signer_validates_polynomial_degree_min_pk() { signer_validates_polynomial_degree::(); } #[test] #[should_panic(expected = "polynomial threshold must equal quorum")] fn test_signer_validates_polynomial_degree_min_sig() { signer_validates_polynomial_degree::(); } fn verifier_validates_polynomial_degree() { let mut rng = test_rng(); let participants = make_participants(&mut rng, 4); // For four participants, N5f1 produces a degree 3 polynomial while this // N3f1 scheme requires degree 2. let (polynomial, _) = dkg::deal_anonymous::(&mut rng, Mode::NonZeroCounter, NZU32!(4)); Scheme::::verifier(NAMESPACE, participants, polynomial); } #[test] #[should_panic(expected = "polynomial threshold must equal quorum")] fn test_verifier_validates_polynomial_degree_min_pk() { verifier_validates_polynomial_degree::(); } #[test] #[should_panic(expected = "polynomial threshold must equal quorum")] fn test_verifier_validates_polynomial_degree_min_sig() { verifier_validates_polynomial_degree::(); } fn signer_polynomial_threshold_must_equal_quorum() { let mut rng = test_rng(); let participants = make_participants(&mut rng, 5); // Create a polynomial with threshold 4, but quorum of 5 participants is 4 // so this should succeed. Let's use threshold 2 to make it fail. // quorum(5) = 4, but polynomial.required() = 2, so this should panic let (polynomial, shares) = dkg::deal_anonymous::(&mut rng, Mode::NonZeroCounter, NZU32!(2)); Scheme::::signer( NAMESPACE, participants, polynomial, shares[0].clone(), ); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_signer_polynomial_threshold_must_equal_quorum_min_pk() { signer_polynomial_threshold_must_equal_quorum::(); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_signer_polynomial_threshold_must_equal_quorum_min_sig() { signer_polynomial_threshold_must_equal_quorum::(); } fn verifier_polynomial_threshold_must_equal_quorum() { let mut rng = test_rng(); let participants = make_participants(&mut rng, 5); // Create a polynomial with threshold 2, but quorum of 5 participants is 4 // quorum(5) = 4, but polynomial.required() = 2, so this should panic let (polynomial, _) = dkg::deal_anonymous::(&mut rng, Mode::NonZeroCounter, NZU32!(2)); Scheme::::verifier(NAMESPACE, participants, polynomial); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_verifier_polynomial_threshold_must_equal_quorum_min_pk() { verifier_polynomial_threshold_must_equal_quorum::(); } #[test] #[should_panic(expected = "polynomial total must equal participant len")] fn test_verifier_polynomial_threshold_must_equal_quorum_min_sig() { verifier_polynomial_threshold_must_equal_quorum::(); } fn certificate_decode_rejects_length_mismatch() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let quorum = N3f1::quorum(schemes.len() as u32) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); let mut encoded = certificate.encode(); encoded.truncate(encoded.len() - 1); assert!(V::Signature::decode(encoded).is_err()); } #[test] fn test_certificate_decode_rejects_length_mismatch_variants() { certificate_decode_rejects_length_mismatch::(); certificate_decode_rejects_length_mismatch::(); } fn sign_vote_partial_matches_share() { let mut rng = test_rng(); let (schemes, _, _) = setup_signers::(&mut rng, 4); let scheme = &schemes[0]; let signature = scheme .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(); // Verify the partial signature matches what we'd get from direct signing let share = scheme.share().expect("expected signer"); let expected = sign_message::(share, NAMESPACE, MESSAGE); assert_eq!(signature.signer, share.index); assert_eq!(signature.signature.get().unwrap(), &expected.value); } #[test] fn test_sign_vote_partial_matches_share_variants() { sign_vote_partial_matches_share::(); sign_vote_partial_matches_share::(); } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use commonware_codec::conformance::CodecConformance; commonware_conformance::conformance_tests! { CodecConformance>, } } }