commonware_cryptography/certificate.rs
1//! Cryptographic primitives for generating and verifying certificates.
2//!
3//! This module provides the [`Scheme`] trait and implementations for producing
4//! signatures, validating them (individually or in batches), assembling
5//! certificates, and verifying recovered certificates.
6//!
7//! # Pluggable Cryptography
8//!
9//! Certificates are generic over the signing scheme, allowing users to choose
10//! the scheme best suited for their requirements:
11//!
12//! - [`ed25519`]: Attributable signatures with individual verification. HSM-friendly, no trusted
13//! setup required, and widely supported. Certificates contain individual signatures from each
14//! signer.
15//!
16//! - [`bls12381_multisig`]: Attributable signatures with aggregated verification. Signatures
17//! can be aggregated into a single multi-signature for compact certificates while preserving
18//! attribution (signer indices are stored alongside the aggregated signature).
19//!
20//! - [`bls12381_threshold`]: Non-attributable threshold signatures. Produces succinct
21//! certificates that are constant-size regardless of committee size. Requires a trusted
22//! setup (distributed key generation) and cannot attribute signatures to individual signers.
23//!
24//! # Attributable Schemes and Fault Evidence
25//!
26//! Signing schemes differ in whether per-participant activities can be used as evidence of
27//! either liveness or of committing a fault:
28//!
29//! - **Attributable Schemes** ([`ed25519`], [`bls12381_multisig`]): Individual signatures can be
30//! presented to some third party as evidence of either liveness or of committing a fault.
31//! Certificates contain signer indices alongside individual signatures, enabling secure
32//! per-participant activity tracking and conflict detection.
33//!
34//! - **Non-Attributable Schemes** ([`bls12381_threshold`]): Individual signatures cannot be
35//! presented to some third party as evidence of either liveness or of committing a fault
36//! because they can be forged by other players (often after some quorum of partial signatures
37//! are collected). With [`bls12381_threshold`], possession of any `t` valid partial signatures
38//! can be used to forge a partial signature for any other player. Because peer connections are
39//! authenticated, evidence can be used locally (as it must be sent by said participant) but
40//! cannot be used by an external observer.
41//!
42//! The [`Scheme::is_attributable()`] method signals whether evidence can be safely exposed to
43//! third parties.
44//!
45//! # Identity Keys vs Signing Keys
46//!
47//! A participant may supply both an identity key and a signing key. The identity key
48//! is used for assigning a unique order to the participant set and authenticating connections
49//! whereas the signing key is used for producing and verifying signatures/certificates.
50//!
51//! This flexibility is supported because some cryptographic schemes are only performant when
52//! used in batch verification (like [bls12381_multisig]) and/or are refreshed frequently
53//! (like [bls12381_threshold]). Refer to [ed25519] for an example of a scheme that uses the
54//! same key for both purposes.
55
56pub use crate::{
57 bls12381::certificate::{multisig as bls12381_multisig, threshold as bls12381_threshold},
58 ed25519::certificate as ed25519,
59 impl_certificate_bls12381_multisig, impl_certificate_bls12381_threshold,
60 impl_certificate_ed25519,
61};
62use crate::{Digest, PublicKey};
63#[cfg(not(feature = "std"))]
64use alloc::{collections::BTreeSet, sync::Arc, vec::Vec};
65use bytes::{Buf, BufMut, Bytes};
66use commonware_codec::{varint::UInt, Codec, CodecFixed, EncodeSize, Error, Read, ReadExt, Write};
67use commonware_utils::{bitmap::BitMap, ordered::Set};
68use core::{fmt::Debug, hash::Hash};
69use rand::{CryptoRng, Rng};
70#[cfg(feature = "std")]
71use std::{collections::BTreeSet, sync::Arc, vec::Vec};
72
73/// A participant's attestation for a certificate.
74#[derive(Clone, Debug)]
75pub struct Attestation<S: Scheme> {
76 /// Index of the signer inside the participant set.
77 pub signer: u32,
78 /// Scheme-specific signature or share produced for a given subject.
79 pub signature: S::Signature,
80}
81
82impl<S: Scheme> PartialEq for Attestation<S> {
83 fn eq(&self, other: &Self) -> bool {
84 self.signer == other.signer && self.signature == other.signature
85 }
86}
87
88impl<S: Scheme> Eq for Attestation<S> {}
89
90impl<S: Scheme> Hash for Attestation<S> {
91 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
92 self.signer.hash(state);
93 self.signature.hash(state);
94 }
95}
96
97impl<S: Scheme> Write for Attestation<S> {
98 fn write(&self, writer: &mut impl BufMut) {
99 UInt(self.signer).write(writer);
100 self.signature.write(writer);
101 }
102}
103
104impl<S: Scheme> EncodeSize for Attestation<S> {
105 fn encode_size(&self) -> usize {
106 UInt(self.signer).encode_size() + self.signature.encode_size()
107 }
108}
109
110impl<S: Scheme> Read for Attestation<S> {
111 type Cfg = ();
112
113 fn read_cfg(reader: &mut impl Buf, _: &()) -> Result<Self, Error> {
114 let signer = UInt::read(reader)?.into();
115 let signature = S::Signature::read(reader)?;
116
117 Ok(Self { signer, signature })
118 }
119}
120
121#[cfg(feature = "arbitrary")]
122impl<S: Scheme> arbitrary::Arbitrary<'_> for Attestation<S>
123where
124 S::Signature: for<'a> arbitrary::Arbitrary<'a>,
125{
126 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
127 let signer = u32::arbitrary(u)?;
128 let signature = S::Signature::arbitrary(u)?;
129 Ok(Self { signer, signature })
130 }
131}
132
133/// Result of batch-verifying attestations.
134pub struct Verification<S: Scheme> {
135 /// Contains the attestations accepted by the scheme.
136 pub verified: Vec<Attestation<S>>,
137 /// Identifies the participant indices rejected during batch verification.
138 pub invalid: Vec<u32>,
139}
140
141impl<S: Scheme> Verification<S> {
142 /// Creates a new `Verification` result.
143 pub const fn new(verified: Vec<Attestation<S>>, invalid: Vec<u32>) -> Self {
144 Self { verified, invalid }
145 }
146}
147
148/// Identifies the subject of a signature or certificate.
149pub trait Subject: Clone + Debug + Send + Sync {
150 /// Returns the namespace and message for the subject, given some base namespace.
151 fn namespace_and_message(&self, namespace: &[u8]) -> (Bytes, Bytes);
152}
153
154/// Cryptographic surface for multi-party certificate schemes.
155///
156/// A `Scheme` produces attestations, validates them (individually or in batches), assembles
157/// certificates, and verifies recovered certificates. Implementations may override the
158/// provided defaults to take advantage of scheme-specific batching strategies.
159pub trait Scheme: Clone + Debug + Send + Sync + 'static {
160 /// Subject type for signing and verification.
161 type Subject<'a, D: Digest>: Subject;
162
163 /// Public key type for participant identity used to order and index the participant set.
164 type PublicKey: PublicKey;
165 /// Signature emitted by individual participants.
166 type Signature: Clone + Debug + PartialEq + Eq + Hash + Send + Sync + CodecFixed<Cfg = ()>;
167 /// Certificate assembled from a set of attestations.
168 type Certificate: Clone + Debug + PartialEq + Eq + Hash + Send + Sync + Codec;
169
170 /// Returns the index of "self" in the participant set, if available.
171 /// Returns `None` if the scheme is a verifier-only instance.
172 fn me(&self) -> Option<u32>;
173
174 /// Returns the ordered set of participant public identity keys managed by the scheme.
175 fn participants(&self) -> &Set<Self::PublicKey>;
176
177 /// Signs a subject using the supplied namespace for domain separation.
178 /// Returns `None` if the scheme cannot sign (e.g. it's a verifier-only instance).
179 fn sign<D: Digest>(
180 &self,
181 namespace: &[u8],
182 subject: Self::Subject<'_, D>,
183 ) -> Option<Attestation<Self>>;
184
185 /// Verifies a single attestation against the participant material managed by the scheme.
186 fn verify_attestation<D: Digest>(
187 &self,
188 namespace: &[u8],
189 subject: Self::Subject<'_, D>,
190 attestation: &Attestation<Self>,
191 ) -> bool;
192
193 /// Batch-verifies attestations and separates valid attestations from signer indices that failed
194 /// verification.
195 ///
196 /// Callers must not include duplicate attestations from the same signer.
197 fn verify_attestations<R, D, I>(
198 &self,
199 _rng: &mut R,
200 namespace: &[u8],
201 subject: Self::Subject<'_, D>,
202 attestations: I,
203 ) -> Verification<Self>
204 where
205 R: Rng + CryptoRng,
206 D: Digest,
207 I: IntoIterator<Item = Attestation<Self>>,
208 {
209 let mut invalid = BTreeSet::new();
210
211 let verified = attestations.into_iter().filter_map(|attestation| {
212 if self.verify_attestation(namespace, subject.clone(), &attestation) {
213 Some(attestation)
214 } else {
215 invalid.insert(attestation.signer);
216 None
217 }
218 });
219
220 Verification::new(verified.collect(), invalid.into_iter().collect())
221 }
222
223 /// Assembles attestations into a certificate, returning `None` if the threshold is not met.
224 ///
225 /// Callers must not include duplicate attestations from the same signer.
226 fn assemble<I>(&self, attestations: I) -> Option<Self::Certificate>
227 where
228 I: IntoIterator<Item = Attestation<Self>>;
229
230 /// Verifies a certificate that was recovered or received from the network.
231 fn verify_certificate<R: Rng + CryptoRng, D: Digest>(
232 &self,
233 rng: &mut R,
234 namespace: &[u8],
235 subject: Self::Subject<'_, D>,
236 certificate: &Self::Certificate,
237 ) -> bool;
238
239 /// Verifies a stream of certificates, returning `false` at the first failure.
240 fn verify_certificates<'a, R, D, I>(
241 &self,
242 rng: &mut R,
243 namespace: &[u8],
244 certificates: I,
245 ) -> bool
246 where
247 R: Rng + CryptoRng,
248 D: Digest,
249 I: Iterator<Item = (Self::Subject<'a, D>, &'a Self::Certificate)>,
250 {
251 for (subject, certificate) in certificates {
252 if !self.verify_certificate(rng, namespace, subject, certificate) {
253 return false;
254 }
255 }
256
257 true
258 }
259
260 /// Returns whether per-participant fault evidence can be safely exposed.
261 ///
262 /// Schemes where individual signatures can be safely reported as fault evidence should
263 /// return `true`.
264 fn is_attributable(&self) -> bool;
265
266 /// Encoding configuration for bounded-size certificate decoding used in network payloads.
267 fn certificate_codec_config(&self) -> <Self::Certificate as Read>::Cfg;
268
269 /// Encoding configuration that allows unbounded certificate decoding.
270 ///
271 /// Only use this when decoding data from trusted local storage, it must not be exposed to
272 /// adversarial inputs or network payloads.
273 fn certificate_codec_config_unbounded() -> <Self::Certificate as Read>::Cfg;
274}
275
276/// Supplies the signing scheme for a given scope.
277///
278/// This trait uses an associated `Scope` type, allowing implementations to work
279/// with any scope representation (e.g., epoch numbers, block heights, etc.).
280pub trait Provider: Clone + Send + Sync + 'static {
281 /// The scope type used to look up schemes.
282 type Scope: Clone + Send + Sync + 'static;
283 /// The signing scheme to provide.
284 type Scheme: Scheme;
285
286 /// Return the signing scheme that corresponds to `scope`.
287 fn scoped(&self, scope: Self::Scope) -> Option<Arc<Self::Scheme>>;
288
289 /// Return a certificate verifier that can validate certificates from all scopes.
290 ///
291 /// This method allows implementations to provide a verifier that can validate
292 /// certificates from all scopes (without scope-specific state). For example,
293 /// `bls12381_threshold::Scheme` maintains a static public key across epochs that
294 /// can be used to verify certificates from any epoch, even after the committee
295 /// has rotated and the underlying secret shares have been refreshed.
296 ///
297 /// The default implementation returns `None`. Callers should fall back to
298 /// [`Provider::scoped`] for scope-specific verification.
299 fn all(&self) -> Option<Arc<Self::Scheme>> {
300 None
301 }
302}
303
304/// Bitmap wrapper that tracks which participants signed a certificate.
305///
306/// Internally, it stores bits in 1-byte chunks for compact encoding.
307#[derive(Clone, Debug, PartialEq, Eq, Hash)]
308pub struct Signers {
309 bitmap: BitMap<1>,
310}
311
312impl Signers {
313 /// Builds [`Signers`] from an iterator of signer indices.
314 ///
315 /// # Panics
316 ///
317 /// Panics if the sequence contains indices larger than the size of the participant set
318 /// or duplicates.
319 pub fn from(participants: usize, signers: impl IntoIterator<Item = u32>) -> Self {
320 let mut bitmap = BitMap::zeroes(participants as u64);
321 for signer in signers.into_iter() {
322 assert!(
323 !bitmap.get(signer as u64),
324 "duplicate signer index: {signer}",
325 );
326 // We opt to not assert order here because some signing schemes allow
327 // for commutative aggregation of signatures (and sorting is unnecessary
328 // overhead).
329
330 bitmap.set(signer as u64, true);
331 }
332
333 Self { bitmap }
334 }
335
336 /// Returns the length of the bitmap (the size of the participant set).
337 #[allow(clippy::len_without_is_empty)]
338 pub const fn len(&self) -> usize {
339 self.bitmap.len() as usize
340 }
341
342 /// Returns how many participants are marked as signers.
343 pub fn count(&self) -> usize {
344 self.bitmap.count_ones() as usize
345 }
346
347 /// Iterates over signer indices in ascending order.
348 pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
349 self.bitmap
350 .iter()
351 .enumerate()
352 .filter_map(|(index, bit)| bit.then_some(index as u32))
353 }
354}
355
356impl Write for Signers {
357 fn write(&self, writer: &mut impl BufMut) {
358 self.bitmap.write(writer);
359 }
360}
361
362impl EncodeSize for Signers {
363 fn encode_size(&self) -> usize {
364 self.bitmap.encode_size()
365 }
366}
367
368impl Read for Signers {
369 type Cfg = usize;
370
371 fn read_cfg(reader: &mut impl Buf, max_participants: &usize) -> Result<Self, Error> {
372 let bitmap = BitMap::read_cfg(reader, &(*max_participants as u64))?;
373 // The participant count is treated as an upper bound for decoding flexibility, e.g. one
374 // might use `Scheme::certificate_codec_config_unbounded` for decoding certificates from
375 // local storage.
376 //
377 // Exact length validation **must** be enforced at verification time by the signing schemes
378 // against the actual participant set size.
379 Ok(Self { bitmap })
380 }
381}
382
383#[cfg(feature = "arbitrary")]
384impl arbitrary::Arbitrary<'_> for Signers {
385 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
386 let participants = u.arbitrary_len::<u8>()? % 10;
387 let signer_count = u.arbitrary_len::<u8>()?.min(participants);
388 let signers = (0..signer_count as u32).collect::<Vec<_>>();
389 Ok(Self::from(participants, signers))
390 }
391}
392
393/// A scheme provider that always returns the same scheme regardless of scope.
394#[derive(Clone, Debug)]
395pub struct ConstantProvider<S: Scheme, Sc = ()> {
396 scheme: Arc<S>,
397 _scope: core::marker::PhantomData<Sc>,
398}
399
400impl<S: Scheme, Sc> ConstantProvider<S, Sc> {
401 /// Creates a new provider that always returns the given scheme.
402 pub fn new(scheme: S) -> Self {
403 Self {
404 scheme: Arc::new(scheme),
405 _scope: core::marker::PhantomData,
406 }
407 }
408}
409
410impl<S: Scheme, Sc: Clone + Send + Sync + 'static> crate::certificate::Provider
411 for ConstantProvider<S, Sc>
412{
413 type Scope = Sc;
414 type Scheme = S;
415
416 fn scoped(&self, _: Sc) -> Option<Arc<S>> {
417 Some(self.scheme.clone())
418 }
419
420 fn all(&self) -> Option<Arc<Self::Scheme>> {
421 Some(self.scheme.clone())
422 }
423}
424
425#[cfg(feature = "mocks")]
426pub mod mocks {
427 //! Mocks for certificate signing schemes.
428
429 /// A fixture containing identities, identity private keys, per-participant
430 /// signing schemes, and a single verifier scheme.
431 #[derive(Clone, Debug)]
432 pub struct Fixture<S> {
433 /// A sorted vector of participant public identity keys.
434 pub participants: Vec<crate::ed25519::PublicKey>,
435 /// A sorted vector of participant private identity keys (matching order with `participants`).
436 pub private_keys: Vec<crate::ed25519::PrivateKey>,
437 /// A vector of per-participant scheme instances (matching order with `participants`).
438 pub schemes: Vec<S>,
439 /// A single scheme verifier.
440 pub verifier: S,
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use commonware_codec::{Decode, Encode};
448
449 #[test]
450 fn test_from_signers() {
451 let signers = Signers::from(6, [0, 3, 5]);
452 let collected: Vec<_> = signers.iter().collect();
453 assert_eq!(collected, vec![0, 3, 5]);
454 assert_eq!(signers.count(), 3);
455 }
456
457 #[test]
458 #[should_panic(expected = "bit 4 out of bounds (len: 4)")]
459 fn test_from_out_of_bounds() {
460 Signers::from(4, [0, 4]);
461 }
462
463 #[test]
464 #[should_panic(expected = "duplicate signer index: 0")]
465 fn test_from_duplicate() {
466 Signers::from(4, [0, 0, 1]);
467 }
468
469 #[test]
470 fn test_from_not_increasing() {
471 Signers::from(4, [2, 1]);
472 }
473
474 #[test]
475 fn test_codec_round_trip() {
476 let signers = Signers::from(9, [1, 6]);
477 let encoded = signers.encode();
478 let decoded = Signers::decode_cfg(encoded, &9).unwrap();
479 assert_eq!(decoded, signers);
480 }
481
482 #[test]
483 fn test_decode_respects_participant_limit() {
484 let signers = Signers::from(8, [0, 3, 7]);
485 let encoded = signers.encode();
486 // More participants than expected should fail.
487 assert!(Signers::decode_cfg(encoded.clone(), &2).is_err());
488 // Exact participant bound succeeds.
489 assert!(Signers::decode_cfg(encoded.clone(), &8).is_ok());
490 // Less participants than expected succeeds (upper bound).
491 assert!(Signers::decode_cfg(encoded, &10).is_ok());
492 }
493
494 #[cfg(feature = "arbitrary")]
495 mod conformance {
496 use super::*;
497 use commonware_codec::conformance::CodecConformance;
498
499 /// Test context type for generic scheme tests.
500 #[derive(Clone, Debug)]
501 pub struct TestSubject<'a> {
502 pub message: &'a [u8],
503 }
504
505 impl<'a> Subject for TestSubject<'a> {
506 fn namespace_and_message(&self, namespace: &[u8]) -> (Bytes, Bytes) {
507 (namespace.to_vec().into(), self.message.to_vec().into())
508 }
509 }
510
511 // Use the macro to generate the test scheme (signer/verifier are unused in conformance tests)
512 impl_certificate_ed25519!(TestSubject<'a>);
513
514 commonware_conformance::conformance_tests! {
515 CodecConformance<Signers>,
516 CodecConformance<Attestation<Scheme>>,
517 }
518 }
519}