//! BLS12-381 multi-signature signing scheme implementation. //! //! This module provides both the generic BLS12-381 multisig implementation and a macro to generate //! protocol-specific wrappers. //! //! # Proof of Possession //! //! Before constructing a scheme, callers must verify a proof of possession (PoP) for every BLS //! public key in the participant map. Without it, aggregate certificate verification is vulnerable //! to rogue-key attacks. Verify each PoP during participant registration with //! [`verify_proof_of_possession`](crate::bls12381::primitives::ops::verify_proof_of_possession), //! then construct schemes only from the validated participant set. #[cfg(feature = "mocks")] pub mod mocks; use crate::{ PublicKey, bls12381::primitives::{ group::Private, ops::{self, aggregate, batch}, variant::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, EncodeSize, Error, FixedSize, Read, ReadExt, Write, types::lazy::Lazy, }; use commonware_parallel::Strategy; use commonware_utils::{ Participant, Widen, iter::NonEmpty, non_empty, ordered::{BiMap, Quorum, Set}, }; use rand_core::CryptoRng; #[cfg(feature = "std")] use std::collections::BTreeSet; /// Generic BLS12-381 multi-signature implementation. /// /// This struct 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. #[derive(Clone, Debug)] pub struct Generic { /// Participants' identity keys and BLS signing keys. /// /// Every signing key must have a verified proof of possession. pub participants: BiMap, /// Key used for generating signatures. pub signer: Option<(Participant, Private)>, /// Pre-computed namespace(s) for this subject type. pub namespace: N, } impl Generic { /// Creates a new scheme instance with the provided key material. /// /// Participants have both an identity key and a signing key. The identity key /// is used for participant set ordering and indexing, while the signing key is used for /// signing and verification. /// /// Returns `None` if the provided private key does not match any signing key /// in the participant set. /// /// # Security /// /// This function does not verify proofs of possession. The caller must verify a PoP for every /// BLS public key in `participants` before constructing the scheme. See the [module-level /// documentation](crate::bls12381::certificate::multisig) for details. pub fn signer( namespace: &[u8], participants: BiMap, private_key: Private, ) -> Option { let public_key = ops::compute_public::(&private_key); let signer = participants .values() .iter() .position(|p| p == &public_key) .map(|index| (Participant::from_usize(index), private_key))?; Some(Self { participants, signer: Some(signer), namespace: N::derive(namespace), }) } /// Builds a verifier that can authenticate signatures and certificates. /// /// Participants have both an identity key and a signing key. The identity key /// is used for participant set ordering and indexing, while the signing key is used for /// verification. /// /// # Security /// /// This function does not verify proofs of possession. The caller must verify a PoP for every /// BLS public key in `participants` before constructing the scheme. See the [module-level /// documentation](crate::bls12381::certificate::multisig) for details. pub fn verifier(namespace: &[u8], participants: BiMap) -> Self { Self { participants, signer: None, namespace: N::derive(namespace), } } /// Returns the ordered set of identity keys. pub const fn participants(&self) -> &Set

{ self.participants.keys() } /// Returns the index this instance signs at, or `None` if it cannot sign. pub fn signer_index(&self) -> Option { self.signer.as_ref().map(|(index, _)| *index) } /// Signs a subject and returns the attestation. pub fn sign(&self, subject: impl Subject) -> Option> where S: Scheme, { let (index, private_key) = self.signer.as_ref()?; let signature = ops::sign_message::( private_key, subject.namespace(&self.namespace), &subject.message(), ); Some(Attestation { signer: *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 Some(public_key) = self.participants.value(attestation.signer.into()) else { return false; }; let Some(sig) = attestation.signature.get() else { return false; }; ops::verify_message::( public_key, subject.namespace(&self.namespace), &subject.message(), sig, ) .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 (filtered, failures) = strategy.map_partition_collect_vec(attestations.into_iter(), |attestation| { let signer = attestation.signer; let value = self .participants .value(signer.into()) .and_then(|public_key| { attestation .signature .get() .cloned() .map(|signature| (attestation, (*public_key, signature))) }); (signer, value) }); let mut invalid: BTreeSet<_> = failures.into_iter().collect(); let (candidates, entries): (Vec<_>, Vec<_>) = filtered.into_iter().unzip(); // If there are no candidates to verify, return before doing any work. if candidates.is_empty() { return Verification::new(candidates, invalid.into_iter().collect()); } // Verify attestations and return any invalid ones. let namespace = subject.namespace(&self.namespace); let message = subject.message(); let entries = non_empty![@entries]; let invalid_indices = batch::verify_same_message::<_, V, _>( rng, namespace, message.as_ref(), entries, strategy, ); // Mark invalid attestations. for idx in invalid_indices { invalid.insert(candidates[idx].signer); } // Collect the verified attestations. let verified = candidates .into_iter() .filter(|attestation| !invalid.contains(&attestation.signer)) .collect(); Verification::new(verified, invalid.into_iter().collect()) } /// Assembles a certificate from a non-empty collection of attestations. pub fn assemble(&self, attestations: NonEmpty) -> Result, AssemblyError> where S: Scheme, I: Iterator>, { // Collect the signers and signatures. let mut entries = Vec::new(); for Attestation { signer, signature } in attestations { self.participants .value(signer.into()) .ok_or(AssemblyError::UnknownSigner(signer))?; let signature = signature .get() .cloned() .ok_or(AssemblyError::MalformedSignature(signer))?; entries.push((signer, signature)); } // Produce signers and aggregate signature. let (signers, signatures): (Vec<_>, Vec<_>) = entries.into_iter().unzip(); let signers = Signers::try_from((self.participants.keys(), signers))? .require(self.participants.quorum::())?; let signatures = non_empty![@signatures.iter()]; let signature = aggregate::combine_signatures::(signatures); Ok(Certificate { signers, signature: Lazy::from(signature), }) } /// Verifies a certificate. pub fn verify_certificate( &self, _rng: &mut R, subject: impl Subject, certificate: &Certificate, ) -> bool where S: Scheme, R: CryptoRng, { // If the certificate signers length does not match the participant set, return false. if certificate.signers.len() != self.participants.len() { return false; } // If the certificate does not meet the quorum, return false. if certificate.signers.count() < Widen::widen(self.participants.quorum::()) { return false; } // Malformed signatures can skip per-signer group operations. let Some(signature) = certificate.signature.get() else { return false; }; // Aggregate the public keys. let mut agg_public = aggregate::PublicKey::::zero(); for signer in certificate.signers.iter() { let Some(public_key) = self.participants.value(signer.into()) else { return false; }; agg_public.add(public_key); } // Verify the aggregate signature. aggregate::verify_same_message::( &agg_public, subject.namespace(&self.namespace), &subject.message(), signature, ) .is_ok() } /// Verifies multiple certificates (no batch optimization for BLS multisig). pub fn verify_certificates<'a, S, R, T, I>( &self, rng: &mut R, certificates: NonEmpty, ) -> bool where S: Scheme, T: Subject, R: CryptoRng, I: Iterator)>, { for (subject, certificate) in certificates { if !self.verify_certificate::(rng, subject, certificate) { return false; } } true } pub const fn is_attributable() -> bool { true } pub const fn is_batchable() -> bool { true } /// Returns the maximum encoded certificate size for at most `participants` participants. pub fn max_certificate_size(participants: u32) -> usize { Signers::max_encode_size(participants) + V::Signature::SIZE } pub const fn certificate_codec_config(&self) -> as Read>::Cfg { self.participants.len() } pub const fn certificate_codec_config_unbounded() -> as Read>::Cfg { u32::MAX as usize } } /// Certificate formed by an aggregated BLS12-381 signature plus the signers that /// contributed to it. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Certificate { /// Bitmap of participant indices that contributed signatures. pub signers: Signers, /// Aggregated BLS signature covering all signatures in this certificate. pub signature: Lazy>, } impl Write for Certificate { fn write(&self, writer: &mut impl BufMut) { self.signers.write(writer); self.signature.write(writer); } } impl EncodeSize for Certificate { fn encode_size(&self) -> usize { self.signers.encode_size() + self.signature.encode_size() } } impl Read for Certificate { type Cfg = usize; fn read_cfg(reader: &mut impl Buf, participants: &usize) -> Result { let signers = Signers::read_cfg(reader, participants)?; if signers.count() == 0 { return Err(Error::Invalid( "cryptography::bls12381::certificate::multisig::Certificate", "Certificate contains no signers", )); } let signature = Lazy::>::read(reader)?; Ok(Self { signers, signature }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Certificate where V::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let signers = Signers::arbitrary(u)?; let signature = aggregate::Signature::arbitrary(u)?; Ok(Self { signers, signature: Lazy::from(signature), }) } } /// Generates a BLS12-381 multisig 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`](commonware_utils::Faults) implementation used to compute certificate /// quorums. /// /// # Security /// /// The generated constructors do not verify proofs of possession. Before constructing a scheme, /// callers must verify a PoP for every BLS public key in the participant map. Otherwise, aggregate /// certificate verification is vulnerable to rogue-key attacks. /// /// # Example /// ```ignore /// // For non-generic subject types with a single namespace: /// impl_certificate_bls12381_multisig!(MySubject, Vec, commonware_utils::N3f1); /// /// // For protocols with generic subject types: /// impl_certificate_bls12381_multisig!( /// Subject<'a, D>, /// Namespace, /// commonware_utils::N3f1, /// ); /// ``` #[macro_export] macro_rules! impl_certificate_bls12381_multisig { ($subject:ty, $namespace:ty, $faults:ty) => { /// Generates a test fixture with Ed25519 identities and BLS12-381 multisig 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::multisig::mocks::fixture::<_, V, _>( rng, namespace, n, Scheme::signer, Scheme::verifier, ) } /// BLS12-381 multi-signature signing scheme wrapper. /// /// Every participant BLS public key must have a verified proof of possession before this /// scheme is constructed. #[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::multisig::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 scheme instance with the provided key material. /// /// # Security /// /// This function does not verify proofs of possession. The caller must verify a PoP /// for every BLS public key in `participants` before constructing the scheme. pub fn signer( namespace: &[u8], participants: commonware_utils::ordered::BiMap, private_key: $crate::bls12381::primitives::group::Private, ) -> Option { Some(Self { generic: $crate::bls12381::certificate::multisig::Generic::signer( namespace, participants, private_key, )?, member: None, }) } /// Builds a verifier that can authenticate signatures and certificates. /// /// A participant without signing material should use [`Self::member`] instead, so the /// scheme reports its membership. /// /// # Security /// /// This function does not verify proofs of possession. The caller must verify a PoP /// for every BLS public key in `participants` before constructing the scheme. pub fn verifier( namespace: &[u8], participants: commonware_utils::ordered::BiMap, ) -> Self { Self { generic: $crate::bls12381::certificate::multisig::Generic::verifier( namespace, participants, ), member: None, } } /// Builds 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 the identity key `public_key` in the participant set. Returns /// `None` if `public_key` is not a participant identity key. /// /// # Security /// /// This function does not verify proofs of possession. The caller must verify a PoP /// for every BLS public key in `participants` before constructing the scheme. /// /// [`Scheme::me`]: commonware_cryptography::certificate::Scheme::me /// [`Membership::Member`]: commonware_cryptography::certificate::Membership::Member pub fn member( namespace: &[u8], participants: commonware_utils::ordered::BiMap, public_key: &P, ) -> Option { let member = commonware_utils::ordered::Quorum::index(participants.keys(), public_key)?; Some(Self { generic: $crate::bls12381::certificate::multisig::Generic::verifier( namespace, participants, ), member: Some(member), }) } } 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::multisig::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) } fn is_batchable() -> bool { $crate::bls12381::certificate::multisig::Generic::::is_batchable() } fn max_certificate_size(participants: u32) -> usize { $crate::bls12381::certificate::multisig::Generic::::max_certificate_size(participants) } fn certificate_codec_config( &self, ) -> ::Cfg { self.generic.certificate_codec_config() } fn certificate_codec_config_unbounded() -> ::Cfg { $crate::bls12381::certificate::multisig::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) } fn is_attributable() -> bool { $crate::bls12381::certificate::multisig::Generic::::is_attributable() } } }; } #[cfg(test)] mod tests { use super::*; use crate::{ Signer as _, bls12381::primitives::{ group::{Private, Scalar}, ops::compute_public, variant::{MinPk, MinSig, Variant}, }, certificate::{Attestation, Membership, Scheme as _, Verifier as _}, ed25519::{self, PrivateKey as Ed25519PrivateKey}, sha256::Digest as Sha256Digest, }; use bytes::Bytes; use commonware_codec::{Decode, Encode}; use commonware_math::algebra::{CryptoGroup, Random}; use commonware_parallel::Sequential; use commonware_utils::{Faults, N3f1, Participant, TryCollect, ordered::BiMap, test_rng}; const NAMESPACE: &[u8] = b"test-bls12381-multisig"; 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_multisig!(TestSubject, Vec, N3f1); fn setup_signers( rng: &mut impl CryptoRng, n: u32, ) -> ( Vec>, Scheme, ) { // Generate identity keys (ed25519) and consensus keys (BLS) let identity_keys: Vec<_> = (0..n) .map(|_| Ed25519PrivateKey::random(&mut *rng)) .collect(); let consensus_keys: Vec = (0..n).map(|_| Private::random(&mut *rng)).collect(); // Build BiMap of identity public keys -> consensus public keys let participants: BiMap = identity_keys .iter() .zip(consensus_keys.iter()) .map(|(id_sk, cons_sk)| (id_sk.public_key(), compute_public::(cons_sk))) .try_collect() .unwrap(); let signers = consensus_keys .into_iter() .map(|sk| Scheme::signer(NAMESPACE, participants.clone(), sk).unwrap()) .collect(); let verifier = Scheme::verifier(NAMESPACE, participants); (signers, verifier) } #[test] fn test_is_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_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) = setup_signers::(&mut rng, 4); let participants = verifier.generic.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::<_, V>::member(NAMESPACE, participants.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.keys().key(index).unwrap().clone(); let member = Scheme::member(NAMESPACE, participants, &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); 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, _) = 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_eq!(certificate.signers.count(), quorum); } #[test] fn test_assemble_certificate_variants() { test_assemble_certificate::(); test_assemble_certificate::(); } fn test_assemble_certificate_sorts_signers() { let mut rng = test_rng(); let (schemes, _) = setup_signers::(&mut rng, 4); // Get indices and sort them to create attestations in guaranteed reverse order let mut indexed: Vec<_> = (0..3) .map(|i| (schemes[i].me().unwrap().index(), i)) .collect(); indexed.sort_by_key(|(idx, _)| *idx); // Create attestations in reverse sorted order (guaranteed non-sorted) let attestations = vec![ schemes[indexed[2].1] .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(), schemes[indexed[1].1] .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(), schemes[indexed[0].1] .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(), ]; let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Verify signers are sorted by signer index let expected: Vec<_> = indexed.iter().map(|(idx, _)| *idx).collect(); assert_eq!(certificate.signers.iter().collect::>(), expected); } #[test] fn test_assemble_certificate_sorts_signers_variants() { test_assemble_certificate_sorts_signers::(); test_assemble_certificate_sorts_signers::(); } 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 mut corrupted = certificate; corrupted.signature = Lazy::from(aggregate::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_cfg(encoded, &schemes.len()).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_invalid_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(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); // Corrupt signer index to be out of range attestations[0].signer = Participant::new(999); assert_eq!( schemes[0].assemble(non_empty![@attestations], &Sequential), Err(AssemblyError::UnknownSigner(Participant::new(999))) ); } #[test] fn test_certificate_rejects_invalid_signer_variants() { test_certificate_rejects_invalid_signer::(); test_certificate_rejects_invalid_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(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let 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(signer)) ); } #[test] fn test_certificate_rejects_malformed_signature_variants() { test_certificate_rejects_malformed_signature::(); test_certificate_rejects_malformed_signature::(); } fn test_verify_certificate_rejects_sub_quorum() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(&mut rng, 4); let participants_len = schemes.len(); let attestations: Vec<_> = schemes .iter() .take(3) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let mut certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Artificially truncate to below quorum let mut signers: Vec = certificate.signers.iter().collect(); signers.pop(); certificate.signers = Signers::new(participants_len.try_into().unwrap(), signers).unwrap(); assert!(!verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &certificate, &Sequential, )); } #[test] fn test_verify_certificate_rejects_sub_quorum_variants() { test_verify_certificate_rejects_sub_quorum::(); test_verify_certificate_rejects_sub_quorum::(); } fn test_verify_certificate_rejects_signers_size_mismatch() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(&mut rng, 4); let participants_len = schemes.len(); let attestations: Vec<_> = schemes .iter() .take(3) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let mut certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Make the signers bitmap size larger than participants let signers: Vec = certificate.signers.iter().collect(); certificate.signers = Signers::new((participants_len + 1).try_into().unwrap(), signers).unwrap(); assert!(!verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &certificate, &Sequential, )); } #[test] fn test_verify_certificate_rejects_signers_size_mismatch_variants() { test_verify_certificate_rejects_signers_size_mismatch::(); test_verify_certificate_rejects_signers_size_mismatch::(); } 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: Vec = [b"msg1".as_slice(), b"msg2".as_slice(), b"msg3".as_slice()] .into_iter() .map(Bytes::copy_from_slice) .collect(); 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: Vec = [b"msg1".as_slice(), b"msg2".as_slice()] .into_iter() .map(Bytes::copy_from_slice) .collect(); 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].signature = Lazy::from(aggregate::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_assemble_certificate_rejects_duplicate_signers() { let mut rng = test_rng(); let (schemes, _) = setup_signers::(&mut rng, 4); let mut attestations: Vec<_> = schemes .iter() .take(3) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); // Add a duplicate of the last attestation let duplicate = attestations.last().unwrap().clone(); let signer = duplicate.signer; attestations.push(duplicate); assert_eq!( schemes[0].assemble(non_empty![@attestations], &Sequential), Err(AssemblyError::DuplicateSigner(signer)) ); } #[test] fn test_assemble_certificate_rejects_duplicate_signers_min_pk() { test_assemble_certificate_rejects_duplicate_signers::(); } #[test] fn test_assemble_certificate_rejects_duplicate_signers_min_sig() { test_assemble_certificate_rejects_duplicate_signers::(); } 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(), "cloned signer should retain signing capability" ); // A verifier cannot produce votes assert!( verifier .sign::(TestSubject { message: Bytes::from_static(MESSAGE) }) .is_none(), "verifier must not sign votes" ); } #[test] fn test_scheme_clone_and_verifier_variants() { test_scheme_clone_and_verifier::(); test_scheme_clone_and_verifier::(); } fn test_certificate_decode_validation() { let mut rng = test_rng(); let (schemes, _) = setup_signers::(&mut rng, 4); let participants_len = schemes.len(); let participant_count = u32::try_from(participants_len).expect("participant count exceeds u32::MAX"); let attestations: Vec<_> = schemes .iter() .take(3) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Well-formed certificate decodes successfully let encoded = certificate.encode(); let decoded = Certificate::::decode_cfg(encoded, &participants_len).expect("decode certificate"); assert_eq!(decoded, certificate); // Certificate with no signers is rejected let empty = Certificate:: { signers: Signers::new(participant_count, std::iter::empty::()).unwrap(), signature: certificate.signature.clone(), }; assert!(Certificate::::decode_cfg(empty.encode(), &participants_len).is_err()); // Certificate containing more signers than the participant set is rejected let mut signers = certificate.signers.iter().collect::>(); signers.push(Participant::from_usize(participants_len)); let extended = Certificate:: { signers: Signers::new(participant_count + 1, signers).unwrap(), signature: certificate.signature, }; assert!(Certificate::::decode_cfg(extended.encode(), &participants_len).is_err()); } #[test] fn test_certificate_decode_validation_variants() { test_certificate_decode_validation::(); test_certificate_decode_validation::(); } fn test_verify_certificate_rejects_unknown_signer() { let mut rng = test_rng(); let (schemes, verifier) = setup_signers::(&mut rng, 4); let participants_len = schemes.len(); let attestations: Vec<_> = schemes .iter() .take(3) .map(|s| { s.sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap() }) .collect(); let mut certificate = schemes[0] .assemble(non_empty![@attestations], &Sequential) .unwrap(); // Add an unknown signer (out of range) let mut signers: Vec = certificate.signers.iter().collect(); signers.push(Participant::from_usize(participants_len)); certificate.signers = Signers::new((participants_len + 1).try_into().unwrap(), signers).unwrap(); assert!(!verifier.verify_certificate::<_, Sha256Digest>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, &certificate, &Sequential, )); } #[test] fn test_verify_certificate_rejects_unknown_signer_variants() { test_verify_certificate_rejects_unknown_signer::(); test_verify_certificate_rejects_unknown_signer::(); } fn test_verify_attestations_rejects_malleability() { let mut rng = test_rng(); let (schemes, _) = setup_signers::(&mut rng, 4); let attestation1 = schemes[0] .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(); let attestation2 = schemes[1] .sign::(TestSubject { message: Bytes::from_static(MESSAGE), }) .unwrap(); let verification = schemes[0].verify_attestations::<_, Sha256Digest, _>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, vec![attestation1.clone(), attestation2.clone()], &Sequential, ); assert!(verification.invalid.is_empty()); assert_eq!(verification.verified.len(), 2); let random_scalar = Scalar::random(&mut rng); let delta = V::Signature::generator() * &random_scalar; let forged_attestation1: Attestation> = Attestation { signer: attestation1.signer, signature: (*attestation1.signature.get().unwrap() - &delta).into(), }; let forged_attestation2: Attestation> = Attestation { signer: attestation2.signer, signature: (*attestation2.signature.get().unwrap() + &delta).into(), }; let forged_sum = *forged_attestation1.signature.get().unwrap() + forged_attestation2.signature.get().unwrap(); let valid_sum = *attestation1.signature.get().unwrap() + attestation2.signature.get().unwrap(); assert_eq!(forged_sum, valid_sum, "signature sums should be equal"); let verification = schemes[0].verify_attestations::<_, Sha256Digest, _>( &mut rng, TestSubject { message: Bytes::from_static(MESSAGE), }, vec![forged_attestation1, forged_attestation2], &Sequential, ); assert!( !verification.invalid.is_empty(), "forged attestations should be detected" ); } #[test] fn test_verify_attestations_rejects_malleability_variants() { test_verify_attestations_rejects_malleability::(); test_verify_attestations_rejects_malleability::(); } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use crate::bls12381::primitives::variant::MinSig; use commonware_codec::conformance::CodecConformance; commonware_conformance::conformance_tests! { CodecConformance>, } } }