//! Cryptographic primitives for generating and verifying certificates. //! //! This module provides the [`Verifier`] and [`Scheme`] traits and implementations for //! producing signatures, validating them (individually or in batches), assembling certificates, and //! verifying recovered certificates. //! //! # Pluggable Cryptography //! //! Certificates are generic over the signing scheme, allowing users to choose //! the scheme best suited for their requirements: //! //! - [`ed25519`]: Attributable signatures with individual verification. HSM-friendly, no trusted //! setup required, and widely supported. Certificates contain individual signatures from each //! signer. //! //! - [`secp256r1`]: Attributable signatures with individual verification. HSM-friendly, no trusted //! setup required, and widely supported by hardware security modules. Unlike ed25519, does not //! benefit from batch verification. Certificates contain individual signatures from each signer. #![cfg_attr( feature = "bls12381", doc = " - [`bls12381_multisig`]: Attributable signatures with aggregated verification. Signatures can be aggregated into a single multi-signature for compact certificates while preserving attribution (signer indices are stored alongside the aggregated signature). Callers must verify a proof of possession for every BLS signing key before constructing the scheme. - [`bls12381_threshold`]: Non-attributable threshold signatures. Produces succinct certificates that are constant-size regardless of committee size. Requires a trusted setup (distributed key generation) and cannot attribute signatures to individual signers. " )] //! # Attributable Schemes and Fault Evidence //! //! Signing schemes differ in whether per-participant activities can be used as evidence of //! either liveness or of committing a fault: //! //! - **Attributable Schemes**: Individual signatures can be presented to some third party as //! evidence of either liveness or of committing a fault. Certificates contain signer indices //! alongside individual signatures, enabling secure per-participant activity tracking and //! conflict detection. //! //! - **Non-Attributable Schemes**: Individual signatures cannot be presented to some third party //! as evidence because they can be forged after collecting a quorum of partial signatures. //! Authenticated peer connections allow this evidence to be used locally, but not by an external //! observer. //! //! The [`Scheme::is_attributable()`] associated function signals whether evidence can be safely //! exposed to third parties. //! //! # Identity Keys vs Signing Keys //! //! A participant may supply both an identity key and a signing key. The identity key //! is used for assigning a unique order to the participant set and authenticating connections //! whereas the signing key is used for producing and verifying signatures/certificates. //! #![cfg_attr( feature = "bls12381", doc = "Some cryptographic schemes are only performant when used in batch verification (like [`bls12381_multisig`]) and/or are refreshed frequently (like [`bls12381_threshold`])." )] //! Refer to [ed25519] for an example of a scheme that uses the same key for both purposes. #[cfg(feature = "bls12381")] pub use crate::bls12381::certificate::{ multisig as bls12381_multisig, threshold as bls12381_threshold, }; pub use crate::ed25519::certificate as ed25519; #[commonware_macros::stability(ALPHA)] pub use crate::secp256r1::certificate as secp256r1; use crate::{Digest, PublicKey}; #[cfg(not(feature = "std"))] use alloc::{collections::BTreeSet, sync::Arc, vec, vec::Vec}; use bytes::{BufMut, Bytes}; use commonware_codec::{ Buf, Codec, CodecFixed, EncodeSize, Error as CodecError, FixedSize, MaxEncodeSize, Read, ReadExt, Write, types::lazy::Lazy, util::max_items_size, }; use commonware_parallel::Strategy; use commonware_utils::{Faults, Participant, Widen, bitmap::BitMap, iter::NonEmpty, ordered::Set}; use core::{fmt::Debug, hash::Hash}; use rand_chacha::ChaCha20Rng; use rand_core::{CryptoRng, SeedableRng}; #[cfg(feature = "std")] use std::{collections::BTreeSet, sync::Arc, vec::Vec}; use thiserror::Error; /// The scheme's own place in the participant set. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Membership { /// A participant that holds signing material at this index. Signer(Participant), /// A participant without signing material at this index. Member(Participant), } impl Membership { /// Returns the participant's index. pub const fn index(&self) -> Participant { match self { Self::Signer(index) | Self::Member(index) => *index, } } /// Returns the participant's index if it holds signing material. pub const fn signer(&self) -> Option { match self { Self::Signer(index) => Some(*index), Self::Member(_) => None, } } } /// A participant's attestation for a certificate. #[derive(Clone, Debug)] pub struct Attestation { /// Index of the signer inside the participant set. pub signer: Participant, /// Scheme-specific signature or share produced for a given subject. pub signature: Lazy, } impl PartialEq for Attestation { fn eq(&self, other: &Self) -> bool { self.signer == other.signer && self.signature == other.signature } } impl Eq for Attestation {} impl Hash for Attestation { fn hash(&self, state: &mut H) { self.signer.hash(state); self.signature.hash(state); } } impl Write for Attestation { fn write(&self, writer: &mut impl BufMut) { self.signer.write(writer); self.signature.write(writer); } } impl EncodeSize for Attestation { fn encode_size(&self) -> usize { self.signer.encode_size() + self.signature.encode_size() } } impl MaxEncodeSize for Attestation { const MAX_ENCODE_SIZE: usize = Participant::MAX_ENCODE_SIZE + S::Signature::SIZE; } impl Read for Attestation { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let signer = Participant::read(reader)?; let signature = ReadExt::read(reader)?; Ok(Self { signer, signature }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Attestation where S::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let signer = Participant::arbitrary(u)?; let signature = S::Signature::arbitrary(u)?; Ok(Self { signer, signature: signature.into(), }) } } /// Result of batch-verifying attestations. pub struct Verification { /// Contains the attestations accepted by the scheme. pub verified: Vec>, /// Identifies the participant indices rejected during batch verification. pub invalid: Vec, } impl Verification { /// Creates a new `Verification` result. pub const fn new(verified: Vec>, invalid: Vec) -> Self { Self { verified, invalid } } } /// Errors returned while assembling attestations into a certificate. #[derive(Debug, Error, PartialEq, Eq)] pub enum AssemblyError { /// Structurally valid attestations do not meet the scheme's quorum. #[error("insufficient attestations: expected={0}, found={1}")] InsufficientAttestations(u32, u32), /// An attestation references an index outside the participant set. #[error("unknown signer: {0}")] UnknownSigner(Participant), /// More than one attestation references the same signer. #[error("duplicate signer: {0}")] DuplicateSigner(Participant), /// An attestation contains a malformed encoded signature. #[error("malformed signature from signer: {0}")] MalformedSignature(Participant), /// The scheme could not recover a certificate from structurally valid attestations. #[error("certificate recovery failed")] RecoveryFailed, } /// Trait for namespace types that can derive themselves from a base namespace. /// /// This trait is implemented by namespace types to define how they are computed /// from a base namespace string. pub trait Namespace: Clone + Send + Sync { /// Derive a namespace from the given base. fn derive(namespace: &[u8]) -> Self; } impl Namespace for Vec { fn derive(namespace: &[u8]) -> Self { namespace.to_vec() } } /// Identifies the subject of a signature or certificate. pub trait Subject: Clone + Debug + Send + Sync { /// Pre-computed namespace(s) for this subject type. type Namespace: Namespace; /// Get the namespace bytes for this subject instance. fn namespace<'a>(&self, derived: &'a Self::Namespace) -> &'a [u8]; /// Get the message bytes for this subject instance. fn message(&self) -> Bytes; } /// Cryptographic surface for recovered certificate verification. /// /// A `Verifier` verifies recovered certificates, but does not expose participant /// metadata or signing operations. pub trait Verifier: Clone + Debug + Send + Sync + 'static { /// Subject type for certificate verification. type Subject<'a, D: Digest>: Subject; /// Fault model used to compute certificate quorums. type Faults: Faults; /// Public key type for participant identity used to order and index the participant set. type PublicKey: PublicKey; /// Certificate assembled from a set of attestations. type Certificate: Clone + Debug + Eq + Hash + Send + Sync + Codec; /// Verifies a certificate that was recovered or received from the network. /// /// Acceptance authenticates a quorum for `subject` under the scheme's key setup, fault model, /// and cryptographic assumptions. It need not authenticate the individual attestations used /// to assemble the certificate. fn verify_certificate( &self, rng: &mut R, subject: Self::Subject<'_, D>, certificate: &Self::Certificate, strategy: &impl Strategy, ) -> bool where R: CryptoRng, D: Digest; /// Verifies a non-empty stream of certificates, returning `false` at the first verification /// failure. fn verify_certificates<'a, R, D, I>( &self, rng: &mut R, certificates: NonEmpty, strategy: &impl Strategy, ) -> bool where R: CryptoRng, D: Digest, I: Iterator, &'a Self::Certificate)>, { for (subject, certificate) in certificates { if !self.verify_certificate(rng, subject, certificate, strategy) { return false; } } true } /// Batch-verifies certificates and returns a per-item result. /// /// For batchable schemes, attempts batch verification first and bisects /// on failure to efficiently identify invalid certificates. For /// non-batchable schemes, verifies each certificate individually. /// Empty input returns an empty result. fn verify_certificates_bisect<'a, R, D>( &self, rng: &mut R, certificates: &[(Self::Subject<'a, D>, &'a Self::Certificate)], strategy: &impl Strategy, ) -> Vec where R: CryptoRng, D: Digest, Self::Subject<'a, D>: Copy, { let len = certificates.len(); let mut verified = vec![false; len]; if len == 0 { return verified; } // Non-batchable schemes (e.g. secp256r1) gain nothing from bisection // since verify_certificates already checks one-by-one. if !Self::is_batchable() { for (i, (subject, certificate)) in certificates.iter().enumerate() { verified[i] = self.verify_certificate(rng, *subject, certificate, strategy); } return verified; } // Iterative bisection: try the full range first. If batch verification // passes, mark the entire range valid. If it fails, split in half and // retry each half. Singletons that fail remain false. // // [0..8) fail // / \ // [0..4) pass [4..8) fail // / \ // [4..6) pass [6..8) fail // / \ // [6..7) pass [7..8) fail let mut stack = vec![(0, len)]; while let Some((start, end)) = stack.pop() { let (first, rest) = certificates[start..end] .split_first() .expect("bisection ranges are non-empty"); let certificates = NonEmpty::new(*first, rest.iter().copied()); if self.verify_certificates(rng, certificates, strategy) { verified[start..end].fill(true); } else if end - start > 1 { let mid = start + (end - start) / 2; stack.push((mid, end)); stack.push((start, mid)); } } verified } /// Returns whether this scheme benefits from batch verification. /// /// Schemes that benefit from batch verification should return `true`, allowing callers to /// optimize by deferring verification until multiple signatures are available. /// /// Schemes that don't benefit from batch verification (like [`secp256r1`]) should /// return `false`, indicating that eager per-signature verification is preferred. fn is_batchable() -> bool; /// Returns an upper bound on the encoded size of a certificate that decodes under /// [`Self::certificate_codec_config`] for a verifier over at most `participants` participants. fn max_certificate_size(participants: u32) -> usize; /// Encoding configuration for bounded-size certificate decoding used in network payloads. fn certificate_codec_config(&self) -> ::Cfg; /// Encoding configuration that allows unbounded certificate decoding. /// /// Only use this when decoding data from trusted local storage, it must not be exposed to /// adversarial inputs or network payloads. fn certificate_codec_config_unbounded() -> ::Cfg; } /// Cryptographic surface for multi-party certificate schemes. /// /// A `Scheme` extends [`Verifier`] with the signing surface: it produces attestations, validates /// them (individually or in batches), and assembles certificates. Implementations may override the /// provided defaults to take advantage of scheme-specific batching strategies. pub trait Scheme: Verifier { /// Signature emitted by individual participants. type Signature: Clone + Debug + Eq + Hash + Send + Sync + CodecFixed; /// Returns the scheme's own [`Membership`] in [`Self::participants`], or `None` if it /// holds no participant identity. fn me(&self) -> Option; /// Returns the ordered set of participant public identity keys managed by the scheme. fn participants(&self) -> &Set; /// Signs a subject, or returns `None` if the scheme cannot sign (a verifier or member /// instance). fn sign(&self, subject: Self::Subject<'_, D>) -> Option>; /// Verifies a single attestation against the participant material managed by the scheme. fn verify_attestation( &self, rng: &mut R, subject: Self::Subject<'_, D>, attestation: &Attestation, strategy: &impl Strategy, ) -> bool where R: CryptoRng, D: Digest; /// Batch-verifies attestations and separates valid attestations from signer indices that failed /// verification. Empty input produces an empty result in both sets. /// /// Callers must not include duplicate attestations from the same signer: duplicates may /// produce incorrect results. fn verify_attestations( &self, rng: &mut R, subject: Self::Subject<'_, D>, attestations: I, strategy: &impl Strategy, ) -> Verification where R: CryptoRng, D: Digest, I: IntoIterator>, I::IntoIter: Send, { let mut invalid = BTreeSet::new(); let verified = attestations.into_iter().filter_map(|attestation| { if self.verify_attestation(&mut *rng, subject.clone(), &attestation, strategy) { Some(attestation) } else { invalid.insert(attestation.signer); None } }); Verification::new(verified.collect(), invalid.into_iter().collect()) } /// Attempts to construct an authenticated certificate from pending and verified attestations. /// /// `verified` must already be valid for this exact scheme configuration and `subject`. /// Signers must be unique across both inputs. Only pending attestations undergo fallback /// verification. /// /// `Ok` authenticates the certificate for `subject`, but does not guarantee that each pending /// attestation was individually verified. Callers requiring individual correctness should use /// [`Self::verify_attestations`] followed by [`Self::assemble`]. /// /// On failure, `Err` contains the verification results for `pending`. Accepted attestations /// can be retained alongside `verified` for subsequent assembly, and may already form a quorum. fn optimistic_assemble<'a, R, D, I, J>( &self, rng: &mut R, subject: Self::Subject<'_, D>, pending: I, verified: J, strategy: &impl Strategy, ) -> Result> where R: CryptoRng, D: Digest, I: IntoIterator>, I::IntoIter: ExactSizeIterator + Send, J: IntoIterator>, J::IntoIter: Send; /// Assembles a non-empty stream of attestations into a candidate certificate. /// /// Inputs may be unverified. `Ok` does not authenticate them or the resulting certificate. /// Callers using such inputs must call [`Verifier::verify_certificate`] for the intended subject. /// /// Insufficient input returns [`AssemblyError::InsufficientAttestations`]. /// A signer-unique quorum already verified for one subject must assemble successfully into a /// certificate that verifies for that subject. fn assemble( &self, attestations: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send; /// Returns whether per-participant fault evidence can be safely exposed. /// /// Schemes where individual signatures can be safely reported as fault evidence should /// return `true`. fn is_attributable() -> bool; } /// Verifies pending attestations before assembling them with verified attestations. /// /// Inputs must satisfy [`Scheme::optimistic_assemble`]'s requirements. /// Rejected attestations return `Err` even if the remaining attestations could certify, /// preserving fault evidence for the caller. Assembly failure also returns the pending results. pub fn verify_then_assemble<'a, S, R, D, I, J>( scheme: &S, rng: &mut R, subject: S::Subject<'_, D>, pending: I, verified: J, strategy: &impl Strategy, ) -> Result> where S: Scheme, R: CryptoRng, D: Digest, I: IntoIterator>, I::IntoIter: Send, J: IntoIterator>, J::IntoIter: Send, { // Empty input already has a known verification result. Preserve rejected signer // evidence for non-empty input even when the accepted attestations could certify. let result = NonEmpty::try_new(pending.into_iter()).map_or_else( || Verification::new(Vec::new(), Vec::new()), |pending| scheme.verify_attestations::<_, D, _>(rng, subject, pending, strategy), ); if !result.invalid.is_empty() { return Err(result); } // A verified quorum needs no further authentication. Keep the pending results // available if assembly fails. let Some(attestations) = NonEmpty::try_new( result .verified .iter() .cloned() .chain(verified.into_iter().cloned()), ) else { return Err(result); }; scheme.assemble(attestations, strategy).map_err(|_| result) } /// Authenticates a candidate certificate first, verifying pending attestations only on failure. /// /// Implements [`Scheme::optimistic_assemble`]'s input and result contract. A quorum consisting /// entirely of verified attestations is assembled without another certificate check. pub fn optimistic_assemble<'a, S, R, D, I, J>( scheme: &S, rng: &mut R, subject: S::Subject<'_, D>, pending: I, verified: J, strategy: &impl Strategy, ) -> Result> where S: Scheme, R: CryptoRng, D: Digest, I: IntoIterator>, I::IntoIter: ExactSizeIterator + Send, J: IntoIterator>, J::IntoIter: Send, { let pending = pending.into_iter(); let verified = verified.into_iter(); // A verified-only quorum needs no further authentication. if pending.len() == 0 { return verify_then_assemble::<_, _, D, _, _>( scheme, rng, subject, pending, verified, strategy, ); } // Decode through the strategy before cloning so assembly and fallback reuse // the decoded values. let mut pending = strategy.map_collect_vec(pending, |attestation| { let _ = attestation.signature.get(); attestation }); let attestations = pending.iter().cloned().chain(verified.cloned()); if let Some(attestations) = NonEmpty::try_new(attestations) && let Ok(certificate) = scheme.assemble(attestations, strategy) && scheme.verify_certificate::<_, D>(rng, subject.clone(), &certificate, strategy) { return Ok(certificate); } // A single pending attestation is the leaf of the fallback search. if pending.len() == 1 { return Err(scheme.verify_attestations::<_, D, _>(rng, subject, pending, strategy)); } // Each half needs ordinary verification, including every signed component // of schemes with more than one signature per attestation. Independent RNGs // allow both halves to run concurrently without sharing the caller's RNG. let rest = pending.split_off(pending.len().div_ceil(2)); let mut first_rng = ChaCha20Rng::from_rng(&mut *rng); let mut rest_rng = ChaCha20Rng::from_rng(rng); let first_subject = subject.clone(); let (mut result, rest) = strategy.join( || scheme.verify_attestations::<_, D, _>(&mut first_rng, first_subject, pending, strategy), || scheme.verify_attestations::<_, D, _>(&mut rest_rng, subject, rest, strategy), ); result.verified.extend(rest.verified); result.invalid.extend(rest.invalid); Err(result) } /// A scheme handle returned by a [`Provider`] for one scope. /// /// Always usable as a [`Verifier`] for certificate verification. The full signing scheme is /// recoverable with [`Scoped::into_scheme`] only when the scope was built with [`Scoped::scheme`]. /// A scope built with [`Scoped::verifier`] yields `None`. #[derive(Clone, Debug)] pub struct Scoped { scheme: Arc, can_sign: bool, } impl Scoped { /// Builds a verify-only scope. pub const fn verifier(scheme: Arc) -> Self { Self { scheme, can_sign: false, } } /// Builds a full signing scope. pub const fn scheme(scheme: Arc) -> Self { Self { scheme, can_sign: true, } } /// Returns the full signing scheme, or `None` for a verify-only scope. pub fn into_scheme(self) -> Option> { self.can_sign.then_some(self.scheme) } } impl Verifier for Scoped { type Subject<'a, D: Digest> = S::Subject<'a, D>; type Faults = S::Faults; type PublicKey = S::PublicKey; type Certificate = S::Certificate; fn verify_certificate( &self, rng: &mut R, subject: Self::Subject<'_, D>, certificate: &Self::Certificate, strategy: &impl Strategy, ) -> bool where R: CryptoRng, D: Digest, { self.scheme .verify_certificate(rng, subject, certificate, strategy) } fn verify_certificates<'a, R, D, I>( &self, rng: &mut R, certificates: NonEmpty, strategy: &impl Strategy, ) -> bool where R: CryptoRng, D: Digest, I: Iterator, &'a Self::Certificate)>, { self.scheme.verify_certificates(rng, certificates, strategy) } fn is_batchable() -> bool { S::is_batchable() } fn max_certificate_size(participants: u32) -> usize { S::max_certificate_size(participants) } fn certificate_codec_config(&self) -> ::Cfg { self.scheme.certificate_codec_config() } fn certificate_codec_config_unbounded() -> ::Cfg { S::certificate_codec_config_unbounded() } } /// Supplies the signing scheme for a given scope. /// /// This trait uses an associated `Scope` type, allowing implementations to work /// with any scope representation (e.g., epoch numbers, block heights, etc.). pub trait Provider: Clone + Send + Sync + 'static { /// The scope type used to look up schemes. type Scope: Clone + Send + Sync + 'static; /// The signing scheme to provide. type Scheme: Scheme; /// Return a [`Scoped`] for `scope` capable of verifying certificates produced under it. /// /// A scheme that can verify certificates from any scope without scope-specific state should /// return a verify-only [`Scoped`] (via [`Scoped::verifier`]) for every scope. A fixed group /// public key that survives committee rotation is one such case. A scheme that needs /// scope-specific verification state should return `None` once that state is unavailable. fn scoped(&self, scope: Self::Scope) -> Option>; /// Return the full signing scheme that corresponds to `scope`, if available. /// /// The default returns a scheme only when [`Provider::scoped`] yields a signing scope, so a /// verify-only scope produces `None`. Override this when the signing scheme is available even /// for scopes that [`Provider::scoped`] serves with a verify-only result. fn scheme(&self, scope: Self::Scope) -> Option> { self.scoped(scope).and_then(Scoped::into_scheme) } } /// Bitmap wrapper that tracks which participants signed a certificate. /// /// Internally, it stores bits in 1-byte chunks for compact encoding. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Signers { bitmap: BitMap<1>, } impl Signers { /// Returns the largest encoded size of a signer bitmap over at most `participants` /// participants. pub(crate) fn max_encode_size(participants: u32) -> usize { BitMap::<1>::max_encode_size(u64::from(participants)) } /// Builds [`Signers`] from an iterator of signer indices. /// /// Indices need not be sorted: some signing schemes aggregate commutatively, so ordering /// is left to the caller. pub fn new( participants: u32, signers: impl IntoIterator, ) -> Result { let mut bitmap = BitMap::zeroes(u64::from(participants)); for signer in signers.into_iter() { let index = u64::from(signer.get()); if index >= bitmap.len() { return Err(AssemblyError::UnknownSigner(signer)); } if bitmap.get(index) { return Err(AssemblyError::DuplicateSigner(signer)); } bitmap.set(index, true); } Ok(Self { bitmap }) } /// Requires at least `required` signers in an already validated set. pub(crate) fn require(self, required: u32) -> Result { let found = u32::try_from(self.count()).expect("signer count exceeds u32::MAX"); if found < required { return Err(AssemblyError::InsufficientAttestations(required, found)); } Ok(self) } /// Returns the length of the bitmap (the size of the participant set). #[allow(clippy::len_without_is_empty)] pub const fn len(&self) -> usize { self.bitmap.len() as usize } /// Returns how many participants are marked as signers. pub fn count(&self) -> usize { self.bitmap.count_ones() as usize } /// Iterates over signer indices in ascending order. pub fn iter(&self) -> impl Iterator + '_ { self.bitmap .ones_iter() .map(|index| Participant::from_usize(index as usize)) } } /// Returns an upper bound on the encoded size of a certificate with a signer bitmap over at most /// `participants` participants and one `signature_size`-byte signature per signer. pub(crate) fn max_individual_certificate_size(participants: u32, signature_size: usize) -> usize { Signers::max_encode_size(participants) + max_items_size(Widen::widen(participants), signature_size) } /// Builds [`Signers`] using the participant set as the valid signer-index range. /// /// # Panics /// /// Panics if the participant count exceeds `u32::MAX`. impl<'a, P, I> TryFrom<(&'a Set

, I)> for Signers where I: IntoIterator, { type Error = AssemblyError; fn try_from((participants, signers): (&'a Set

, I)) -> Result { let total = u32::try_from(participants.len()).expect("participant count exceeds u32::MAX"); Self::new(total, signers) } } impl Write for Signers { fn write(&self, writer: &mut impl BufMut) { self.bitmap.write(writer); } } impl EncodeSize for Signers { fn encode_size(&self) -> usize { self.bitmap.encode_size() } } impl Read for Signers { type Cfg = usize; fn read_cfg(reader: &mut impl Buf, max_participants: &usize) -> Result { let bitmap = BitMap::read_cfg(reader, &(..=*max_participants as u64).into())?; // The participant count is treated as an upper bound for decoding flexibility, e.g. one // might use `Scheme::certificate_codec_config_unbounded` for decoding certificates from // local storage. // // Exact length validation **must** be enforced at verification time by the signing schemes // against the actual participant set size. Ok(Self { bitmap }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Signers { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let participants = u.arbitrary_len::()? % 10; let signer_count = u.arbitrary_len::()?.min(participants); let signers = (0..u32::try_from(signer_count).expect("signer count exceeds u32::MAX")) .map(Participant::new) .collect::>(); Ok(Self::new(participants.try_into().unwrap(), signers) .expect("indices are unique and in range")) } } /// A scheme provider that always returns the same scheme regardless of scope. #[derive(Clone, Debug)] pub struct ConstantProvider { scheme: Arc, _scope: core::marker::PhantomData, } impl ConstantProvider { /// Creates a new provider that always returns the given scheme. pub fn new(scheme: S) -> Self { Self { scheme: Arc::new(scheme), _scope: core::marker::PhantomData, } } } impl crate::certificate::Provider for ConstantProvider { type Scope = Sc; type Scheme = S; fn scoped(&self, _: Sc) -> Option> { Some(Scoped::scheme(self.scheme.clone())) } } #[cfg(feature = "mocks")] pub mod mocks; #[cfg(test)] mod tests { use super::*; #[cfg(feature = "bls12381")] use crate::bls12381::{ dkg::feldman_desmedt as dkg, primitives::{ group::Private as BlsPrivate, ops::{compute_public, sign_proof_of_possession, verify_proof_of_possession}, sharing::Mode, variant::{MinPk, MinSig, Variant}, }, }; use crate::{ Signer as _, ed25519::{PrivateKey, Signature}, sha256::Digest as Sha256Digest, }; use commonware_codec::{Decode, Encode, varint::MAX_U32_VARINT_SIZE}; use commonware_math::algebra::Random; use commonware_parallel::Sequential; use commonware_utils::{ N3f1, TryCollect, Widen, non_empty, ordered::{Quorum as _, Set}, test_rng, }; #[cfg(feature = "bls12381")] use commonware_utils::{NZU32, ordered::BiMap, sync::Mutex}; #[cfg(feature = "bls12381")] use core::sync::atomic::{AtomicUsize, Ordering}; use ed25519_fixture::{Scheme as Ed25519Scheme, TestSubject}; #[test] fn test_max_individual_certificate_size() { // The bound covers every signature and never decreases as the committee grows. let mut previous = 0; for n in [0, 1, 7, 8, 9, 127, 128] { let signers = Signers::new(n, (0..n).map(Participant::new)).unwrap(); let signatures = vec![[0u8; Signature::SIZE]; Widen::widen(n)]; assert_eq!(Signers::max_encode_size(n), signers.encode().len()); let size = max_individual_certificate_size(n, Signature::SIZE); assert_eq!( size, signers.encode().len() + MAX_U32_VARINT_SIZE + signatures.len() * Signature::SIZE ); assert!(signers.encode().len() + signatures.encode().len() <= size); assert!(size >= previous); previous = size; } // Counts up to `u32::MAX` fit the signature length prefix. let n: usize = Widen::widen(u32::MAX); assert_eq!( max_individual_certificate_size(u32::MAX, Signature::SIZE), u64::SIZE + n.div_ceil(8) + MAX_U32_VARINT_SIZE + n * Signature::SIZE, ); } #[test] fn test_new_signers() { let signers = Signers::new(6, [0, 3, 5].map(Participant::new)).unwrap(); let collected: Vec<_> = signers.iter().collect(); assert_eq!( collected, vec![0, 3, 5] .into_iter() .map(Participant::new) .collect::>() ); assert_eq!(signers.count(), 3); } #[test] fn test_new_out_of_bounds() { assert_eq!( Signers::new(4, [0, 4].map(Participant::new)), Err(AssemblyError::UnknownSigner(Participant::new(4))) ); } #[test] fn test_new_duplicate() { assert_eq!( Signers::new(4, [0, 0, 1].map(Participant::new)), Err(AssemblyError::DuplicateSigner(Participant::new(0))) ); } #[test] fn test_new_not_increasing() { assert!(Signers::new(4, [2, 1].map(Participant::new)).is_ok()); } #[test] fn test_try_from_set_and_require() { let participants = Set::from_iter_dedup(0..4); let signers = Signers::try_from((&participants, [0, 2, 3].map(Participant::new))) .unwrap() .require(3) .unwrap(); assert_eq!(signers.count(), 3); assert_eq!( Signers::try_from((&participants, [0, 2].map(Participant::new))) .unwrap() .require(3), Err(AssemblyError::InsufficientAttestations(3, 2)) ); } #[test] fn test_try_from_set_checks_signer_bounds() { let participants = Set::::default(); let signer = Participant::new(0); assert_eq!( Signers::try_from((&participants, [signer])), Err(AssemblyError::UnknownSigner(signer)) ); } #[test] fn test_codec_round_trip() { let signers = Signers::new(9, [1, 6].map(Participant::new)).unwrap(); let encoded = signers.encode(); let decoded = Signers::decode_cfg(encoded, &9).unwrap(); assert_eq!(decoded, signers); } #[test] fn test_decode_respects_participant_limit() { let signers = Signers::new(8, [0, 3, 7].map(Participant::new)).unwrap(); let encoded = signers.encode(); // More participants than expected should fail. assert!(Signers::decode_cfg(encoded.clone(), &2).is_err()); // Exact participant bound succeeds. assert!(Signers::decode_cfg(encoded.clone(), &8).is_ok()); // Less participants than expected succeeds (upper bound). assert!(Signers::decode_cfg(encoded, &10).is_ok()); } mod ed25519_fixture { use crate::{certificate::Subject, impl_certificate_ed25519}; use commonware_utils::N3f1; /// Test subject for certificate verification tests. #[derive(Copy, Clone, Debug)] pub struct TestSubject { pub message: &'static [u8], } impl Subject for TestSubject { type Namespace = Vec; fn namespace<'a>(&self, derived: &'a Self::Namespace) -> &'a [u8] { derived } fn message(&self) -> bytes::Bytes { bytes::Bytes::from_static(self.message) } } // Use the macro to generate the test scheme impl_certificate_ed25519!(TestSubject, Vec, N3f1); } const NAMESPACE: &[u8] = b"test-bisect"; const MESSAGE: &[u8] = b"good message"; const BAD_MESSAGE: &[u8] = b"bad message"; fn make_certificate( schemes: &[Ed25519Scheme], message: &'static [u8], ) -> ::Certificate { let attestations: Vec<_> = schemes .iter() .filter_map(|s| s.sign::(TestSubject { message })) .collect(); schemes[0] .assemble(non_empty![@attestations], &Sequential) .expect("assembly failed") } fn setup_ed25519(n: u32) -> (Vec, Ed25519Scheme) { let mut rng = test_rng(); let private_keys: Vec<_> = (0..n).map(|_| PrivateKey::random(&mut rng)).collect(); let participants: Set = private_keys .iter() .map(|sk| sk.public_key()) .try_collect() .unwrap(); let signers: Vec<_> = private_keys .into_iter() .map(|sk| Ed25519Scheme::signer(NAMESPACE, participants.clone(), sk).unwrap()) .collect(); let verifier = Ed25519Scheme::verifier(NAMESPACE, participants); (signers, verifier) } /// Signers report a signing membership, members report a non-signing one at their /// identity key's position, and an identity-less verifier reports none. #[test] fn membership_reports_index_and_signing() { let (signers, verifier) = setup_ed25519(4); let participants = verifier.participants().clone(); let signer = signers[0].me().unwrap(); let index = signer.index(); let key = participants.key(index).unwrap().clone(); let member = Ed25519Scheme::member(NAMESPACE, participants, &key).unwrap(); assert_eq!(member.me(), Some(Membership::Member(index))); assert_eq!(member.me().unwrap().index(), index); assert_eq!(member.me().unwrap().signer(), None); assert_eq!(signer, Membership::Signer(index)); assert_eq!(signer.signer(), Some(index)); assert_eq!(verifier.me(), None); } #[test] fn test_bisect_empty() { let mut rng = test_rng(); let (_, verifier) = setup_ed25519(4); let result = verifier.verify_certificates_bisect::<_, Sha256Digest>(&mut rng, &[], &Sequential); assert!(result.is_empty()); } #[test] fn test_bisect_all_valid() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(4); let cert = make_certificate(&schemes, MESSAGE); let good = TestSubject { message: MESSAGE }; let pairs: Vec<_> = (0..5).map(|_| (good, &cert)).collect(); let result = verifier.verify_certificates_bisect::<_, Sha256Digest>(&mut rng, &pairs, &Sequential); assert_eq!(result, vec![true; 5]); } #[test] fn test_bisect_mixed() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(4); let cert = make_certificate(&schemes, MESSAGE); let good = TestSubject { message: MESSAGE }; let bad = TestSubject { message: BAD_MESSAGE, }; let pairs = vec![ (good, &cert), (bad, &cert), (good, &cert), (bad, &cert), (good, &cert), (good, &cert), (bad, &cert), (bad, &cert), ]; let expected = vec![true, false, true, false, true, true, false, false]; let result = verifier.verify_certificates_bisect::<_, Sha256Digest>(&mut rng, &pairs, &Sequential); assert_eq!(result, expected); } #[test] fn test_bisect_all_invalid() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(4); let cert = make_certificate(&schemes, MESSAGE); let bad = TestSubject { message: BAD_MESSAGE, }; let pairs: Vec<_> = (0..4).map(|_| (bad, &cert)).collect(); let result = verifier.verify_certificates_bisect::<_, Sha256Digest>(&mut rng, &pairs, &Sequential); assert_eq!(result, vec![false; 4]); } #[test] fn test_bisect_single_valid() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(4); let cert = make_certificate(&schemes, MESSAGE); let pairs = vec![(TestSubject { message: MESSAGE }, &cert)]; let result = verifier.verify_certificates_bisect::<_, Sha256Digest>(&mut rng, &pairs, &Sequential); assert_eq!(result, vec![true]); } #[test] fn test_bisect_single_invalid() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(4); let cert = make_certificate(&schemes, MESSAGE); let pairs = vec![( TestSubject { message: BAD_MESSAGE, }, &cert, )]; let result = verifier.verify_certificates_bisect::<_, Sha256Digest>(&mut rng, &pairs, &Sequential); assert_eq!(result, vec![false]); } #[test] fn test_scoped_verifies_and_into_scheme() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(4); let cert = make_certificate(&schemes, MESSAGE); let subject = TestSubject { message: MESSAGE }; let as_verifier = Scoped::verifier(Arc::new(verifier)); let as_scheme = Scoped::scheme(Arc::new(schemes[0].clone())); // Both scopes verify a valid certificate through the `Verifier` impl. assert!(as_verifier.verify_certificate::<_, Sha256Digest>( &mut rng, subject, &cert, &Sequential, )); assert!(as_scheme.verify_certificate::<_, Sha256Digest>( &mut rng, subject, &cert, &Sequential, )); let pairs = [(subject, &cert)]; assert!(as_verifier.verify_certificates::<_, Sha256Digest, _>( &mut rng, non_empty![@pairs.iter().copied()], &Sequential, )); assert_eq!( as Verifier>::is_batchable(), ::is_batchable(), ); assert_eq!( as_verifier.certificate_codec_config(), schemes[0].certificate_codec_config(), ); for participants in [0, 4, 128] { assert_eq!( as Verifier>::max_certificate_size(participants), ::max_certificate_size(participants), ); } let _ = as Verifier>::certificate_codec_config_unbounded(); // A verify-only scope never yields its scheme; a full scope always does. assert!(as_verifier.into_scheme().is_none()); assert!(as_scheme.into_scheme().is_some()); let provider = ConstantProvider::<_, ()>::new(schemes[0].clone()); assert!(provider.scoped(()).is_some()); assert!(provider.scheme(()).is_some()); } #[cfg(feature = "bls12381")] #[allow(dead_code)] mod threshold { use super::TestSubject; use crate::impl_certificate_bls12381_threshold; use commonware_utils::N3f1; impl_certificate_bls12381_threshold!(TestSubject, Vec, N3f1); } #[cfg(feature = "bls12381")] #[allow(dead_code)] mod multisig { use super::TestSubject; use crate::impl_certificate_bls12381_multisig; use commonware_utils::N3f1; impl_certificate_bls12381_multisig!(TestSubject, Vec, N3f1); } const SUBJECT: TestSubject = TestSubject { message: b"subject", }; const OTHER_SUBJECT: TestSubject = TestSubject { message: b"other-subject", }; #[cfg(feature = "bls12381")] const THRESHOLD_NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_CERTIFICATE_THRESHOLD_OPTIMISTIC_ASSEMBLE"; #[cfg(feature = "bls12381")] const MULTISIG_NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_CERTIFICATE_MULTISIG_OPTIMISTIC_ASSEMBLE"; #[cfg(feature = "bls12381")] const MULTISIG_POP_NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_CERTIFICATE_MULTISIG_OPTIMISTIC_ASSEMBLE_POP"; #[cfg(feature = "bls12381")] fn threshold_signers( rng: &mut impl CryptoRng, n: u32, ) -> Vec> { let participants: Set = (0..n) .map(|_| PrivateKey::random(&mut *rng).public_key()) .try_collect() .unwrap(); let (polynomial, shares) = dkg::deal_anonymous::(&mut *rng, Mode::NonZeroCounter, NZU32!(n)); shares .into_iter() .map(|share| { threshold::Scheme::signer( THRESHOLD_NAMESPACE, participants.clone(), polynomial.clone(), share, ) .unwrap() }) .collect() } #[cfg(feature = "bls12381")] fn multisig_signers( rng: &mut impl CryptoRng, n: u32, ) -> Vec> { let identity_keys: Vec<_> = (0..n).map(|_| PrivateKey::random(&mut *rng)).collect(); let signing_keys: Vec<_> = (0..n).map(|_| BlsPrivate::random(&mut *rng)).collect(); let signing_publics: Vec<_> = signing_keys.iter().map(compute_public::).collect(); for (private, public) in signing_keys.iter().zip(&signing_publics) { let proof = sign_proof_of_possession::(private, MULTISIG_POP_NAMESPACE); verify_proof_of_possession::(public, MULTISIG_POP_NAMESPACE, &proof) .expect("proof of possession must verify"); } let participants: BiMap<_, _> = identity_keys .iter() .map(|key| key.public_key()) .zip(signing_publics) .try_collect() .unwrap(); signing_keys .into_iter() .map(|private| { multisig::Scheme::signer(MULTISIG_NAMESPACE, participants.clone(), private) .expect("signer must be a participant") }) .collect() } #[cfg(feature = "bls12381")] fn threshold_success() { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, 5); let quorum = N3f1::quorum(schemes.len()) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| scheme.sign::(SUBJECT).unwrap()) .collect(); let expected = schemes[0] .assemble(non_empty![@attestations.clone()], &Sequential) .unwrap(); let prior: Vec<_> = attestations[..1] .iter() .cloned() .map(Recording::outer) .collect(); let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, attestations[1..].iter().cloned().map(Recording::outer), &prior, &Sequential, ); let certificate = result.ok().expect("valid quorum must certify"); assert_eq!(certificate, expected); assert!(calls.attestations.lock().is_empty()); assert!(schemes[0].verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); assert!(!schemes[0].verify_certificate::<_, Sha256Digest>( &mut rng, OTHER_SUBJECT, &certificate, &Sequential, )); // Threshold recovery uses the lowest quorum of signer indices. let unused_bad = schemes[quorum].sign::(OTHER_SUBJECT).unwrap(); let pending: Vec<_> = attestations .into_iter() .chain([unused_bad]) .map(Recording::outer) .collect(); let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ); assert_eq!(result.ok(), Some(expected)); assert!(calls.attestations.lock().is_empty()); } #[cfg(feature = "bls12381")] #[test] fn test_threshold_recovers_exact_certificate_with_verified_attestations() { threshold_success::(); threshold_success::(); } #[cfg(feature = "bls12381")] fn multisig_optimistic_assemble_uses_one_certificate_check() { let mut rng = test_rng(); let schemes = multisig_signers::(&mut rng, 5); let quorum = N3f1::quorum(schemes.len()) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|scheme| scheme.sign::(SUBJECT).unwrap()) .collect(); let prior: Vec<_> = attestations[..1] .iter() .cloned() .map(Recording::outer) .collect(); let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let certificate = verifier .optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, attestations[1..].iter().cloned().map(Recording::outer), &prior, &Sequential, ) .ok() .expect("valid mixed quorum must certify"); assert_eq!(calls.certificates.load(Ordering::Relaxed), 1); assert!(calls.attestations.lock().is_empty()); assert!(schemes[0].verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); assert!(!schemes[0].verify_certificate::<_, Sha256Digest>( &mut rng, OTHER_SUBJECT, &certificate, &Sequential, )); } #[cfg(feature = "bls12381")] #[test] fn test_multisig_optimistic_assemble_uses_one_certificate_check() { multisig_optimistic_assemble_uses_one_certificate_check::(); multisig_optimistic_assemble_uses_one_certificate_check::(); } #[test] fn test_optimistic_assemble_individual_inputs() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(5); let empty = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, Vec::new(), std::iter::empty(), &Sequential, ); let verification = empty.unwrap_err(); assert!(verification.verified.is_empty()); assert!(verification.invalid.is_empty()); let pending: Vec<_> = schemes .iter() .map(|scheme| scheme.sign::(SUBJECT).unwrap()) .collect(); let certificate = verifier .optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ) .ok() .expect("valid quorum must certify"); assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); let pending: Vec<_> = schemes .iter() .enumerate() .map(|(i, scheme)| { scheme .sign::(if i == 0 { OTHER_SUBJECT } else { SUBJECT }) .unwrap() }) .collect(); let signers: Vec<_> = pending.iter().map(|a| a.signer).collect(); let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ); let verification = result.unwrap_err(); assert_eq!(verification.invalid, vec![signers[0]]); assert_eq!( verification .verified .iter() .map(|a| a.signer) .collect::>(), signers[1..] ); let certificate = verifier .assemble(non_empty![@verification.verified], &Sequential) .unwrap(); assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); } #[test] fn test_optimistic_assemble_individual_quorum() { let mut rng = test_rng(); let (schemes, verifier) = setup_ed25519(5); let quorum = N3f1::quorum(schemes.len()) as usize; let attestations: Vec<_> = schemes .iter() .take(quorum) .map(|s| s.sign::(SUBJECT).unwrap()) .collect(); // Both pending-only and verified-only inputs require quorum. On failure, // the result contains only pending attestations, never retained inputs. for count in [0, quorum - 1, quorum] { for pending in [0, count] { let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, attestations[..pending].iter().cloned(), &attestations[pending..count], &Sequential, ); assert_eq!(result.is_ok(), count == quorum); match result { Ok(certificate) => assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )), Err(verification) => { assert_eq!(verification.verified, attestations[..pending]); assert!(verification.invalid.is_empty()); } } } } } #[test] fn test_optimistic_assemble_empty_committee() { let mut rng = test_rng(); let (_, verifier) = setup_ed25519(0); let (schemes, _) = setup_ed25519(1); let unknown = schemes[0].sign::(SUBJECT).unwrap(); for pending in [Vec::new(), vec![unknown]] { let expected: Vec<_> = pending.iter().map(|a| a.signer).collect(); let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ); let verification = result.unwrap_err(); assert!(verification.verified.is_empty()); assert_eq!(verification.invalid, expected); } } #[cfg(feature = "bls12381")] #[derive(Debug, Default)] struct RecordingCalls { attestations: Mutex>>, assemblies: AtomicUsize, certificates: AtomicUsize, } #[cfg(feature = "bls12381")] #[derive(Clone, Debug)] struct Recording { inner: S, calls: Arc, } #[cfg(feature = "bls12381")] impl Recording { fn inner(attestation: Attestation) -> Attestation { Attestation { signer: attestation.signer, signature: attestation.signature, } } fn outer(attestation: Attestation) -> Attestation { Attestation { signer: attestation.signer, signature: attestation.signature, } } } #[cfg(feature = "bls12381")] impl Verifier for Recording { type Subject<'a, D: Digest> = S::Subject<'a, D>; type Faults = S::Faults; type PublicKey = S::PublicKey; type Certificate = S::Certificate; fn verify_certificate( &self, rng: &mut R, subject: Self::Subject<'_, D>, certificate: &Self::Certificate, strategy: &impl Strategy, ) -> bool where R: CryptoRng, D: Digest, { self.calls.certificates.fetch_add(1, Ordering::Relaxed); self.inner .verify_certificate(rng, subject, certificate, strategy) } fn is_batchable() -> bool { S::is_batchable() } fn max_certificate_size(participants: u32) -> usize { S::max_certificate_size(participants) } fn certificate_codec_config(&self) -> ::Cfg { self.inner.certificate_codec_config() } fn certificate_codec_config_unbounded() -> ::Cfg { S::certificate_codec_config_unbounded() } } #[cfg(feature = "bls12381")] impl Scheme for Recording { type Signature = S::Signature; fn me(&self) -> Option { self.inner.me() } fn participants(&self) -> &Set { self.inner.participants() } fn sign(&self, subject: Self::Subject<'_, D>) -> Option> { self.inner.sign(subject).map(Self::outer) } fn verify_attestation( &self, rng: &mut R, subject: Self::Subject<'_, D>, attestation: &Attestation, strategy: &impl Strategy, ) -> bool where R: CryptoRng, D: Digest, { self.calls .attestations .lock() .push(vec![attestation.signer]); self.inner .verify_attestation(rng, subject, &Self::inner(attestation.clone()), strategy) } fn verify_attestations( &self, rng: &mut R, subject: Self::Subject<'_, D>, attestations: I, strategy: &impl Strategy, ) -> Verification where R: CryptoRng, D: Digest, I: IntoIterator>, I::IntoIter: Send, { let attestations: Vec<_> = attestations.into_iter().collect(); self.calls .attestations .lock() .push(attestations.iter().map(|a| a.signer).collect()); let result = self.inner.verify_attestations( rng, subject, attestations.into_iter().map(Self::inner), strategy, ); Verification::new( result.verified.into_iter().map(Self::outer).collect(), result.invalid, ) } fn optimistic_assemble<'a, R, D, I, J>( &self, rng: &mut R, subject: Self::Subject<'_, D>, pending: I, verified: J, strategy: &impl Strategy, ) -> Result> where R: CryptoRng, D: 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: NonEmpty, strategy: &impl Strategy, ) -> Result where I: Iterator> + Send, { self.calls.assemblies.fetch_add(1, Ordering::Relaxed); self.inner.assemble( non_empty![@attestations.into_iter().map(Self::inner)], strategy, ) } fn is_attributable() -> bool { S::is_attributable() } } #[cfg(feature = "bls12381")] fn expected_halves(len: usize) -> Vec> { let middle = len.div_ceil(2); let first = (0..middle).map(Participant::from_usize).collect(); if middle == len { vec![first] } else { vec![first, (middle..len).map(Participant::from_usize).collect()] } } #[cfg(feature = "bls12381")] #[test] fn test_rejected_certificate_verifies_pending_halves_directly() { for n in [1, 5, 7] { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, n); let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let quorum = N3f1::quorum(n) as usize; let pending: Vec<_> = schemes .iter() .take(quorum) .map(|s| Recording::outer(s.sign::(OTHER_SUBJECT).unwrap())) .collect(); let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ); let verification = result.unwrap_err(); assert!(verification.verified.is_empty()); assert_eq!( verification.invalid, (0..quorum).map(Participant::from_usize).collect::>() ); assert_eq!(*calls.attestations.lock(), expected_halves(quorum)); } } #[cfg(feature = "bls12381")] #[test] fn test_malformed_assembly_falls_back_to_pending_halves() { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, 5); let quorum = N3f1::quorum(schemes.len()) as usize; for count in [quorum, schemes.len()] { let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let mut pending: Vec<_> = schemes .iter() .take(count) .map(|s| Recording::outer(s.sign::(SUBJECT).unwrap())) .collect(); let mut malformed = Bytes::from_static(&[0]); pending[0].signature = Lazy::deferred(&mut malformed, ()); let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ); let verification = result.unwrap_err(); assert_eq!(verification.verified.len(), count - 1); assert_eq!(verification.invalid, vec![Participant::new(0)]); assert_eq!(*calls.attestations.lock(), expected_halves(count)); if count > quorum { let certificate = verifier .assemble(non_empty![@verification.verified], &Sequential) .unwrap(); assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); } } } #[cfg(feature = "bls12381")] #[test] fn test_optimistic_assemble_retains_verified_inputs() { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, 5); let mut prior: Vec<_> = schemes[..2] .iter() .map(|s| Recording::outer(s.sign::(SUBJECT).unwrap())) .collect(); let accepted = Recording::outer(schemes[2].sign::(SUBJECT).unwrap()); let rejected = Recording::outer(schemes[3].sign::(OTHER_SUBJECT).unwrap()); let invalid = rejected.signer; let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, vec![accepted.clone(), rejected], &prior, &Sequential, ); let verification = result.unwrap_err(); assert_eq!(verification.verified, vec![accepted.clone()]); assert_eq!(verification.invalid, vec![invalid]); assert_eq!( *calls.attestations.lock(), vec![vec![accepted.signer], vec![invalid]] ); prior.extend(verification.verified); calls.attestations.lock().clear(); let replacement = Recording::outer(schemes[4].sign::(SUBJECT).unwrap()); let signer = replacement.signer; let verification = verifier.verify_attestations::<_, Sha256Digest, _>( &mut rng, SUBJECT, vec![replacement.clone()], &Sequential, ); assert_eq!(verification.verified, vec![replacement]); assert!(verification.invalid.is_empty()); assert_eq!(*calls.attestations.lock(), vec![vec![signer]]); prior.extend(verification.verified); let certificate = verifier.assemble(non_empty![@prior], &Sequential).unwrap(); assert!(verifier.verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); } #[cfg(feature = "bls12381")] #[test] fn test_optimistic_assemble_with_verified_votes() { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, 5); let quorum = N3f1::quorum(schemes.len()) as usize; let attestations: Vec<_> = schemes[..quorum] .iter() .map(|scheme| scheme.sign::(SUBJECT).unwrap()) .collect(); for pending_len in [0, 1, 2] { // Retained votes are authenticated for this configuration and subject. // The remaining votes cover empty, singleton, and evenly split inputs. let (prior, pending) = attestations.split_at(quorum - pending_len); let prior = schemes[0].verify_attestations::<_, Sha256Digest, _>( &mut rng, SUBJECT, prior.iter().cloned(), &Sequential, ); assert!(prior.invalid.is_empty()); let prior: Vec<_> = prior.verified.into_iter().map(Recording::outer).collect(); let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let certificate = verifier .optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending.iter().cloned().map(Recording::outer), &prior, &Sequential, ) .ok() .expect("verified quorum must certify"); // Pending votes require certificate authentication, even when most // inputs are already verified. A verified-only quorum needs assembly alone. assert_eq!(calls.assemblies.load(Ordering::Relaxed), 1); assert_eq!( calls.certificates.load(Ordering::Relaxed), usize::from(pending_len != 0) ); assert!(calls.attestations.lock().is_empty()); assert!(schemes[0].verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); } } #[cfg(feature = "bls12381")] #[test] fn test_optimistic_assemble_retries_with_replacements() { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, 10); let quorum = N3f1::quorum(schemes.len()) as usize; for failures in [1, 2] { let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; // Ten participants permit three faulty signers across these attempts. // A failed initial quorum retains the local vote and its valid peer votes. let own = schemes[0].verify_attestations::<_, Sha256Digest, _>( &mut rng, SUBJECT, [schemes[0].sign::(SUBJECT).unwrap()], &Sequential, ); assert!(own.invalid.is_empty()); let mut prior: Vec<_> = own.verified.into_iter().map(Recording::outer).collect(); let pending: Vec<_> = schemes[1..quorum] .iter() .enumerate() .map(|(index, scheme)| { let subject = if index + 1 >= quorum - failures { OTHER_SUBJECT } else { SUBJECT }; Recording::outer(scheme.sign::(subject).unwrap()) }) .collect(); let result = verifier .optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, &prior, &Sequential, ) .unwrap_err(); assert_eq!(result.invalid.len(), failures); prior.extend(result.verified); assert_eq!(prior.len(), quorum - failures); // A new faulty signer is mixed with any honest replacements. The // candidate fails authentication, so fallback identifies the bad vote. let pending: Vec<_> = schemes[quorum..quorum + failures] .iter() .enumerate() .map(|(index, scheme)| { let subject = if index + 1 == failures { OTHER_SUBJECT } else { SUBJECT }; Recording::outer(scheme.sign::(subject).unwrap()) }) .collect(); let signers: Vec<_> = pending.iter().map(|vote| vote.signer).collect(); let invalid = *signers.last().unwrap(); calls.attestations.lock().clear(); calls.assemblies.store(0, Ordering::Relaxed); calls.certificates.store(0, Ordering::Relaxed); let result = verifier .optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, &prior, &Sequential, ) .unwrap_err(); assert_eq!(result.invalid, vec![invalid]); assert_eq!(result.verified.len(), failures - 1); assert_eq!(calls.assemblies.load(Ordering::Relaxed), 1); assert_eq!(calls.certificates.load(Ordering::Relaxed), 1); assert_eq!( *calls.attestations.lock(), signers .into_iter() .map(|signer| vec![signer]) .collect::>() ); prior.extend(result.verified); assert_eq!(prior.len(), quorum - 1); // The last honest replacement completes the same authenticated subject. let replacement = Recording::outer( schemes[quorum + failures] .sign::(SUBJECT) .unwrap(), ); calls.attestations.lock().clear(); calls.assemblies.store(0, Ordering::Relaxed); calls.certificates.store(0, Ordering::Relaxed); let certificate = verifier .optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, [replacement], &prior, &Sequential, ) .ok() .expect("valid replacement must certify"); assert_eq!(calls.assemblies.load(Ordering::Relaxed), 1); assert_eq!(calls.certificates.load(Ordering::Relaxed), 1); assert!(calls.attestations.lock().is_empty()); assert!(schemes[0].verify_certificate::<_, Sha256Digest>( &mut rng, SUBJECT, &certificate, &Sequential, )); assert!(!schemes[0].verify_certificate::<_, Sha256Digest>( &mut rng, OTHER_SUBJECT, &certificate, &Sequential, )); } } #[cfg(feature = "bls12381")] #[test] fn test_optimistic_assemble_below_quorum() { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, 5); let pending: Vec<_> = schemes[..2] .iter() .map(|s| Recording::outer(s.sign::(SUBJECT).unwrap())) .collect(); let expected = pending.clone(); let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ); let verification = result.unwrap_err(); assert_eq!(verification.verified, expected); assert!(verification.invalid.is_empty()); assert_eq!(*calls.attestations.lock(), expected_halves(expected.len())); } #[cfg(feature = "bls12381")] #[test] fn test_optimistic_assemble_unknown_pending_signer() { let mut rng = test_rng(); let schemes = threshold_signers::(&mut rng, 5); let quorum = N3f1::quorum(schemes.len()) as usize; let mut pending: Vec<_> = schemes .iter() .take(quorum) .map(|s| Recording::outer(s.sign::(SUBJECT).unwrap())) .collect(); pending[0].signer = Participant::new(999); let calls = Arc::new(RecordingCalls::default()); let verifier = Recording { inner: schemes[0].clone(), calls: Arc::clone(&calls), }; let result = verifier.optimistic_assemble::<_, Sha256Digest, _, _>( &mut rng, SUBJECT, pending, std::iter::empty(), &Sequential, ); let verification = result.unwrap_err(); assert_eq!(verification.verified.len(), quorum - 1); assert_eq!(verification.invalid, vec![Participant::new(999)]); assert_eq!( *calls.attestations.lock(), vec![ vec![Participant::new(999), Participant::new(1)], vec![Participant::new(2), Participant::new(3)], ] ); } #[cfg(feature = "arbitrary")] mod conformance { use super::{ed25519_fixture::Scheme, *}; use commonware_codec::conformance::CodecConformance; commonware_conformance::conformance_tests! { CodecConformance, CodecConformance>, } } }