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..ba9c8f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,12 @@ ahash = "0.8" anyhow = "1" base32 = "0.5.1" base64 = "0.22" -bls-signatures = { version = "0.15" } +blake2 = { git = "https://github.com/huitseeker/hashes.git", branch = "blake2x-pr", features = [ + "blake2x", +] } +bls-signatures = { version = "0.15", default-features = false, features = [ + "blst", +] } bls12_381 = "0.8" cid = { version = "0.10.1", features = ["std"] } fvm_ipld_bitfield = "0.7.1" diff --git a/blssig/Cargo.toml b/blssig/Cargo.toml index f755987..03f72d7 100644 --- a/blssig/Cargo.toml +++ b/blssig/Cargo.toml @@ -10,8 +10,10 @@ edition.workspace = true rust-version.workspace = true [dependencies] +blake2.workspace = true bls-signatures.workspace = true -bls12_381.workspace = true +blstrs = "0.7" +group = "0.13" filecoin-f3-gpbft = { path = "../gpbft", version = "0.1.0" } hashlink.workspace = true parking_lot.workspace = true diff --git a/blssig/src/bdn/mod.rs b/blssig/src/bdn/mod.rs index 30b4ca3..08c0f64 100644 --- a/blssig/src/bdn/mod.rs +++ b/blssig/src/bdn/mod.rs @@ -2,16 +2,25 @@ // 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 blstrs::{G1Projective, G2Projective, Scalar}; +use group::Group; /// BDN aggregation context for managing signature and public key aggregation pub struct BDNAggregation { - pub_keys: Vec, + pub(crate) pub_keys: Vec, + pub(crate) coefficients: Vec, + pub(crate) terms: Vec, } impl BDNAggregation { @@ -20,11 +29,18 @@ 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 { + pub_keys, + coefficients, + terms, + }) } - /// Aggregates signatures using standard BLS aggregation - /// TODO: Implement BDN aggregation scheme: https://github.com/ChainSafe/rust-f3/issues/29 + /// Aggregates signatures using BDN aggregation with coefficients. + /// Computes: sum((coef_i + 1) * sig_i) pub fn aggregate_sigs(&self, sigs: Vec) -> Result { if sigs.len() != self.pub_keys.len() { return Err(BLSError::LengthMismatch { @@ -33,11 +49,14 @@ impl BDNAggregation { }); } - // Standard BLS aggregation let mut agg_point = G2Projective::identity(); - for sig in sigs { - let sig: G2Affine = sig.into(); - agg_point += sig; + for (i, sig) in sigs.iter().enumerate() { + let coef = self.coefficients[i]; + 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 +64,68 @@ 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_le(&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 { + let mut terms = vec![]; + for (i, pub_key) in pub_keys.iter().enumerate() { + let pub_key_point: G1Projective = (*pub_key).into(); + let pub_c = pub_key_point * coefficients[i]; + let term = pub_c + pub_key_point; + terms.push(term.into()); + } + terms + } } diff --git a/blssig/src/verifier/mod.rs b/blssig/src/verifier/mod.rs index 61d9122..1171d2b 100644 --- a/blssig/src/verifier/mod.rs +++ b/blssig/src/verifier/mod.rs @@ -17,6 +17,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,6 +33,10 @@ 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 @@ -162,14 +168,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); } + if signer_indices.is_empty() { + return Err(BLSError::EmptySigners); + } let mut typed_pub_keys = vec![]; - for pub_key in signers { + for pub_key in power_table { if pub_key.0.len() != BLS_PUBLIC_KEY_LENGTH { return Err(BLSError::InvalidPublicKeyLength(pub_key.0.len())); } @@ -178,7 +188,7 @@ impl Verifier for BLSVerifier { } let bdn = BDNAggregation::new(typed_pub_keys)?; - let agg_pub_key = bdn.aggregate_pub_keys()?; + 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/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>; }