diff --git a/.config/rust-f3.dic b/.config/rust-f3.dic index 5e8f1a9..6ca3598 100644 --- a/.config/rust-f3.dic +++ b/.config/rust-f3.dic @@ -41,4 +41,8 @@ G1 G2 JSON CBOR -TODO \ No newline at end of file +TODO +coef_i +sig_i +pub_key_i ++ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index af43b07..4863de6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ ahash = "0.8" anyhow = "1" base32 = "0.5.1" base64 = "0.22" +blake2 = { git = "https://github.com/huitseeker/hashes.git", rev = "4d3debf264a45da9e33d52645eb6ee9963336f66", features = ["blake2x"] } # PR: https://github.com/RustCrypto/hashes/pull/704 bls-signatures = { version = "0.15" } bls12_381 = "0.8" cid = { version = "0.10.1", features = ["std"] } @@ -27,6 +28,7 @@ num-bigint = { version = "0.4.6", features = ["serde"] } num-traits = "0.2.19" parking_lot = "0.12" rand = "0.8" +rayon = "1.10" serde = { version = "1", features = ["derive"] } serde_cbor = "0.11.2" serde_json = { version = "1", features = ["raw_value"] } diff --git a/blssig/Cargo.toml b/blssig/Cargo.toml index f755987..d2d4c5f 100644 --- a/blssig/Cargo.toml +++ b/blssig/Cargo.toml @@ -10,11 +10,13 @@ edition.workspace = true rust-version.workspace = true [dependencies] +blake2.workspace = true bls-signatures.workspace = true bls12_381.workspace = true filecoin-f3-gpbft = { path = "../gpbft", version = "0.1.0" } hashlink.workspace = true parking_lot.workspace = true +rayon.workspace = true thiserror.workspace = true [dev-dependencies] diff --git a/blssig/src/bdn/mod.rs b/blssig/src/bdn/mod.rs index 30b4ca3..5ffd3ae 100644 --- a/blssig/src/bdn/mod.rs +++ b/blssig/src/bdn/mod.rs @@ -2,16 +2,24 @@ // SPDX-License-Identifier: Apache-2.0, MIT //! BDN (Boneh-Drijvers-Neven) signature aggregation scheme, for preventing rogue public-key attacks. +//! Those attacks could allow an attacker to forge a public-key and then make a verifiable +//! signature for an aggregation of signatures. It fixes the situation by adding coefficients to the aggregate. //! -//! NOTE: currently uses standard BLS aggregation without coefficient weighting, hence returns incorrect values compared to go-f3. +//! See the papers: +//! `https://eprint.iacr.org/2018/483.pdf` +//! `https://crypto.stanford.edu/~dabo/pubs/papers/BLSmultisig.html` //! use crate::verifier::BLSError; -use bls_signatures::{PublicKey, Signature}; -use bls12_381::{G1Projective, G2Affine, G2Projective}; +use blake2::Blake2xs; +use blake2::digest::{ExtendableOutput, Update, XofReader}; +use bls_signatures::{PublicKey, Serialize, Signature}; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use rayon::prelude::*; /// BDN aggregation context for managing signature and public key aggregation pub struct BDNAggregation { - pub_keys: Vec, + pub(crate) coefficients: Vec, + pub(crate) terms: Vec, } impl BDNAggregation { @@ -20,24 +28,42 @@ impl BDNAggregation { return Err(BLSError::EmptyPublicKeys); } - Ok(Self { pub_keys }) + let coefficients = Self::calc_coefficients(&pub_keys)?; + let terms = Self::calc_terms(&pub_keys, &coefficients); + + Ok(Self { + coefficients, + terms, + }) } - /// Aggregates signatures using standard BLS aggregation - /// TODO: Implement BDN aggregation scheme: https://github.com/ChainSafe/rust-f3/issues/29 - pub fn aggregate_sigs(&self, sigs: Vec) -> Result { - if sigs.len() != self.pub_keys.len() { + /// Aggregates signatures using BDN aggregation with coefficients. + /// Computes: sum((coef_i + 1) * sig_i) for signatures at the given indices + pub fn aggregate_sigs( + &self, + indices: &[u64], + sigs: &[Signature], + ) -> Result { + if sigs.len() != indices.len() { return Err(BLSError::LengthMismatch { - pub_keys: self.pub_keys.len(), + pub_keys: indices.len(), sigs: sigs.len(), }); } - // Standard BLS aggregation let mut agg_point = G2Projective::identity(); - for sig in sigs { - let sig: G2Affine = sig.into(); - agg_point += sig; + for (sig, &idx) in sigs.iter().zip(indices.iter()) { + let idx = idx as usize; + if idx >= self.coefficients.len() { + return Err(BLSError::SignerIndexOutOfRange(idx)); + } + + let coef = self.coefficients[idx]; + let sig_point: G2Projective = (*sig).into(); + let sig_c = sig_point * coef; + let sig_c = sig_c + sig_point; + + agg_point += sig_c; } // Convert back to Signature @@ -45,18 +71,70 @@ impl BDNAggregation { Ok(agg_sig) } - /// Aggregates public keys using standard BLS aggregation - /// TODO: Implement BDN aggregation scheme: https://github.com/ChainSafe/rust-f3/issues/29 - pub fn aggregate_pub_keys(&self) -> Result { - // Standard BLS aggregation + /// Aggregates public keys indices using BDN aggregation with coefficients. + /// Computes: sum((coef_i + 1) * pub_key_i) + pub fn aggregate_pub_keys(&self, indices: &[u64]) -> Result { + // Sum of pre-computed terms (which are already (coef_i + 1) * pub_key_i) let mut agg_point = G1Projective::identity(); - for pub_key in &self.pub_keys { - let pub_key_point: G1Projective = (*pub_key).into(); - agg_point += pub_key_point; + for &idx in indices { + let idx = idx as usize; + if idx >= self.terms.len() { + return Err(BLSError::SignerIndexOutOfRange(idx)); + } + let term_point: G1Projective = self.terms[idx].into(); + agg_point += term_point; } // Convert back to PublicKey let agg_pub_key: PublicKey = agg_point.into(); Ok(agg_pub_key) } + + pub fn calc_coefficients(pub_keys: &[PublicKey]) -> Result, BLSError> { + let mut hasher = Blake2xs::new(0xFFFF); + + // Hash all public keys + for pub_key in pub_keys { + let bytes = pub_key.as_bytes(); + hasher.update(&bytes); + } + + // Read 16 bytes per public key + let mut reader = hasher.finalize_xof(); + let mut output = vec![0u8; pub_keys.len() * 16]; + reader.read(&mut output); + + // Convert every consecutive 16 bytes chunk to a scalar + let mut coefficients = Vec::with_capacity(pub_keys.len()); + for i in 0..pub_keys.len() { + let chunk = &output[i * 16..(i + 1) * 16]; + + // Convert 16 bytes to 32 bytes, for scalar (pad with zeros) + let mut bytes_32 = [0u8; 32]; + bytes_32[..16].copy_from_slice(chunk); + + // BLS12-381 scalars expects little-endian byte representation + let scalar = Scalar::from_bytes(&bytes_32); + if scalar.is_some().into() { + coefficients.push(scalar.unwrap()); + } else { + return Err(BLSError::InvalidScalar); + } + } + + Ok(coefficients) + } + + pub fn calc_terms(pub_keys: &[PublicKey], coefficients: &[Scalar]) -> Vec { + pub_keys + .par_iter() + .enumerate() + .map(|(i, pub_key)| { + let pub_key_point: G1Projective = (*pub_key).into(); + let pub_c = pub_key_point * coefficients[i]; + let term = pub_c + pub_key_point; + term.into() + }) + .collect() + } } diff --git a/blssig/src/verifier/mod.rs b/blssig/src/verifier/mod.rs index 61d9122..1e21892 100644 --- a/blssig/src/verifier/mod.rs +++ b/blssig/src/verifier/mod.rs @@ -6,6 +6,7 @@ use filecoin_f3_gpbft::PubKey; use filecoin_f3_gpbft::api::Verifier; use hashlink::LruCache; use parking_lot::RwLock; +use std::sync::Arc; use thiserror::Error; use crate::bdn::BDNAggregation; @@ -17,6 +18,8 @@ mod tests; pub enum BLSError { #[error("empty public keys provided")] EmptyPublicKeys, + #[error("empty signers provided")] + EmptySigners, #[error("empty signatures provided")] EmptySignatures, #[error("invalid public key length: expected {BLS_PUBLIC_KEY_LENGTH} bytes, got {0}")] @@ -31,17 +34,24 @@ pub enum BLSError { SignatureVerificationFailed, #[error("mismatched number of public keys and signatures: {pub_keys} != {sigs}")] LengthMismatch { pub_keys: usize, sigs: usize }, + #[error("invalid scalar value")] + InvalidScalar, + #[error("signer index {0} is out of range")] + SignerIndexOutOfRange(usize), } /// BLS signature verifier using BDN aggregation scheme /// /// This verifier implements the same scheme used by `go-f3/blssig`, with: /// - BLS12_381 curve -/// - G1 for public keys, G2 for signatures +/// - G1 for public keys, G2 for signatures /// - BDN aggregation for rogue-key attack prevention pub struct BLSVerifier { /// Cache for deserialized public key points to avoid expensive repeated operations point_cache: RwLock, PublicKey>>, + /// Cache for current power table's BDN aggregation + /// Only caches the current since power tables don't tend to repeat after rotation + bdn_cache: RwLock, Arc)>>, } impl Default for BLSVerifier { @@ -64,6 +74,7 @@ impl BLSVerifier { Self { // key size: 48, value size: 196, total estimated: 1.83 MiB point_cache: RwLock::new(LruCache::new(MAX_POINT_CACHE_SIZE)), + bdn_cache: RwLock::new(None), } } @@ -114,6 +125,34 @@ impl BLSVerifier { fn deserialize_signature(&self, sig: &[u8]) -> Result { Signature::from_bytes(sig).map_err(BLSError::SignatureDeserialization) } + + /// Gets a cached BDN aggregation or creates and caches it + fn get_or_cache_bdn(&self, power_table: &[PubKey]) -> Result, BLSError> { + // Check cache first + if let Some((cached_power_table, cached_bdn)) = self.bdn_cache.read().as_ref() { + if cached_power_table == power_table { + return Ok(cached_bdn.clone()); + } + } + + // Deserialize and create new BDN aggregation + let mut typed_pub_keys = vec![]; + for pub_key in power_table { + if pub_key.0.len() != BLS_PUBLIC_KEY_LENGTH { + return Err(BLSError::InvalidPublicKeyLength(pub_key.0.len())); + } + typed_pub_keys.push(self.get_or_cache_public_key(&pub_key.0)?); + } + + let bdn = Arc::new(BDNAggregation::new(typed_pub_keys)?); + + // Cache it + self.bdn_cache + .write() + .replace((power_table.to_vec(), bdn.clone())); + + Ok(bdn) + } } impl Verifier for BLSVerifier { @@ -154,7 +193,8 @@ impl Verifier for BLSVerifier { } let bdn = BDNAggregation::new(typed_pub_keys)?; - let agg_sig = bdn.aggregate_sigs(typed_sigs)?; + let indices: Vec = (0..typed_sigs.len() as u64).collect(); + let agg_sig = bdn.aggregate_sigs(&indices, &typed_sigs)?; Ok(agg_sig.as_bytes()) } @@ -162,23 +202,18 @@ impl Verifier for BLSVerifier { &self, payload: &[u8], agg_sig: &[u8], - signers: &[PubKey], + power_table: &[PubKey], + signer_indices: &[u64], ) -> Result<(), Self::Error> { - if signers.is_empty() { + if power_table.is_empty() { return Err(BLSError::EmptyPublicKeys); } - - let mut typed_pub_keys = vec![]; - for pub_key in signers { - if pub_key.0.len() != BLS_PUBLIC_KEY_LENGTH { - return Err(BLSError::InvalidPublicKeyLength(pub_key.0.len())); - } - - typed_pub_keys.push(self.get_or_cache_public_key(&pub_key.0)?); + if signer_indices.is_empty() { + return Err(BLSError::EmptySigners); } - let bdn = BDNAggregation::new(typed_pub_keys)?; - let agg_pub_key = bdn.aggregate_pub_keys()?; + let bdn = self.get_or_cache_bdn(power_table)?; + let agg_pub_key = bdn.aggregate_pub_keys(signer_indices)?; let agg_pub_key_bytes = PubKey(agg_pub_key.as_bytes().to_vec()); self.verify_single(&agg_pub_key_bytes, payload, agg_sig) } diff --git a/blssig/src/verifier/tests.rs b/blssig/src/verifier/tests.rs index 496c42c..b24ff90 100644 --- a/blssig/src/verifier/tests.rs +++ b/blssig/src/verifier/tests.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use super::BLSVerifier; +use crate::bdn::BDNAggregation; use bls_signatures::{PrivateKey, Serialize}; use filecoin_f3_gpbft::PubKey; use filecoin_f3_gpbft::api::Verifier; @@ -68,3 +69,49 @@ fn test_invalid_signature() { "corrupted signature should fail verification" ); } + +#[test] +fn test_aggregate_signature_verification() { + let verifier = BLSVerifier::new(); + let message = b"consensus message"; + + // Generate 10 validators for the power table + let mut signers = Vec::new(); + let mut power_table = Vec::new(); + for _ in 0..10 { + let private_key = PrivateKey::generate(&mut rand::thread_rng()); + let signer = BLSSigner::new(private_key); + power_table.push(signer.public_key().clone()); + signers.push(signer); + } + + // Only 5 validators sign (indices 0, 2, 4, 6, 8) + let signers_subset = vec![0u64, 2, 4, 6, 8]; + let sigs: Vec> = signers_subset + .iter() + .map(|&i| signers[i as usize].sign(message)) + .collect(); + + // Aggregate using BDN with full power table + let typed_pub_keys: Vec<_> = power_table + .iter() + .map(|pk| bls_signatures::PublicKey::from_bytes(&pk.0).unwrap()) + .collect(); + let typed_sigs: Vec<_> = sigs + .iter() + .map(|sig| bls_signatures::Signature::from_bytes(sig).unwrap()) + .collect(); + + let bdn = BDNAggregation::new(typed_pub_keys).expect("BDN creation should succeed"); + let agg_sig = bdn + .aggregate_sigs(&signers_subset, &typed_sigs) + .expect("aggregation should succeed"); + + // Verify aggregate using full power table and signer indices + let result = + verifier.verify_aggregate(message, &agg_sig.as_bytes(), &power_table, &signers_subset); + assert!( + result.is_ok(), + "aggregate signature verification should succeed" + ); +} diff --git a/certs/src/lib.rs b/certs/src/lib.rs index d362820..4379fc2 100644 --- a/certs/src/lib.rs +++ b/certs/src/lib.rs @@ -293,27 +293,19 @@ fn verify_signature( // Encode the payload for signing let payload_bytes = payload.serialize_for_signing(&network.to_string()); - // Extract public keys for signers - let signers_pk: Vec = signers + // Extract all public keys from power table + let pub_keys: Vec = power_table .iter() - .map(|&index| power_table[index as usize].pub_key.clone()) + .map(|entry| entry.pub_key.clone()) .collect(); // Verify the aggregate signature - let res = verifier - .verify_aggregate(&payload_bytes, &cert.signature, &signers_pk) + verifier + .verify_aggregate(&payload_bytes, &cert.signature, &pub_keys, &signers) .map_err(|e| CertsError::SignatureVerificationFailed { instance: cert.gpbft_instance, error: e.to_string(), - }); - - // Temporarily silencing verification errors - // The current BDN implementation uses standard BLS aggregation, causing verification to fail. - // This logging allows development to continue. - // TODO: Remove this workaround once BDN aggregation scheme is implemented - if let Err(err) = res { - println!("WARN: {}", err); - } + })?; Ok(()) } @@ -721,7 +713,8 @@ mod tests { &self, payload: &[u8], agg_sig: &[u8], - signers: &[PubKey], + power_table: &[PubKey], + signer_indices: &[u64], ) -> std::result::Result<(), Self::Error> { Ok(()) } diff --git a/gpbft/src/api.rs b/gpbft/src/api.rs index f8696a4..76658c3 100644 --- a/gpbft/src/api.rs +++ b/gpbft/src/api.rs @@ -33,12 +33,16 @@ pub trait Verifier: Send + Sync { /// Verifies an aggregate signature /// + /// The aggregation requires all public keys from the power table in order + /// to compute coefficients correctly, even when only a subset actually signed. + /// /// This method must be safe for concurrent use. /// /// # Arguments /// * `payload` - The payload that was signed /// * `agg_sig` - The aggregate signature to verify - /// * `signers` - The public keys of the signers + /// * `power_table` - All public keys from the power table + /// * `signer_indices` - Indices of the signers in the power table /// /// # Returns /// A Result indicating success or failure with an error message @@ -46,6 +50,7 @@ pub trait Verifier: Send + Sync { &self, payload: &[u8], agg_sig: &[u8], - signers: &[PubKey], + power_table: &[PubKey], + signer_indices: &[u64], ) -> Result<(), Self::Error>; }