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