From c22eed8245120fd4be3c24bc236749e78d05a361 Mon Sep 17 00:00:00 2001 From: Skalman Date: Wed, 13 May 2026 11:41:41 -0400 Subject: [PATCH 01/14] Addressing [Wildcard match arm defeats exhaustiveness check](https://github.com/paritytech/srlabs_findings/issues/660) --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c97a2e6..d839f4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,7 +215,8 @@ impl Message { fn cipher_suite(&self) -> Vec { let id = match self.2 { MessageType::ProofOfPossession => PROOF_OF_POSSESSION_ID, - _ => NORMAL_MESSAGE_SIGNATURE_ID, + MessageType::NormalAssumingPoP => NORMAL_MESSAGE_SIGNATURE_ID, + MessageType::NormalBasic => NORMAL_MESSAGE_SIGNATURE_ID, }; let h2c_suite_id = [ @@ -228,7 +229,7 @@ impl Message { let sc_tag = match self.2 { MessageType::ProofOfPossession => POP_MESSAGE, MessageType::NormalAssumingPoP => NORMAL_MESSAGE_SIGNATURE_ASSUMING_POP, - _ => NORMAL_MESSAGE_SIGNATURE_BASIC, + MessageType::NormalBasic => NORMAL_MESSAGE_SIGNATURE_BASIC, }; [id, &h2c_suite_id[..], sc_tag].concat() From a5bd782ce7f8cfaba3023d0803eded9c6ff8ad05 Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 19 May 2026 00:07:35 -0400 Subject: [PATCH 02/14] Add ValidateKey Step according to IETF BLS draft and tests. Resolving https://github.com/paritytech/srlabs_findings/issues/661 --- src/engine.rs | 13 ++++- src/experimental/bit.rs | 8 ++- src/nugget_pop.rs | 40 +++++++++++++- src/single.rs | 27 +++++++++- src/verifiers.rs | 113 ++++++++++++++++++++++++++++++++++------ 5 files changed, 179 insertions(+), 22 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 0c7dfa2..73bff45 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -32,7 +32,7 @@ use ark_ec::{ }; use ark_ff::field_hashers::{DefaultFieldHasher, HashToField}; use ark_ff::{Field, PrimeField, UniformRand, Zero}; -use ark_serialize::CanonicalSerialize; +use ark_serialize::{CanonicalSerialize, Valid}; use rand::Rng; use rand_core::RngCore; @@ -219,6 +219,17 @@ pub trait EngineBLS { Self::PublicKeyPrepared::from(g_affine) } + /// Reject public keys that are the identity element or not in the + /// prime-order subgroup. Defends verification against inputs that + /// bypass the deserialization-time check — e.g. direct tuple-struct + /// construction, or aggregates that sum to the identity. + /// + /// Implements `KeyValidate` from + /// . + fn validate_public_key(g: &Self::PublicKeyGroupAffine) -> bool { + !g.is_zero() && g.check().is_ok() + } + /// Process the signature to be use in pairing. This has to be /// implemented by the type of BLS system implementing the engine /// by calling either prepare_g1 or prepare_g2 based on which group diff --git a/src/experimental/bit.rs b/src/experimental/bit.rs index 27c91b5..1f04db7 100644 --- a/src/experimental/bit.rs +++ b/src/experimental/bit.rs @@ -632,7 +632,9 @@ mod tests { .collect::>(); let mut bitsig1 = BitSignedMessage::::new(pop.clone(), &msg1); - assert!(bitsig1.verify()); // verifiers::verify_with_distinct_messages(&dms,true) + // Empty aggregate: the summed public key is the identity element, + // which `validate_public_key` rejects per IETF KeyValidate. + assert!(!bitsig1.verify()); for (i, sig) in sigs1.iter().enumerate().take(2) { assert!(bitsig1.add(sig).is_ok() == (i < 4)); assert!(bitsig1.verify()); // verifiers::verify_with_distinct_messages(&dms,true) @@ -687,7 +689,9 @@ mod tests { let mut countsig = CountSignedMessage::::new(pop.clone(), msg1); assert!(countsig.signers.len() == 1); - assert!(countsig.verify()); // verifiers::verify_with_distinct_messages(&dms,true) + // Empty aggregate: the summed public key is the identity element, + // which `validate_public_key` rejects per IETF KeyValidate. + assert!(!countsig.verify()); assert!(countsig.add_bitsig(&bitsig1).is_ok()); assert!(bitsig1.signature == countsig.signature); assert!(countsig.signers.len() == 1); diff --git a/src/nugget_pop.rs b/src/nugget_pop.rs index c05f2f3..9ec7126 100644 --- a/src/nugget_pop.rs +++ b/src/nugget_pop.rs @@ -92,7 +92,11 @@ impl let mut randomized_pub_in_g1 = public_key_in_signature_group; randomized_pub_in_g1 *= randomization_coefficient; let signature = E::prepare_signature(self.0 + randomized_pub_in_g1); - let prepared_public_key = E::prepare_public_key(public_key_of_prover.1); + let Some(prepared_public_key) = + crate::verifiers::validate_and_prepare_public_key::(public_key_of_prover.1) + else { + return false; + }; let prepared = [ ( prepared_public_key.clone(), @@ -162,12 +166,13 @@ where #[cfg(all(test, feature = "std"))] mod tests { use crate::double_nugget::DoubleNuggetBLS; - use crate::engine::TinyBLS381; + use crate::engine::{EngineBLS, TinyBLS381}; use crate::serialize::SerializableToBytes; use crate::single::Keypair; use crate::{nugget_pop::NuggetBLSPoP, NuggetDoublePublicKey}; use crate::{ProofOfPossession, ProofOfPossessionGenerator}; + use ark_ff::Zero; use rand::thread_rng; use sha2::Sha256; @@ -324,4 +329,35 @@ mod tests { NuggetBLSnCPPoP, >(); } + + /// A keypair with secret scalar zero and identity public key. + /// `SecretKeyVT::sign` returns `sk * H(msg) = identity` for such + /// a keypair, so the PoP produced by `generate_pok` is itself + /// identity. The pairing equation in `NuggetBLSPoP::verify` then + /// reduces to `identity == identity`, so without `validate_public_key` + /// the verifier would accept — rejection here is attributable to + /// the public key check. + #[test] + fn nugget_bls_pop_rejects_identity_pk() { + use crate::single::{PublicKey, SecretKeyVT}; + let mut keypair = Keypair:: { + public: PublicKey::( + ::PublicKeyGroup::zero(), + ), + secret: SecretKeyVT::(::Scalar::zero()) + .into_split(thread_rng()), + }; + let pop = as ProofOfPossessionGenerator< + TinyBLS381, + Sha256, + NuggetDoublePublicKey, + NuggetBLSPoP, + >>::generate_pok(&mut keypair); + let double_pk = DoubleNuggetBLS::into_nugget_double_public_key(&keypair); + assert!(! as ProofOfPossession< + TinyBLS381, + Sha256, + NuggetDoublePublicKey, + >>::verify(&pop, &double_pk)); + } } diff --git a/src/single.rs b/src/single.rs index 7b826b3..0b220f7 100644 --- a/src/single.rs +++ b/src/single.rs @@ -458,7 +458,11 @@ impl Signature { /// Verify a single BLS signature pub fn verify(&self, message: &Message, publickey: &PublicKey) -> bool { - let publickey = E::prepare_public_key(publickey.0); + let Some(publickey) = + crate::verifiers::validate_and_prepare_public_key::(publickey.0) + else { + return false; + }; // TODO: Bentchmark these two variants // Variant 1. Do not batch any normalizations let message = E::prepare_signature(message.hash_to_signature_curve::()); @@ -992,4 +996,25 @@ mod tests { random_seed.as_slice(), ); } + + /// Keypair with secret=0, public=identity produces an identity + /// signature, so the pairing equation reduces to `identity == identity` + /// — an unprotected `Signature::verify` would accept. Rejection here + /// is attributable to `validate_public_key`. + #[test] + fn signature_verify_rejects_identity_pk() { + use ark_ff::Zero; + use rand::{rngs::StdRng, SeedableRng}; + + type EB = UsualBLS; + + let mut keypair = Keypair:: { + public: PublicKey::(::PublicKeyGroup::zero()), + secret: SecretKeyVT::(::Scalar::zero()) + .into_split(StdRng::from_seed([0u8; 32])), + }; + let s = keypair.signed_message(&Message::new(b"ctx", b"test message")); + assert!(!s.signature.verify(&s.message, &s.publickey)); + assert!(!Signed::verify(&s)); + } } diff --git a/src/verifiers.rs b/src/verifiers.rs index 20fbb05..927a410 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -37,6 +37,22 @@ pub type SignatureAffine = <::SignatureGroup as CurveGroup>:: // ── Shared helpers ────────────────────────────────────────────────── +/// Validate a public key with `EngineBLS::validate_public_key` and, on +/// success, return the prepared form. Returns `None` if the public key +/// is the identity element or not in the prime-order subgroup. All +/// verifier paths should go through this wrapper so the check cannot +/// be skipped by callers constructing `PublicKey` directly or by +/// aggregates that sum to the identity. +pub fn validate_and_prepare_public_key( + g: impl Into>, +) -> Option { + let g_affine: PublicKeyAffine = g.into(); + if !E::validate_public_key(&g_affine) { + return None; + } + Some(E::prepare_public_key(g_affine)) +} + /// Verify from fully normalized (affine) inputs. /// All public keys, messages, and the signature must already be in affine form. /// This prepares the pairing inputs and calls `verify_prepared`. @@ -46,11 +62,13 @@ fn verify_normalized( affine_signature: SignatureAffine, ) -> bool { let prepared_sig = E::prepare_signature(affine_signature); - let prepared = affine_publickeys - .iter() - .zip(affine_messages) - .map(|(pk, m)| (E::prepare_public_key(*pk), E::prepare_signature(*m))) - .collect::>(); + let mut prepared = Vec::with_capacity(affine_publickeys.len()); + for (pk, m) in affine_publickeys.iter().zip(affine_messages) { + let Some(prepared_pk) = validate_and_prepare_public_key::(*pk) else { + return false; + }; + prepared.push((prepared_pk, E::prepare_signature(*m))); + } E::verify_prepared(prepared_sig, prepared.iter()) } @@ -143,15 +161,18 @@ fn normalize_messages_and_signature( /// Simple unoptimized BLS signature verification. Useful for testing. pub fn verify_unoptimized(s: S) -> bool { let signature = S::E::prepare_signature(s.signature().0); - let prepared = s - .messages_and_publickeys() - .map(|(message, public_key)| { - ( - S::E::prepare_public_key(public_key.borrow().0), - S::E::prepare_signature(message.borrow().hash_to_signature_curve::()), - ) - }) - .collect::>(); + let mut prepared = Vec::new(); + for (message, public_key) in s.messages_and_publickeys() { + let Some(prepared_pk) = + validate_and_prepare_public_key::(public_key.borrow().0) + else { + return false; + }; + prepared.push(( + prepared_pk, + S::E::prepare_signature(message.borrow().hash_to_signature_curve::()), + )); + } S::E::verify_prepared(signature, prepared.iter()) } @@ -365,13 +386,47 @@ fn verify_with_gaussian_elimination(s: S) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::{Keypair, Message, UsualBLS}; - use ark_bls12_381::Bls12_381; + use crate::single::{SecretKeyVT, SignedMessage}; + use crate::{Keypair, Message, PublicKey, UsualBLS}; + use ark_bls12_381::{Bls12_381, Fq, G1Affine}; + use ark_ec::AffineRepr; + use ark_ff::{UniformRand, Zero}; + use ark_serialize::Valid; use rand::rngs::StdRng; use rand::SeedableRng; type EB = UsualBLS; + /// Keypair with secret scalar zero and identity public key. The + /// produced signature is also identity, so every pairing equation + /// reduces to `identity == identity` and an unprotected verifier + /// would accept. Any rejection observed in the verifier tests + /// below is therefore attributable to `validate_public_key`. + fn identity_keypair() -> Keypair { + Keypair { + public: PublicKey::(::PublicKeyGroup::zero()), + secret: SecretKeyVT::(::Scalar::zero()) + .into_split(StdRng::from_seed([0u8; 32])), + } + } + + fn identity_signed() -> SignedMessage { + identity_keypair().signed_message(&Message::new(b"ctx", b"test message")) + } + + /// A G1 point on the curve but outside the prime-order subgroup. + fn non_subgroup_g1() -> G1Affine { + let mut rng = StdRng::from_seed([42u8; 32]); + loop { + let x = Fq::rand(&mut rng); + if let Some(point) = G1Affine::get_point_from_x_unchecked(x, false) { + if point.check().is_err() { + return point; + } + } + } + } + #[test] fn verify_simple_single_signature() { let good = Message::new(b"ctx", b"test message"); @@ -401,4 +456,30 @@ mod tests { let signed = keypair.signed_message(&good); assert!(verify_unoptimized(&signed)); } + + #[test] + fn verify_simple_rejects_identity_pk() { + assert!(!verify_simple(&identity_signed())); + } + + #[test] + fn verify_unoptimized_rejects_identity_pk() { + assert!(!verify_unoptimized(&identity_signed())); + } + + #[test] + fn verify_with_distinct_messages_rejects_identity_pk() { + assert!(!verify_with_distinct_messages(&identity_signed(), true)); + } + + #[test] + fn validate_and_prepare_public_key_rejects_identity() { + let identity = PublicKeyAffine::::zero(); + assert!(validate_and_prepare_public_key::(identity).is_none()); + } + + #[test] + fn validate_and_prepare_public_key_rejects_non_subgroup() { + assert!(validate_and_prepare_public_key::(non_subgroup_g1()).is_none()); + } } From 5c30868de44362a8b2fe24d41766d40bf74447b7 Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 19 May 2026 00:20:19 -0400 Subject: [PATCH 03/14] Fail verification instead of panicing when final exponentiation fails addressing https://github.com/paritytech/srlabs_findings/issues/666 --- src/engine.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 73bff45..7079bcd 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -198,8 +198,7 @@ pub trait EngineBLS { signature, )]; Self::final_exponentiation(Self::miller_loop(inputs.into_iter().map(|t| t).chain(&lhs))) - .unwrap() - == (PairingOutput::::zero()) //zero is the target_field::one !! + == Some(PairingOutput::::zero()) //zero is the target_field::one !! } /// Prepared negative of the generator of the public key curve. From 07fdfcff8efc2d2c96766760d70cdc2cf779e16f Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 19 May 2026 01:07:48 -0400 Subject: [PATCH 04/14] Add side-channel protection to NuggetBLS::sign resolving https://github.com/paritytech/srlabs_findings/issues/673 --- src/chaum_pedersen_signature.rs | 49 ++++++++++++++++++++++++++++++++- src/nugget.rs | 28 ++++++++++++++++--- src/single.rs | 39 ++++++++++++++------------ 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/chaum_pedersen_signature.rs b/src/chaum_pedersen_signature.rs index ab4709a..6b840a9 100644 --- a/src/chaum_pedersen_signature.rs +++ b/src/chaum_pedersen_signature.rs @@ -13,7 +13,7 @@ use crate::dual_scalar_mul::DualScalarMultiplication; use crate::engine::EngineBLS; use crate::nugget::{NuggetBLS, NuggetPublicKey, NuggetSignature}; use crate::serialize::SerializableToBytes; -use crate::{Message, SecretKeyVT}; +use crate::{Message, SecretKey, SecretKeyVT}; pub type DLEQProof = (::Scalar, ::Scalar); pub type ChaumPedersenSignature = NuggetSignature; @@ -336,6 +336,53 @@ where } } +/// Side-channel-protected variant: BLS signing goes through +/// `SecretKey::seeded_sign` (resplits the key with deterministic +/// randomness), while DLEQ proof generation and witness derivation +/// delegate to the vartime form, which is acceptable because they +/// operate over auxiliary scalars rather than the long-term key. +impl + ChaumPedersenSigner for SecretKey +where + S: PrimeGroup + SerializableToBytes, +{ + fn generate_cp_signature(&mut self, message: &Message) -> ChaumPedersenSignature { + let bls_signature = self.seeded_sign(message); + let dleq = >::generate_dleq_proof( + self, + message, + bls_signature.0, + ); + NuggetSignature(bls_signature.0, dleq) + } + + fn generate_dleq_proof( + &mut self, + message: &Message, + bls_signature: E::SignatureGroup, + ) -> DLEQProof { + as ChaumPedersenSigner>::generate_dleq_proof( + &mut self.into_vartime(), + message, + bls_signature, + ) + } + + fn generate_witness_scaler( + &self, + _message_point_as_bytes: &Vec, + ) -> <::PublicKeyGroup as PrimeGroup>::ScalarField { + // Unreachable: `generate_dleq_proof` for `SecretKey` delegates to + // the `SecretKeyVT` impl, which calls its own `generate_witness_scaler`. + // Trait coherence forces us to provide a body here, but no caller + // ever lands on it. + unimplemented!( + "SecretKey::generate_witness_scaler is never called; \ + dleq generation delegates to SecretKeyVT" + ) + } +} + #[cfg(all(test, feature = "std"))] mod tests { use rand::thread_rng; diff --git a/src/nugget.rs b/src/nugget.rs index 0c7df81..734f36f 100644 --- a/src/nugget.rs +++ b/src/nugget.rs @@ -25,7 +25,7 @@ use crate::chaum_pedersen_signature::{ChaumPedersenSigner, ChaumPedersenVerifier use crate::dual_scalar_mul::DualScalarMultiplication; use crate::chaum_pedersen_signature::DLEQProof; use crate::serialize::SerializableToBytes; -use crate::single::{Keypair, KeypairVT, PublicKey, SecretKeyVT, Signature}; +use crate::single::{Keypair, KeypairVT, PublicKey, SecretKey, SecretKeyVT, Signature}; use crate::{EngineBLS, Message, Signed}; /// Wrapper for a point in the signature group which is supposed to @@ -89,6 +89,26 @@ where } } +/// Side-channel-protected variant: signing goes through the +/// `ChaumPedersenSigner` impl for `SecretKey`, so the resplit happens +/// on the split key (no `into_vartime` conversion is done here). +impl NuggetBLS for SecretKey +where + S: PrimeGroup + SerializableToBytes, +{ + fn into_public_key_in_signature_group(&self) -> PublicKeyInSignatureGroup { + NuggetBLS::::into_public_key_in_signature_group(&self.into_vartime()) + } + + fn into_public_key_in_sister_group(&self) -> PublicKeyInSisterGroup { + self.into_vartime().into_public_key_in_sister_group() + } + + fn sign(&mut self, message: &Message) -> NuggetSignature { + ChaumPedersenSigner::::generate_cp_signature(self, &message) + } +} + impl NuggetBLS for KeypairVT where S: PrimeGroup + SerializableToBytes, @@ -112,16 +132,16 @@ where S: PrimeGroup + SerializableToBytes, { fn into_public_key_in_signature_group(&self) -> PublicKeyInSignatureGroup { - NuggetBLS::::into_public_key_in_signature_group(&self.into_vartime()) + NuggetBLS::::into_public_key_in_signature_group(&self.secret) } fn into_public_key_in_sister_group(&self) -> PublicKeyInSisterGroup { - self.into_vartime().into_public_key_in_sister_group() + NuggetBLS::::into_public_key_in_sister_group(&self.secret) } /// Sign a message using a Seedabale RNG created from a seed derived from the message and key fn sign(&mut self, message: &Message) -> NuggetSignature { - NuggetBLS::::sign(&mut self.into_vartime(), message) + NuggetBLS::::sign(&mut self.secret, message) } } diff --git a/src/single.rs b/src/single.rs index 0b220f7..573bda1 100644 --- a/src/single.rs +++ b/src/single.rs @@ -269,6 +269,27 @@ impl SecretKey { self.sign_once(message) } + /// Sign deterministically, reseeding the resplit RNG from a hash + /// of both key halves and the message. + pub fn seeded_sign(&mut self, message: &Message) -> Signature { + let mut serialized_part1 = [0u8; 32]; + let mut serialized_part2 = [0u8; 32]; + self.key[0] + .serialize_compressed(&mut serialized_part1[..]) + .unwrap(); + self.key[1] + .serialize_compressed(&mut serialized_part2[..]) + .unwrap(); + + let seed_digest = Sha256::new() + .chain_update(serialized_part1) + .chain_update(serialized_part2) + .chain_update(message.0); + + let seed: [u8; 32] = seed_digest.finalize().into(); + self.sign(message, StdRng::from_seed(seed)) + } + /// Derive our public key from our secret key /// /// We do not resplit for side channel protections here since @@ -605,23 +626,7 @@ impl Keypair { /// Sign a message using a Seedabale RNG created from a seed derived from the message and key pub fn sign(&mut self, message: &Message) -> Signature { - let mut serialized_part1 = [0u8; 32]; - let mut serialized_part2 = [0u8; 32]; - self.secret.key[0] - .serialize_compressed(&mut serialized_part1[..]) - .unwrap(); - self.secret.key[1] - .serialize_compressed(&mut serialized_part2[..]) - .unwrap(); - - let seed_digest = Sha256::new() - .chain_update(serialized_part1) - .chain_update(serialized_part2) - .chain_update(message.0); - - let seed: [u8; 32] = seed_digest.finalize().into(); - - self.sign_with_rng::(message, SeedableRng::from_seed(seed)) + self.secret.seeded_sign(message) } #[cfg(feature = "std")] From f614d3ce73a236d9402108eb28b46c5d619ee495 Mon Sep 17 00:00:00 2001 From: Skalman Date: Thu, 21 May 2026 10:06:54 -0400 Subject: [PATCH 05/14] Add zeroize to secret keys and zeroize it serializations for generating cp secrets. Resolves https://github.com/paritytech/srlabs_findings/issues/675 --- src/chaum_pedersen_signature.rs | 6 ++++-- src/experimental/schnorr_pop.rs | 4 +++- src/single.rs | 15 ++++++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/chaum_pedersen_signature.rs b/src/chaum_pedersen_signature.rs index 6b840a9..e217953 100644 --- a/src/chaum_pedersen_signature.rs +++ b/src/chaum_pedersen_signature.rs @@ -322,10 +322,12 @@ where &self, message_point_as_bytes: &Vec, ) -> <::PublicKeyGroup as PrimeGroup>::ScalarField { - let secret_key_as_bytes = self.to_bytes(); - let mut secret_key_hasher = H::default(); + + let mut secret_key_as_bytes = self.to_bytes(); secret_key_hasher.update(secret_key_as_bytes.as_slice()); + ::zeroize::Zeroize::zeroize(secret_key_as_bytes.as_mut_slice()); + let hashed_secret_key = secret_key_hasher.finalize_fixed_reset().to_vec(); let hasher = as HashToField< diff --git a/src/experimental/schnorr_pop.rs b/src/experimental/schnorr_pop.rs index e06371d..b486f8f 100644 --- a/src/experimental/schnorr_pop.rs +++ b/src/experimental/schnorr_pop.rs @@ -41,11 +41,13 @@ impl BLSSchnorrPo //The pseudo random witness is generated similar to eddsa witness //hash(secret_key|publick_key) fn witness_scalar(&self) -> <::PublicKeyGroup as PrimeGroup>::ScalarField { - let secret_key_as_bytes = self.secret.to_bytes(); + let mut secret_key_as_bytes = self.secret.to_bytes(); let public_key_as_bytes = ::public_key_point_to_byte(&self.public.0); let mut secret_key_hasher = H::default(); secret_key_hasher.update(secret_key_as_bytes.as_slice()); + ::zeroize::Zeroize::zeroize(secret_key_as_bytes.as_mut_slice()); + let hashed_secret_key = secret_key_hasher.finalize_fixed_reset().to_vec(); let hasher = as HashToField< diff --git a/src/single.rs b/src/single.rs index 573bda1..6eeb967 100644 --- a/src/single.rs +++ b/src/single.rs @@ -45,6 +45,7 @@ use sha3::{ }; use digest::Digest; +use zeroize::{Zeroize, ZeroizeOnDrop}; use core::iter::once; @@ -54,8 +55,8 @@ use crate::{EngineBLS, Message, Signed}; /// Secret signing key lacking the side channel protections from /// key splitting. Avoid using directly in production. -#[derive(CanonicalSerialize, CanonicalDeserialize)] -pub struct SecretKeyVT(pub E::Scalar); +#[derive(CanonicalSerialize, CanonicalDeserialize, Zeroize, ZeroizeOnDrop)] +pub struct SecretKeyVT(#[zeroize] pub E::Scalar); impl Clone for SecretKeyVT { fn clone(&self) -> Self { @@ -163,10 +164,11 @@ impl SecretKeyVT { /// Secret signing key including the side channel protections from /// key splitting. +#[derive(ZeroizeOnDrop)] pub struct SecretKey { - key: [E::Scalar; 2], - old_unsigned: E::SignatureGroup, - old_signed: E::SignatureGroup, + #[zeroize] key: [E::Scalar; 2], + #[zeroize] old_unsigned: E::SignatureGroup, + #[zeroize] old_signed: E::SignatureGroup, } impl Clone for SecretKey { @@ -286,6 +288,9 @@ impl SecretKey { .chain_update(serialized_part2) .chain_update(message.0); + ::zeroize::Zeroize::zeroize(&mut serialized_part1); + ::zeroize::Zeroize::zeroize(&mut serialized_part2); + let seed: [u8; 32] = seed_digest.finalize().into(); self.sign(message, StdRng::from_seed(seed)) } From d2eb9dacc6e193111ca0f2c073d4a8a6fba8ac3a Mon Sep 17 00:00:00 2001 From: Skalman Date: Thu, 28 May 2026 17:35:23 -0400 Subject: [PATCH 06/14] - Restrict identity public key check to verifying PoP. - Only perform subgroup checks when verifying signature. - Add test for failure of strirct unforgiblity. --- src/engine.rs | 20 ++- src/experimental/bit.rs | 8 +- src/nugget_pop.rs | 9 +- src/single.rs | 41 ++---- src/verifiers.rs | 270 ++++++++++++++++++++++++++++++---------- 5 files changed, 239 insertions(+), 109 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 7079bcd..943bafb 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -218,15 +218,27 @@ pub trait EngineBLS { Self::PublicKeyPrepared::from(g_affine) } + /// Subgroup-membership check for a public-key-group point. + /// Used during signature verification (identity is allowed there). + fn verify_public_key_in_public_key_subgroup(g: &Self::PublicKeyGroupAffine) -> bool { + g.check().is_ok() + } + + /// Subgroup-membership check for a signature-group point. + /// Used during signature verification. + fn verify_signature_in_signature_subgroup(g: &Self::SignatureGroupAffine) -> bool { + g.check().is_ok() + } + /// Reject public keys that are the identity element or not in the - /// prime-order subgroup. Defends verification against inputs that - /// bypass the deserialization-time check — e.g. direct tuple-struct - /// construction, or aggregates that sum to the identity. + /// prime-order subgroup. Defends PoP verification against inputs + /// that bypass the deserialization-time check — e.g. direct + /// tuple-struct construction, or aggregates that sum to the identity. /// /// Implements `KeyValidate` from /// . fn validate_public_key(g: &Self::PublicKeyGroupAffine) -> bool { - !g.is_zero() && g.check().is_ok() + !g.is_zero() && Self::verify_public_key_in_public_key_subgroup(g) } /// Process the signature to be use in pairing. This has to be diff --git a/src/experimental/bit.rs b/src/experimental/bit.rs index 1f04db7..f53ddde 100644 --- a/src/experimental/bit.rs +++ b/src/experimental/bit.rs @@ -632,9 +632,7 @@ mod tests { .collect::>(); let mut bitsig1 = BitSignedMessage::::new(pop.clone(), &msg1); - // Empty aggregate: the summed public key is the identity element, - // which `validate_public_key` rejects per IETF KeyValidate. - assert!(!bitsig1.verify()); + assert!(bitsig1.verify()); // verifiers::verify_with_distinct_messages(&dms,true) for (i, sig) in sigs1.iter().enumerate().take(2) { assert!(bitsig1.add(sig).is_ok() == (i < 4)); assert!(bitsig1.verify()); // verifiers::verify_with_distinct_messages(&dms,true) @@ -689,9 +687,7 @@ mod tests { let mut countsig = CountSignedMessage::::new(pop.clone(), msg1); assert!(countsig.signers.len() == 1); - // Empty aggregate: the summed public key is the identity element, - // which `validate_public_key` rejects per IETF KeyValidate. - assert!(!countsig.verify()); + assert!(countsig.verify()); assert!(countsig.add_bitsig(&bitsig1).is_ok()); assert!(bitsig1.signature == countsig.signature); assert!(countsig.signers.len() == 1); diff --git a/src/nugget_pop.rs b/src/nugget_pop.rs index 9ec7126..b817d80 100644 --- a/src/nugget_pop.rs +++ b/src/nugget_pop.rs @@ -92,11 +92,12 @@ impl let mut randomized_pub_in_g1 = public_key_in_signature_group; randomized_pub_in_g1 *= randomization_coefficient; let signature = E::prepare_signature(self.0 + randomized_pub_in_g1); - let Some(prepared_public_key) = - crate::verifiers::validate_and_prepare_public_key::(public_key_of_prover.1) - else { + let public_key_affine: ::PublicKeyGroupAffine = + public_key_of_prover.1.into(); + if !E::validate_public_key(&public_key_affine) { return false; - }; + } + let prepared_public_key = E::prepare_public_key(public_key_affine); let prepared = [ ( prepared_public_key.clone(), diff --git a/src/single.rs b/src/single.rs index 6eeb967..ce35a76 100644 --- a/src/single.rs +++ b/src/single.rs @@ -484,21 +484,17 @@ impl Signature { /// Verify a single BLS signature pub fn verify(&self, message: &Message, publickey: &PublicKey) -> bool { - let Some(publickey) = - crate::verifiers::validate_and_prepare_public_key::(publickey.0) - else { + let pk_affine: ::PublicKeyGroupAffine = publickey.0.into(); + if !E::verify_public_key_in_public_key_subgroup(&pk_affine) { return false; - }; - // TODO: Bentchmark these two variants - // Variant 1. Do not batch any normalizations + } + let sig_affine: ::SignatureGroupAffine = self.0.into(); + if !E::verify_signature_in_signature_subgroup(&sig_affine) { + return false; + } + let publickey = E::prepare_public_key(pk_affine); let message = E::prepare_signature(message.hash_to_signature_curve::()); - let signature = E::prepare_signature(self.0); - // Variant 2. Batch signature curve normalizations - // let mut s = [E::hash_to_signature_curve(message), signature.0]; - // E::SignatureCurve::batch_normalization(&s); - // let message = s[0].into_affine().prepare(); - // let signature = s[1].into_affine().prepare(); - // TODO: Compare benchmarks on variants + let signature = E::prepare_signature(sig_affine); E::verify_prepared(signature, &[(publickey, message)]) } } @@ -1007,24 +1003,5 @@ mod tests { ); } - /// Keypair with secret=0, public=identity produces an identity - /// signature, so the pairing equation reduces to `identity == identity` - /// — an unprotected `Signature::verify` would accept. Rejection here - /// is attributable to `validate_public_key`. - #[test] - fn signature_verify_rejects_identity_pk() { - use ark_ff::Zero; - use rand::{rngs::StdRng, SeedableRng}; - - type EB = UsualBLS; - let mut keypair = Keypair:: { - public: PublicKey::(::PublicKeyGroup::zero()), - secret: SecretKeyVT::(::Scalar::zero()) - .into_split(StdRng::from_seed([0u8; 32])), - }; - let s = keypair.signed_message(&Message::new(b"ctx", b"test message")); - assert!(!s.signature.verify(&s.message, &s.publickey)); - assert!(!Signed::verify(&s)); - } } diff --git a/src/verifiers.rs b/src/verifiers.rs index 927a410..3271c69 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -8,7 +8,7 @@ use core::borrow::Borrow; // We use BTreeMap instead of HashMap for no_std compatibility. use alloc::collections::BTreeMap; use ark_ec::AffineRepr; -use ark_ff::field_hashers::{DefaultFieldHasher, HashToField}; +use ark_ff::{One, Zero, field_hashers::{DefaultFieldHasher, HashToField}}; use ark_serialize::CanonicalSerialize; use digest::FixedOutputReset; @@ -37,22 +37,6 @@ pub type SignatureAffine = <::SignatureGroup as CurveGroup>:: // ── Shared helpers ────────────────────────────────────────────────── -/// Validate a public key with `EngineBLS::validate_public_key` and, on -/// success, return the prepared form. Returns `None` if the public key -/// is the identity element or not in the prime-order subgroup. All -/// verifier paths should go through this wrapper so the check cannot -/// be skipped by callers constructing `PublicKey` directly or by -/// aggregates that sum to the identity. -pub fn validate_and_prepare_public_key( - g: impl Into>, -) -> Option { - let g_affine: PublicKeyAffine = g.into(); - if !E::validate_public_key(&g_affine) { - return None; - } - Some(E::prepare_public_key(g_affine)) -} - /// Verify from fully normalized (affine) inputs. /// All public keys, messages, and the signature must already be in affine form. /// This prepares the pairing inputs and calls `verify_prepared`. @@ -61,13 +45,16 @@ fn verify_normalized( affine_messages: &[SignatureAffine], affine_signature: SignatureAffine, ) -> bool { + if !E::verify_signature_in_signature_subgroup(&affine_signature) { + return false; + } let prepared_sig = E::prepare_signature(affine_signature); let mut prepared = Vec::with_capacity(affine_publickeys.len()); for (pk, m) in affine_publickeys.iter().zip(affine_messages) { - let Some(prepared_pk) = validate_and_prepare_public_key::(*pk) else { + if !E::verify_public_key_in_public_key_subgroup(pk) { return false; - }; - prepared.push((prepared_pk, E::prepare_signature(*m))); + } + prepared.push((E::prepare_public_key(*pk), E::prepare_signature(*m))); } E::verify_prepared(prepared_sig, prepared.iter()) } @@ -160,16 +147,19 @@ fn normalize_messages_and_signature( /// Simple unoptimized BLS signature verification. Useful for testing. pub fn verify_unoptimized(s: S) -> bool { - let signature = S::E::prepare_signature(s.signature().0); + let affine_signature = s.signature().0.into(); + if !S::E::verify_signature_in_signature_subgroup(&affine_signature) { + return false; + } + let signature = S::E::prepare_signature(affine_signature); let mut prepared = Vec::new(); for (message, public_key) in s.messages_and_publickeys() { - let Some(prepared_pk) = - validate_and_prepare_public_key::(public_key.borrow().0) - else { + let pk_affine: PublicKeyAffine = public_key.borrow().0.into(); + if !S::E::verify_public_key_in_public_key_subgroup(&pk_affine) { return false; - }; + } prepared.push(( - prepared_pk, + S::E::prepare_public_key(pk_affine), S::E::prepare_signature(message.borrow().hash_to_signature_curve::()), )); } @@ -304,6 +294,12 @@ pub fn verify_using_aggregated_auxiliary_public_keys< // And verify the aggregate signature. let (affine_msgs, affine_sig) = normalize_messages_and_signature::(merged_msgs, signature); + // `verify_normalized` runs `verify_public_key_in_public_key_subgroup` + // on every entry of `merged_pks`. That subgroup check is critical for + // this scheme: the auxiliary-key construction binds `aggregated_aux_pub_key` + // to the signers via the pseudo-random scalar, and a public key that + // sits outside the prime-order subgroup would let an attacker forge a + // matching `aggregated_aux_pub_key` and pass verification. Do not drop it. verify_normalized::(&merged_pks, &affine_msgs, affine_sig) } @@ -386,47 +382,145 @@ fn verify_with_gaussian_elimination(s: S) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::single::{SecretKeyVT, SignedMessage}; - use crate::{Keypair, Message, PublicKey, UsualBLS}; - use ark_bls12_381::{Bls12_381, Fq, G1Affine}; - use ark_ec::AffineRepr; - use ark_ff::{UniformRand, Zero}; - use ark_serialize::Valid; + use crate::single::SignedMessage; + use crate::{Keypair, Message, PublicKey, Signature, UsualBLS}; + use ark_bls12_381::{Bls12_381, Fq, Fq2, G1Affine, G2Affine}; + use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; + use ark_ff::{BitIteratorBE, PrimeField, UniformRand}; use rand::rngs::StdRng; use rand::SeedableRng; type EB = UsualBLS; - /// Keypair with secret scalar zero and identity public key. The - /// produced signature is also identity, so every pairing equation - /// reduces to `identity == identity` and an unprotected verifier - /// would accept. Any rejection observed in the verifier tests - /// below is therefore attributable to `validate_public_key`. - fn identity_keypair() -> Keypair { - Keypair { - public: PublicKey::(::PublicKeyGroup::zero()), - secret: SecretKeyVT::(::Scalar::zero()) - .into_split(StdRng::from_seed([0u8; 32])), + /// Multiply `point` by an integer `scalar` (big-endian limbs) using + /// plain double-and-add over the curve. We deliberately bypass the + /// `Group::mul_bigint` path because arkworks' BLS12-381 curves use a + /// GLV-optimized scalar multiplication that decomposes the scalar + /// modulo `r`. For a point `P` outside the prime-order subgroup the + /// GLV identity `ϕ(P) = λ·P` does not hold, and any scalar that is + /// `0 mod r` would also reduce away, breaking the cofactor-projection + /// arithmetic we rely on below. Bit-by-bit double-and-add computes + /// `scalar·P` as a literal integer multiple on the curve. + fn mul_by_int_no_glv>(point: G, scalar: S) -> G { + let mut result = G::zero(); + for b in BitIteratorBE::without_leading_zeros(scalar) { + result.double_in_place(); + if b { + result += point; + } } + result } - fn identity_signed() -> SignedMessage { - identity_keypair().signed_message(&Message::new(b"ctx", b"test message")) - } - - /// A G1 point on the curve but outside the prime-order subgroup. - fn non_subgroup_g1() -> G1Affine { + /// Sample a random point on E(Fq), then multiply by `r`. This kills + /// the r-torsion component and leaves a point in the cofactor + /// subgroup G1[h₁]. Such a point pairs to 1 with any G2[r] element + /// under the optimal-ate pairing — so attaching it to a public key + /// shifts the key out of the prime-order subgroup *without disturbing + /// the verification equation*. The subgroup check is therefore the + /// only thing that can reject inputs built from it. + fn cofactor_subgroup_g1() -> G1Affine { let mut rng = StdRng::from_seed([42u8; 32]); + let r = ::MODULUS; loop { let x = Fq::rand(&mut rng); - if let Some(point) = G1Affine::get_point_from_x_unchecked(x, false) { - if point.check().is_err() { - return point; - } + let Some(point) = G1Affine::get_point_from_x_unchecked(x, false) else { + continue; + }; + let projected = mul_by_int_no_glv(point.into_group(), r).into_affine(); + if !projected.is_zero() { + return projected; + } + } + } + + /// G2 analogue of `cofactor_subgroup_g1`: a point in `E'(Fq²)[h₂]`, + /// obtained by multiplying a random `E'(Fq²)` point by `r` to clear + /// the r-torsion component. + /// + /// **Asymmetry with the G1 case.** Unlike the G1 cofactor point, + /// this one does **not** pair to 1 with `G1[r]` under optimal-ate pairing. + /// For BLS12-381's sextic twist, `E'(Fq²)` has order exactly `r·h₂` + /// with `gcd(r, h₂) = 1`, so `E'(Fq²)[r]` is cyclic of order `r` and + /// equals G2 itself — there is no Fq²-rational "anti-G2" subspace. + /// The other ψ-eigenspace of `E'[r]` lives in `E'(Fq¹²)` and cannot + /// be represented as a `G2Affine`. Consequently, the cofactor-attack + /// shape `e(g1, q_g2) = 1` simply does not hold on the G2 side of + /// BLS12-381: a non-G2 candidate in `E'(Fq²)` either fails the + /// subgroup check **or** produces a non-trivial Fq¹² residue through + /// the pairing. + /// + /// The point produced here still drives the rejection tests: it sits + /// outside `E'(Fq²)[r]` (because `[r]·random` retains the h₂-cofactor + /// component) and is rejected by `verify_signature_in_signature_subgroup`. + /// The pairing equation would also reject it, so on the G2 side the + /// subgroup check is belt-and-braces, in contrast to the G1 side + /// where it is the sole defence. + fn cofactor_subgroup_g2() -> G2Affine { + let mut rng = StdRng::from_seed([43u8; 32]); + let r = ::MODULUS; + loop { + let x = Fq2::rand(&mut rng); + let Some(point) = G2Affine::get_point_from_x_unchecked(x, false) else { + continue; + }; + let projected = mul_by_int_no_glv(point.into_group(), r).into_affine(); + if !projected.is_zero() { + return projected; } } } + /// Build a `SignedMessage` whose public key carries a G1-cofactor + /// component. Under optimal-ate `e(Q_g1, ·) = 1`, so the pairing + /// equation still balances — an unprotected verifier accepts. The + /// G1 subgroup check is the only thing that catches this. + /// + /// Construction (E = UsualBLS, PK in G1, sig in G2): + /// pk' = sk · (g1 + Q_g1) = pk + sk · Q_g1 with Q_g1 ∈ G1[h₁] + /// sig' = sk · H(m) (unchanged) + fn signed_with_g1_cofactor_pk() -> SignedMessage { + let message = Message::new(b"ctx", b"test message"); + let mut keypair = Keypair::::generate(StdRng::from_seed([0u8; 32])); + let signed = keypair.signed_message(&message); + let sk = keypair.into_vartime().secret.0; + + let q_g1: ::PublicKeyGroup = cofactor_subgroup_g1().into(); + let bad_pk = q_g1 * sk + signed.publickey.0; + + SignedMessage { + message: signed.message, + publickey: PublicKey::(bad_pk), + signature: signed.signature, + } + } + + /// Build a `SignedMessage` whose signature carries a G2-cofactor + /// component. Unlike the G1 case the pairing equation does **not** + /// remain balanced (see `cofactor_subgroup_g2` for why), so on + /// BLS12-381 this attack would be rejected by `verify_prepared` even + /// without the subgroup check. It still drives the rejection tests + /// because the subgroup check trips first. + /// + /// Construction: + /// pk' = sk · g1 (unchanged) + /// sig' = sk · (H(m) + Q_g2) = sig + sk · Q_g2 with Q_g2 ∈ E'(Fq²)[h₂] + fn signed_with_g2_cofactor_sig() -> SignedMessage { + let message = Message::new(b"ctx", b"test message"); + let mut keypair = Keypair::::generate(StdRng::from_seed([0u8; 32])); + let signed = keypair.signed_message(&message); + let sk = keypair.into_vartime().secret.0; + + let q_g2: ::SignatureGroup = cofactor_subgroup_g2().into(); + let bad_sig = q_g2 * sk + signed.signature.0; + + SignedMessage { + message: signed.message, + publickey: signed.publickey, + signature: Signature::(bad_sig), + } + } + #[test] fn verify_simple_single_signature() { let good = Message::new(b"ctx", b"test message"); @@ -457,29 +551,79 @@ mod tests { assert!(verify_unoptimized(&signed)); } + /// Sanity check that `e(Q_g1, g2_gen) = 1`: this is the mathematical + /// basis for the G1-side cofactor attack — without it the G1 + /// rejection tests would have no meaning. + /// + /// We do **not** assert the symmetric `e(g1_gen, Q_g2) = 1`. For + /// BLS12-381's sextic twist no such non-trivial `Q_g2 ∈ E'(Fq²)` + /// exists (see `cofactor_subgroup_g2`); the G2 rejection tests + /// succeed because the subgroup check **and** the pairing equation + /// independently reject the doctored input, not because of a + /// pair-to-1 property. #[test] - fn verify_simple_rejects_identity_pk() { - assert!(!verify_simple(&identity_signed())); + fn cofactor_points_pair_to_one() { + use ark_ec::pairing::Pairing; + let q_g1 = cofactor_subgroup_g1(); + let g2_gen = G2Affine::generator(); + let p1 = Bls12_381::pairing(q_g1, g2_gen); + if !p1.0.is_one() { + eprintln!("e(Q_g1, g2_gen) = {:?} (expected 1)", p1.0); + } + assert!(p1.0.is_one(), "e(Q_g1, g2_gen) != 1"); } + /// With only the G1-cofactor component spliced into the public key, + /// the underlying pairing equation still balances (because + /// `e(Q_g1, ·) = 1`). `verify_prepared`, which performs no subgroup + /// validation, accepts. This proves the high-level verifier + /// rejections below are attributable solely to the subgroup checks, + /// not to broken pairing math. #[test] - fn verify_unoptimized_rejects_identity_pk() { - assert!(!verify_unoptimized(&identity_signed())); + fn verify_prepared_accepts_cofactor_components() { + let bad = signed_with_g1_cofactor_pk(); + let prepared_pk = ::prepare_public_key(bad.publickey.0); + let prepared_msg = ::prepare_signature( + bad.message.hash_to_signature_curve::(), + ); + let prepared_sig = ::prepare_signature(bad.signature.0); + let pairs = [(prepared_pk, prepared_msg)]; + assert!(::verify_prepared(prepared_sig, pairs.iter())); } #[test] - fn verify_with_distinct_messages_rejects_identity_pk() { - assert!(!verify_with_distinct_messages(&identity_signed(), true)); + fn verify_simple_rejects_g1_cofactor_pk() { + assert!(!verify_simple(&signed_with_g1_cofactor_pk())); } #[test] - fn validate_and_prepare_public_key_rejects_identity() { - let identity = PublicKeyAffine::::zero(); - assert!(validate_and_prepare_public_key::(identity).is_none()); + fn verify_unoptimized_rejects_g1_cofactor_pk() { + assert!(!verify_unoptimized(&signed_with_g1_cofactor_pk())); } #[test] - fn validate_and_prepare_public_key_rejects_non_subgroup() { - assert!(validate_and_prepare_public_key::(non_subgroup_g1()).is_none()); + fn verify_with_distinct_messages_rejects_g1_cofactor_pk() { + assert!(!verify_with_distinct_messages( + &signed_with_g1_cofactor_pk(), + true + )); + } + + #[test] + fn verify_simple_rejects_g2_cofactor_sig() { + assert!(!verify_simple(&signed_with_g2_cofactor_sig())); + } + + #[test] + fn verify_unoptimized_rejects_g2_cofactor_sig() { + assert!(!verify_unoptimized(&signed_with_g2_cofactor_sig())); + } + + #[test] + fn verify_with_distinct_messages_rejects_g2_cofactor_sig() { + assert!(!verify_with_distinct_messages( + &signed_with_g2_cofactor_sig(), + true + )); } } From 397b38097a0b0a33b573d61fc523e2cbdea4b1db Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 2 Jun 2026 11:30:45 -0400 Subject: [PATCH 07/14] - Remove comments about affine coordinates - Remove redundant imports - fix shifting 6bit instead of 64bits in delinear.rs (srlabs finding 659 --- src/experimental/delinear.rs | 2 +- src/single.rs | 22 +++++----------------- src/verifiers.rs | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/experimental/delinear.rs b/src/experimental/delinear.rs index 67f1367..ca14ec0 100644 --- a/src/experimental/delinear.rs +++ b/src/experimental/delinear.rs @@ -122,7 +122,7 @@ impl Delinearized { let (x, y) = array_refs!(&b, 8, 8); let mut x: ::BigInt = u64::from_le_bytes(*x).into(); let y: ::BigInt = u64::from_le_bytes(*y).into(); - x <<= 6; //warning: use of deprecated method `ark_ff::BigInteger::muln`: please use the operator `<<` instead : x.muln(64); + x <<= 64; //warning: use of deprecated method `ark_ff::BigInteger::muln`: please use the operator `<<` instead : x.muln(64); x.add_with_carry(&y); ::from_bigint(x).unwrap() } diff --git a/src/single.rs b/src/single.rs index ce35a76..86e2d1b 100644 --- a/src/single.rs +++ b/src/single.rs @@ -28,7 +28,7 @@ use alloc::{vec, vec::Vec}; use ark_ff::field_hashers::{DefaultFieldHasher, HashToField}; use ark_ff::{UniformRand, Zero}; -use ark_ec::{AffineRepr, CurveGroup}; +use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; use ark_serialize::{ CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, @@ -106,11 +106,7 @@ impl SecretKeyVT { /// Derive our public key from our secret key pub fn into_public(&self) -> PublicKey { - // TODO str4d never decided on projective vs affine here, so benchmark both versions. - PublicKey(::Affine::generator().into_group() * self.0) - // let mut g = ::one(); - // g *= self.0; - // PublicKey(p) + PublicKey(::generator() * self.0) } } @@ -300,19 +296,10 @@ impl SecretKey { /// We do not resplit for side channel protections here since /// this call should be rare. pub fn into_public(&self) -> PublicKey { - let generator = ::Affine::generator(); + let generator = ::generator(); let mut publickey = generator * self.key[0]; - publickey += generator.into_group() * self.key[1]; + publickey += generator * self.key[1]; PublicKey(publickey) - // TODO str4d never decided on projective vs affine here, so benchmark this. - /* - let mut x = ::one(); - x *= self.0; - let y = ::one(); - y *= self.1; - x += &y; - PublicKey(x) - */ } } @@ -485,6 +472,7 @@ impl Signature { /// Verify a single BLS signature pub fn verify(&self, message: &Message, publickey: &PublicKey) -> bool { let pk_affine: ::PublicKeyGroupAffine = publickey.0.into(); + //This is redundant if we have verified public key's PoP which reject out of subgroup keys if !E::verify_public_key_in_public_key_subgroup(&pk_affine) { return false; } diff --git a/src/verifiers.rs b/src/verifiers.rs index 3271c69..158e8d6 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -8,7 +8,7 @@ use core::borrow::Borrow; // We use BTreeMap instead of HashMap for no_std compatibility. use alloc::collections::BTreeMap; use ark_ec::AffineRepr; -use ark_ff::{One, Zero, field_hashers::{DefaultFieldHasher, HashToField}}; +use ark_ff::{field_hashers::{DefaultFieldHasher, HashToField}}; use ark_serialize::CanonicalSerialize; use digest::FixedOutputReset; From d9f078dcb7297bf19f7f51a99b5b22443c15d462 Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 21 Apr 2026 11:20:50 -0400 Subject: [PATCH 08/14] - Remove SingleAggergator. - Change Verifier to support verifiying multimessage signature using aux pubkey - Update example and tests. --- README.md | 36 +- ...gate_with_public_key_in_signature_group.rs | 31 +- examples/experimental/aggregated_with_pop.rs | 4 +- src/experimental/bench.rs | 12 +- src/experimental/bit.rs | 3 +- src/lib.rs | 49 ++- ...ti_pop_aggregator.rs => pop_aggregator.rs} | 226 ++++++++++-- src/single_pop_aggregator.rs | 344 ------------------ src/verifiers.rs | 156 +++++--- 9 files changed, 367 insertions(+), 494 deletions(-) rename src/{multi_pop_aggregator.rs => pop_aggregator.rs} (54%) delete mode 100644 src/single_pop_aggregator.rs diff --git a/README.md b/README.md index 68be7ed..fb242f8 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Assuming you already have proofs-of-possession, then you'll want to do aggregati The library offers method for generating and verifying proof of positions both based on BLS and [Schnorr Signature](https://en.wikipedia.org/wiki/Schnorr_signature) which is faster to verify than when using BLS signature itself as proof of position. The following example demonstrate how to generate and verify proof of positions and then using `SignatureAggregatorAssumingPoP` to batch and verify multiple BLS signatures. ```rust -use w3f_bls::{Keypair,PublicKey,ZBLS,Message,Signed, ProofOfPossessionGenerator, ProofOfPossession, experimental::schnorr_pop::{SchnorrPoP}, multi_pop_aggregator::MultiMessageSignatureAggregatorAssumingPoP}; +use w3f_bls::{Keypair,PublicKey,ZBLS,Message,Signed, ProofOfPossessionGenerator, ProofOfPossession, experimental::schnorr_pop::{SchnorrPoP}, pop_aggregator::SignatureAggregatorAssumingPoP}; use sha2::Sha256; let mut keypairs = [Keypair::::generate(::rand::thread_rng()), Keypair::::generate(::rand::thread_rng())]; @@ -92,7 +92,7 @@ let pops = keypairs.iter_mut().map(|k|(ProofOfPossessionGenerator::>::verify(pop,publickey)); publickey}).collect::>(); let batch_poped = msgs.iter().zip(publickeys).zip(sigs).fold( - MultiMessageSignatureAggregatorAssumingPoP::::new(), + SignatureAggregatorAssumingPoP::::new(), |mut bpop,((message, publickey),sig)| { bpop.add_message_n_publickey(message, &publickey); bpop.add_signature(&sig); bpop } ); assert!(batch_poped.verify()) @@ -106,12 +106,11 @@ The scheme introduced in [`our recent paper`](https://eprint.iacr.org/2022/1611) ```rust use sha2::Sha256; use ark_bls12_377::Bls12_377; -use ark_ff::Zero; use rand::thread_rng; use w3f_bls::{ - single_pop_aggregator::SignatureAggregatorAssumingPoP, DoubleNuggetBLS, EngineBLS, Keypair, - Message, NuggetPublicKey, PublicKey, PublicKeyInSignatureGroup, Signed, TinyBLS, TinyBLS377, + pop_aggregator::SignatureAggregatorAssumingPoP, DoubleNuggetBLS, EngineBLS, Keypair, + Message, NuggetPublicKey, PublicKeyInSignatureGroup, TinyBLS, TinyBLS377, }; @@ -125,28 +124,13 @@ let pub_keys_in_sig_grp: Vec> = keypairs .map(|k| DoubleNuggetBLS::::into_nugget_double_public_key(k).into_public_key_in_signature_group()) .collect(); -let mut prover_aggregator = - SignatureAggregatorAssumingPoP::::new(message.clone()); -let mut aggregated_public_key = - PublicKey::(::PublicKeyGroup::zero()); +let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); -//sign and aggegate -let _ = keypairs - .iter_mut() - .map(|k| { - prover_aggregator.add_signature(&k.sign(&message)); - aggregated_public_key.0 += k.public.0; - }) - .count(); - -let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(message); - -verifier_aggregator.add_signature(&(&prover_aggregator).signature()); - -//aggregate public keys in signature group -verifier_aggregator.add_publickey(&aggregated_public_key); - -pub_keys_in_sig_grp.iter().for_each(|pk| {verifier_aggregator.add_auxiliary_public_key(pk);}); +//sign, aggregate, and add (publickey, aux) pairs +for (k, aux) in keypairs.iter_mut().zip(pub_keys_in_sig_grp.iter()) { + verifier_aggregator.add_signature(&k.sign(&message)); + verifier_aggregator.add_message_n_publickey(&message, &(k.public, *aux)); +} assert!( verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), diff --git a/examples/aggregate_with_public_key_in_signature_group.rs b/examples/aggregate_with_public_key_in_signature_group.rs index 9b49124..dff7d2d 100644 --- a/examples/aggregate_with_public_key_in_signature_group.rs +++ b/examples/aggregate_with_public_key_in_signature_group.rs @@ -2,15 +2,13 @@ use sha2::Sha256; #[cfg(feature = "std")] use w3f_bls::{ - single_pop_aggregator::SignatureAggregatorAssumingPoP, EngineBLS, Keypair, Message, NuggetBLS, - PublicKey, PublicKeyInSignatureGroup, Signed, TinyBLS, TinyBLS377, + pop_aggregator::SignatureAggregatorAssumingPoP, EngineBLS, Keypair, Message, NuggetBLS, + PublicKeyInSignatureGroup, TinyBLS, TinyBLS377, }; #[cfg(feature = "std")] use ark_bls12_377::Bls12_377; #[cfg(feature = "std")] -use ark_ff::Zero; -#[cfg(feature = "std")] use rand::thread_rng; /// Run using @@ -31,26 +29,13 @@ fn main() { .iter() .map(|k| NuggetBLS::<_, ::SignatureGroup>::into_public_key_in_signature_group(k)) .collect(); - let mut prover_aggregator = - SignatureAggregatorAssumingPoP::::new(message.clone()); - let mut aggregated_public_key = - PublicKey::(::PublicKeyGroup::zero()); - - //sign and aggegate - keypairs.iter_mut().for_each(|k| { - prover_aggregator.add_signature(&k.sign(&message)); - aggregated_public_key.0 += k.public.0; - }); - - let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(message); - //get the signature and already aggregated public key from the prover - verifier_aggregator.add_signature(&(&prover_aggregator).signature()); - verifier_aggregator.add_publickey(&aggregated_public_key); + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); - //aggregate public keys in signature group - pub_keys_in_sig_grp.iter().for_each(|pk| { - verifier_aggregator.add_auxiliary_public_key(pk); - }); + //sign, aggregate, and add (publickey, aux) pairs + for (k, aux) in keypairs.iter_mut().zip(pub_keys_in_sig_grp.iter()) { + verifier_aggregator.add_signature(&k.sign(&message)); + verifier_aggregator.add_message_n_publickey(&message, &(k.public, *aux)); + } assert!( verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), diff --git a/examples/experimental/aggregated_with_pop.rs b/examples/experimental/aggregated_with_pop.rs index f2f29f4..ab4ef0b 100644 --- a/examples/experimental/aggregated_with_pop.rs +++ b/examples/experimental/aggregated_with_pop.rs @@ -3,7 +3,7 @@ use sha2::Sha256; #[cfg(feature = "std")] use w3f_bls::{ experimental::schnorr_pop::SchnorrPoP, - multi_pop_aggregator::MultiMessageSignatureAggregatorAssumingPoP, + pop_aggregator::SignatureAggregatorAssumingPoP, Keypair, Message, ProofOfPossession, ProofOfPossessionGenerator, PublicKey, Signed, ZBLS, }; @@ -53,7 +53,7 @@ fn main() { //now that we have confidence in keys we can verify the batched signature let batch_poped = msgs.iter().zip(publickeys).zip(sigs).fold( - MultiMessageSignatureAggregatorAssumingPoP::::new(), + SignatureAggregatorAssumingPoP::::new(), |mut bpop, ((message, publickey), sig)| { bpop.add_message_n_publickey(message, &publickey); bpop.add_signature(&sig); diff --git a/src/experimental/bench.rs b/src/experimental/bench.rs index 97ab102..5ba6470 100644 --- a/src/experimental/bench.rs +++ b/src/experimental/bench.rs @@ -5,7 +5,7 @@ extern crate test; const NO_OF_MULTI_SIG_SIGNERS: usize = 100; use crate::chaum_pedersen_signature::ChaumPedersenSigner; use crate::chaum_pedersen_signature::ChaumPedersenVerifier; -use crate::multi_pop_aggregator::MultiMessageSignatureAggregatorAssumingPoP; +use crate::pop_aggregator::SignatureAggregatorAssumingPoP; use crate::Keypair; use crate::Message; use crate::Signature as BLSSignature; @@ -55,7 +55,7 @@ use crate::PublicKeyInSignatureGroup; // let mut pub_keys_in_sig_grp : Vec> = keypairs.iter().map(|k| k.into_public_key_in_signature_group()).collect(); // let mut aggregated_public_key = PublicKey::(::PublicKeyGroup::zero()); -// let mut aggregator = MultiMessageSignatureAggregatorAssumingPoP::::new(); +// let mut aggregator = SignatureAggregatorAssumingPoP::::new(); // for k in &mut keypairs { // aggregator.aggregate(&k.signed_message(message)); @@ -63,7 +63,7 @@ use crate::PublicKeyInSignatureGroup; // } // b.iter(|| { -// let mut verifier_aggregator = MultiMessageSignatureAggregatorAssumingPoP::::new(); +// let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); // let mut verifier_aggregated_public_key = PublicKey::(::PublicKeyGroup::zero()); // verifier_aggregator.add_signature(&aggregator.signature); @@ -85,7 +85,7 @@ use crate::PublicKeyInSignatureGroup; // let message = Message::new(b"ctx",b"test message"); // b.iter(|| { -// let mut aggregator = MultiMessageSignatureAggregatorAssumingPoP::::new(); +// let mut aggregator = SignatureAggregatorAssumingPoP::::new(); // let mut aggregated_public_key = PublicKey::(::PublicKeyGroup::zero()); // for k in &mut keypairs { @@ -102,7 +102,7 @@ use crate::PublicKeyInSignatureGroup; // let mut keypairs = generate_many_keypairs(NO_OF_MULTI_SIG_SIGNERS); // let mut pub_keys_in_sig_grp : Vec> = keypairs.iter().map(|k| k.into_public_key_in_signature_group()).collect(); -// let mut aggregator = MultiMessageSignatureAggregatorAssumingPoP::::new(); +// let mut aggregator = SignatureAggregatorAssumingPoP::::new(); // let mut aggregated_public_key = PublicKey::(::PublicKeyGroup::zero()); // for k in &mut keypairs { @@ -111,7 +111,7 @@ use crate::PublicKeyInSignatureGroup; // } // b.iter(|| { -// let mut verifier_aggregator = MultiMessageSignatureAggregatorAssumingPoP::::new(); +// let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); // verifier_aggregator.add_signature(&aggregator.signature); // verifier_aggregator.add_message_n_publickey(&message, &aggregated_public_key); diff --git a/src/experimental/bit.rs b/src/experimental/bit.rs index f53ddde..7f196ed 100644 --- a/src/experimental/bit.rs +++ b/src/experimental/bit.rs @@ -660,8 +660,7 @@ mod tests { assert!(bitsig1.merge(&bitsig2).is_err()); let mut multimsg = - crate::multi_pop_aggregator::MultiMessageSignatureAggregatorAssumingPoP::::new( - ); + crate::pop_aggregator::SignatureAggregatorAssumingPoP::::new(); multimsg.aggregate(&bitsig1); multimsg.aggregate(&bitsig2); assert!(multimsg.verify()); // verifiers::verify_with_distinct_messages(&dms,true) diff --git a/src/lib.rs b/src/lib.rs index d839f4f..03c95ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,7 @@ extern crate sha3; extern crate alloc; +use ark_ff::Zero; use core::borrow::Borrow; use digest::DynDigest; @@ -119,8 +120,7 @@ pub mod serialize; pub mod single; pub mod verifiers; -pub mod multi_pop_aggregator; -pub mod single_pop_aggregator; +pub mod pop_aggregator; #[cfg(feature = "experimental")] pub mod experimental; @@ -138,7 +138,48 @@ pub use single::{Keypair, KeypairVT, PublicKey, SecretKey, SecretKeyVT, Signatur use alloc::vec::Vec; -/// Internal message hash size. +/// Public key types usable in the [`Signed`] trait. +/// +/// Standard BLS uses [`PublicKey`] which carries only the key in the +/// public-key group. Nugget-style schemes use [`NuggetDoublePublicKey`] +/// (or similar) which carries keys in **both** curve groups and thus +/// supports auxiliary-key verification. +pub trait GeneralizedBLSPublicKey { + /// The public key in the public-key group. + fn public_key(&self) -> PublicKey; + + /// The auxiliary public key in the signature group. + /// Returns zero by default (no auxiliary key). + fn public_key_in_signature_group(&self) -> nugget::PublicKeyInSignatureGroup { + nugget::PublicKeyInSignatureGroup(E::SignatureGroup::zero()) + } +} + +impl GeneralizedBLSPublicKey for PublicKey { + fn public_key(&self) -> PublicKey { + *self + } +} + +impl GeneralizedBLSPublicKey for (PublicKey, nugget::PublicKeyInSignatureGroup) { + fn public_key(&self) -> PublicKey { + self.0 + } + fn public_key_in_signature_group(&self) -> nugget::PublicKeyInSignatureGroup { + self.1 + } +} + +impl<'a, E: EngineBLS, T: GeneralizedBLSPublicKey> GeneralizedBLSPublicKey for &'a T { + fn public_key(&self) -> PublicKey { + (*self).public_key() + } + fn public_key_in_signature_group(&self) -> nugget::PublicKeyInSignatureGroup { + (*self).public_key_in_signature_group() + } +} + +/// Internal message hash size. /// /// We choose 256 bits here so that birthday bound attacks cannot /// find messages with the same hash. @@ -263,7 +304,7 @@ pub trait Signed: Sized { fn signature(&self) -> Signature; type M: Borrow; // = Message; - type PKG: Borrow>; // = PublicKey; + type PKG: GeneralizedBLSPublicKey; // = PublicKey; /// Iterator over, messages and public key reference pairs. type PKnM: Iterator + ExactSizeIterator; diff --git a/src/multi_pop_aggregator.rs b/src/pop_aggregator.rs similarity index 54% rename from src/multi_pop_aggregator.rs rename to src/pop_aggregator.rs index 81a07b2..5aee8d8 100644 --- a/src/multi_pop_aggregator.rs +++ b/src/pop_aggregator.rs @@ -34,15 +34,19 @@ // Aside about proof-of-possession in the DLOG setting // https://twitter.com/btcVeg/status/1085490561082183681 -use core::borrow::Borrow; // BorrowMut -// We use BTreeMap instead of BTreeMap for no_std compatibility. +use core::borrow::Borrow; +// We use BTreeMap instead of HashMap for no_std compatibility. use alloc::collections::BTreeMap; use ark_ff::Zero; -use super::verifiers::verify_with_distinct_messages; +use super::verifiers::{ + verify_using_aggregated_auxiliary_public_keys, verify_with_distinct_messages, +}; use super::*; +use digest::FixedOutputReset; + /// Batch or aggregate BLS signatures with attached messages and /// signers, for whom we previously checked proofs-of-possession. /// @@ -72,69 +76,88 @@ use super::*; /// the `ProofsOfPossession` trait tooling permits both enforce the /// proofs-of-possession and provide a compact serialization. /// We see no reason to support serialization for this type as present. -// +/// message assumptions, or other aggre +/// /// In principle, one might combine proof-of-possession with distinct /// message assumptions, or other aggregation strategies, when /// verifiers have only observed a subset of the proofs-of-possession, /// but this sounds complex or worse fragile. /// -// TODO: Implement gaussian elimination verification scheme. +/// TODO: Implement gaussian elimination verification scheme. +use nugget::PublicKeyInSignatureGroup; use single::PublicKey; -/// ProofOfPossion trait which should be implemented by secret #[derive(Clone)] -pub struct MultiMessageSignatureAggregatorAssumingPoP { - messages_n_publickeys: BTreeMap>, +pub struct SignatureAggregatorAssumingPoP { + messages_n_publickeys: BTreeMap, PublicKeyInSignatureGroup)>, signature: Signature, } -impl MultiMessageSignatureAggregatorAssumingPoP { - pub fn new() -> MultiMessageSignatureAggregatorAssumingPoP { - MultiMessageSignatureAggregatorAssumingPoP { +impl SignatureAggregatorAssumingPoP { + pub fn new() -> SignatureAggregatorAssumingPoP { + SignatureAggregatorAssumingPoP { messages_n_publickeys: BTreeMap::new(), signature: Signature(E::SignatureGroup::zero()), } } /// Add only a `Signature` to our internal signature. - /// - /// Useful for constructing an aggregate signature, but we - /// recommend instead using a custom types like `BitPoPSignedMessage`. pub fn add_signature(&mut self, signature: &Signature) { self.signature.0 += &signature.0; } - /// Add only a `Message` and `PublicKey` to our internal data. + /// Add a `Message` and public key to our internal data. /// - /// Useful for constructing an aggregate signature, but we - /// recommend instead using a custom types like `BitPoPSignedMessage`. - pub fn add_message_n_publickey(&mut self, message: &Message, publickey: &PublicKey) { + /// Public keys signing the same message are merged so that each + /// distinct message ends up paired with a single aggregated key. + /// If the public key carries an auxiliary key in the signature group, + /// it is automatically aggregated as well. + pub fn add_message_n_publickey(&mut self, message: &Message, publickey: &impl GeneralizedBLSPublicKey) { + let pk = publickey.public_key(); + let aux = publickey.public_key_in_signature_group(); self.messages_n_publickeys .entry(message.clone()) - .and_modify(|pk0| pk0.0 += &publickey.0) - .or_insert(*publickey); + .and_modify(|(pk0, aux0)| { + pk0.0 += &pk.0; + aux0.0 += &aux.0; + }) + .or_insert((pk, aux)); } - /// Aggregage BLS signatures assuming they have proofs-of-possession + /// Aggregate BLS signatures assuming they have proofs-of-possession. + /// + /// Folds in every `(message, publickey)` pair carried by `signed` + /// and adds its signature to our running total. Public keys signing + /// the same message are merged together. pub fn aggregate<'a, S>(&mut self, signed: &'a S) where &'a S: Signed, - <&'a S as Signed>::PKG: Borrow>, { let signature = signed.signature(); - for (message, pubickey) in signed.messages_and_publickeys() { - self.add_message_n_publickey(message.borrow(), pubickey.borrow()); + for (message, publickey) in signed.messages_and_publickeys() { + self.add_message_n_publickey(message.borrow(), &publickey); } self.add_signature(&signature); } + + pub fn verify_using_aggregated_auxiliary_public_keys< + RandomOracle: FixedOutputReset + Default + Clone, + >( + &self, + ) -> bool { + verify_using_aggregated_auxiliary_public_keys::( + self, + true, + ) + } } -impl<'a, E: EngineBLS> Signed for &'a MultiMessageSignatureAggregatorAssumingPoP { +impl<'a, E: EngineBLS> Signed for &'a SignatureAggregatorAssumingPoP { type E = E; type M = &'a Message; - type PKG = &'a PublicKey; - type PKnM = alloc::collections::btree_map::Iter<'a, Message, PublicKey>; + type PKG = &'a (PublicKey, PublicKeyInSignatureGroup); + type PKnM = alloc::collections::btree_map::Iter<'a, Message, (PublicKey, PublicKeyInSignatureGroup)>; fn messages_and_publickeys(self) -> Self::PKnM { self.messages_n_publickeys.iter() @@ -158,12 +181,16 @@ impl<'a, E: EngineBLS> Signed for &'a MultiMessageSignatureAggregatorAssumingPoP #[cfg(test)] mod tests { + use crate::EngineBLS; use crate::Keypair; use crate::Message; + use crate::TinyBLS; use crate::UsualBLS; use rand::SeedableRng; use rand::rngs::StdRng; + use sha2::Sha256; + use ark_bls12_377::Bls12_377; use ark_bls12_381::Bls12_381; use super::*; @@ -190,7 +217,7 @@ mod tests { Keypair::>::generate(StdRng::from_seed([1u8; 32])); let good_sig1 = keypair1.sign(&good); - let mut aggregated_sigs = MultiMessageSignatureAggregatorAssumingPoP::< + let mut aggregated_sigs = SignatureAggregatorAssumingPoP::< UsualBLS, >::new(); aggregated_sigs.add_signature(&good_sig0); @@ -216,7 +243,7 @@ mod tests { let good_sig0 = keypair.sign(&good0); let good_sig1 = keypair.sign(&good1); - let mut aggregated_sigs = MultiMessageSignatureAggregatorAssumingPoP::< + let mut aggregated_sigs = SignatureAggregatorAssumingPoP::< UsualBLS, >::new(); aggregated_sigs.add_signature(&good_sig0); @@ -244,7 +271,7 @@ mod tests { Keypair::>::generate(StdRng::from_seed([1u8; 32])); let good_sig1 = keypair1.sign(&good1); - let mut aggregated_sigs = MultiMessageSignatureAggregatorAssumingPoP::< + let mut aggregated_sigs = SignatureAggregatorAssumingPoP::< UsualBLS, >::new(); aggregated_sigs.add_signature(&good_sig0); @@ -267,7 +294,7 @@ mod tests { Keypair::>::generate(StdRng::from_seed([0u8; 32])); let good_sig = keypair.sign(&good); - let mut aggregated_sigs = MultiMessageSignatureAggregatorAssumingPoP::< + let mut aggregated_sigs = SignatureAggregatorAssumingPoP::< UsualBLS, >::new(); aggregated_sigs.add_signature(&good_sig); @@ -295,7 +322,7 @@ mod tests { Keypair::>::generate(StdRng::from_seed([1u8; 32])); let bad_sig1 = keypair1.sign(&bad1); - let mut aggregated_sigs = MultiMessageSignatureAggregatorAssumingPoP::< + let mut aggregated_sigs = SignatureAggregatorAssumingPoP::< UsualBLS, >::new(); aggregated_sigs.add_signature(&good_sig0); @@ -309,4 +336,139 @@ mod tests { "aggregated signature of a wrong message should not verify" ); } + + #[test] + fn test_aggregate_tiny_sigs_and_verify_in_g1() { + let message = Message::new(b"ctx", b"test message"); + let mut keypairs: Vec<_> = (0..3) + .into_iter() + .map(|i| Keypair::>::generate(StdRng::from_seed([i; 32]))) + .collect(); + let pub_keys_in_sig_grp: Vec> = keypairs + .iter() + .map(|k| { + nugget::NuggetBLS::< + TinyBLS, + as EngineBLS>::SignatureGroup, + >::into_public_key_in_signature_group(k) + }) + .collect(); + + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + + for (k, aux) in keypairs.iter_mut().zip(pub_keys_in_sig_grp.iter()) { + verifier_aggregator.add_signature(&k.sign(&message)); + verifier_aggregator.add_message_n_publickey(&message, &(k.public, *aux)); + } + + assert!( + verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verifying with honest auxilary public key should pass" + ); + + // Pair signer 0's public key with signer 1's aux key — should fail. + let mut bad_aggregator = SignatureAggregatorAssumingPoP::::new(); + + for (k, _aux) in keypairs.iter_mut().zip(pub_keys_in_sig_grp.iter()) { + bad_aggregator.add_signature(&k.sign(&message)); + } + // signer 0 gets signer 1's aux key + bad_aggregator.add_message_n_publickey(&message, &(keypairs[0].public, pub_keys_in_sig_grp[1])); + // signers 1 and 2 get their correct aux keys + bad_aggregator.add_message_n_publickey(&message, &(keypairs[1].public, pub_keys_in_sig_grp[1])); + bad_aggregator.add_message_n_publickey(&message, &(keypairs[2].public, pub_keys_in_sig_grp[2])); + + assert!( + !bad_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verification using non-matching auxilary public key should fail" + ); + } + + #[test] + fn test_aggregate_tiny_sigs_multi_messages_and_verify_in_g1() { + let messages: Vec = (0..3) + .map(|i| Message::new(b"ctx", &[b'm', b'0' + i as u8])) + .collect(); + let mut keypairs: Vec<_> = (0..3) + .map(|i| Keypair::>::generate(StdRng::from_seed([i; 32]))) + .collect(); + let pub_keys_in_sig_grp: Vec> = keypairs + .iter() + .map(|k| { + nugget::NuggetBLS::< + TinyBLS, + as EngineBLS>::SignatureGroup, + >::into_public_key_in_signature_group(k) + }) + .collect(); + + // Each signer signs their own distinct message, then we aggregate + // signatures, (message, publickey+aux) pairs into a single verifier aggregator. + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + + for ((k, m), aux) in keypairs.iter_mut().zip(messages.iter()).zip(pub_keys_in_sig_grp.iter()) { + verifier_aggregator.add_signature(&k.sign(m)); + verifier_aggregator.add_message_n_publickey(m, &(k.public, *aux)); + } + + assert!( + verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verifying multi-message aggregate with honest auxilary public keys should pass" + ); + + // Rebuild with signer 0 having signer 1's aux key — should fail. + let mut bad_aggregator = SignatureAggregatorAssumingPoP::::new(); + + for ((k, m), _aux) in keypairs.iter_mut().zip(messages.iter()).zip(pub_keys_in_sig_grp.iter()) { + bad_aggregator.add_signature(&k.sign(m)); + } + // signer 0 gets signer 1's aux key + bad_aggregator.add_message_n_publickey(&messages[0], &(keypairs[0].public, pub_keys_in_sig_grp[1])); + bad_aggregator.add_message_n_publickey(&messages[1], &(keypairs[1].public, pub_keys_in_sig_grp[1])); + bad_aggregator.add_message_n_publickey(&messages[2], &(keypairs[2].public, pub_keys_in_sig_grp[2])); + + assert!( + !bad_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "multi-message verification using non-matching auxilary public key should fail" + ); + } + + #[test] + fn test_aggregate_tiny_sigs_with_mislabeled_message_fails_verification_in_g1() { + // Each signer signs its real message, but in the verifier we + // deliberately pair every signer's public key with the *wrong* + // message (rotated by one). The aggregated signature is honest, + // yet the (message, publickey) bookkeeping the verifier consumes + // is a lie, so verification must fail. + let real_messages: Vec = (0..3) + .map(|i| Message::new(b"ctx", &[b'm', b'0' + i as u8])) + .collect(); + let mut keypairs: Vec<_> = (0..3) + .map(|i| Keypair::>::generate(StdRng::from_seed([i; 32]))) + .collect(); + let pub_keys_in_sig_grp: Vec> = keypairs + .iter() + .map(|k| { + nugget::NuggetBLS::< + TinyBLS, + as EngineBLS>::SignatureGroup, + >::into_public_key_in_signature_group(k) + }) + .collect(); + + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + + for ((i, k), aux) in keypairs.iter_mut().enumerate().zip(pub_keys_in_sig_grp.iter()) { + // Sign the real message... + verifier_aggregator.add_signature(&k.sign(&real_messages[i])); + // ...but lie to the verifier about which message this key signed. + let wrong_message = &real_messages[(i + 1) % real_messages.len()]; + verifier_aggregator.add_message_n_publickey(wrong_message, &(k.public, *aux)); + } + + assert!( + !verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verification must fail when public keys are paired with the wrong messages" + ); + } } diff --git a/src/single_pop_aggregator.rs b/src/single_pop_aggregator.rs deleted file mode 100644 index 344e51c..0000000 --- a/src/single_pop_aggregator.rs +++ /dev/null @@ -1,344 +0,0 @@ -//! ## Aggregation of BLS signatures using proofs-of-possession -//! -//! In this module, we provide the linear flavor of aggregate -//! BLS signature in which the verifiers has previously checked -//! proofs-of-possession for all public keys. In other words, -//! we simply add up the signatures because the previously checked -//! proofs-of-possession for all signers prevent rogue key attacks. -//! See the security arguments in The Power of Proofs-of-Possession: -//! Securing Multiparty Signatures against Rogue-Key Attacks -//! by Thomas Ristenpart and Scott Yilek at https://eprint.iacr.org/2007/264.pdf -//! -//! These proof-of-possession are simply self-signed certificates, -//! so a BLS signature by each secret key on its own public key. -//! Importantly, the message for this self-signed certificates -//! must uniquely distinguish the public key for which the signature -//! establishes a proof-of-possession. -//! It follows that each proof-of-possession has a unique message, -//! so distinct message aggregation is optimal for verifying them. -//! -//! In this vein, we note that aggregation under proofs-of-possession -//! cannot improve performance when signers sign distinct messages, -//! so proofs-of-possession help with aggregating votes in a concensus -//! protocol, but should never be used for accounts on a block chain. -//! -//! We assume here that users provide their own data structure for -//! proofs-of-poossession. We provide more structure for users who -//! one bit per vote in a concensus protocol: -//! You first verify the proofs-of-possession when building a data -//! structure that holds the voters' keys. You implement the -//! `ProofsOfPossession` trait for this data strtcuture as well, -//! so that the `BitPoPSignedMessage` type provides a signature -//! data type with reasonable sanity checks. - -// Aside about proof-of-possession in the DLOG setting -// https://twitter.com/btcVeg/status/1085490561082183681 - -use ark_ff::Zero; - -use super::verifiers::{ - verify_using_aggregated_auxiliary_public_keys, verify_with_distinct_messages, -}; -use super::*; - -use digest::FixedOutputReset; - -/// Batch or aggregate BLS signatures with attached messages and -/// signers, for whom we previously checked proofs-of-possession. -/// -/// In this type, we provide a high-risk low-level batching and -/// aggregation mechanism that merely adds up signatures under the -/// assumption that all required proofs-of-possession were previously -/// checked. -/// -/// We say a signing key has provided a proof-of-possession if the -/// verifier remembers having checked some self-signed certificate -/// by that key. It's insecure to use this aggregation strategy -/// without first cehcking proofs-of-possession. In particular -/// it is insecure to use this aggregation strategy when checking -/// proofs-of-possession, and could not improve performance anyways. -/// Distinct message aggregation is always optimal for checking -/// proofs-of-possession. Please see the module level doumentation -/// for additional discussion and notes on security. -/// -/// We foresee this type primarily being used to batch several -/// `BitPoPSignedMessage`s into one verification. We do not track -/// aggreggated public keys here, instead merging multiples signers -/// public keys anytime they sign the same message, so this type -/// essentially provides only fast batch verificartion. -/// In principle, our `add_*` methods suffice for building an actual -/// aggregate signature type. Yet, normally direct approaches like -/// `BitPoPSignedMessage` work better for aggregation because -/// the `ProofsOfPossession` trait tooling permits both enforce the -/// proofs-of-possession and provide a compact serialization. -/// We see no reason to support serialization for this type as present. -/// message assumptions, or other aggre -/// -/// In principle, one might combine proof-of-possession with distinct -/// message assumptions, or other aggregation strategies, when -/// verifiers have only observed a subset of the proofs-of-possession, -/// but this sounds complex or worse fragile. -/// -/// TODO: Implement gaussian elimination verification scheme. -use core::iter::once; - -use nugget::PublicKeyInSignatureGroup; -use single::PublicKey; - -#[derive(Clone)] -pub struct SignatureAggregatorAssumingPoP { - message: Message, - aggregated_publickey: PublicKey, - signature: Signature, - aggregated_auxiliary_public_key: PublicKeyInSignatureGroup, -} - -impl SignatureAggregatorAssumingPoP { - pub fn new(message: Message) -> SignatureAggregatorAssumingPoP { - SignatureAggregatorAssumingPoP { - message: message, - aggregated_publickey: PublicKey(E::PublicKeyGroup::zero()), - signature: Signature(E::SignatureGroup::zero()), - aggregated_auxiliary_public_key: PublicKeyInSignatureGroup(E::SignatureGroup::zero()), - } - } - - /// Add only a `Signature` to our internal signature. - /// - /// Useful for constructing an aggregate signature, but we - pub fn add_signature(&mut self, signature: &Signature) { - self.signature.0 += &signature.0; - } - - /// Add only a `PublicKey` to our internal data. - /// - /// Useful for constructing an aggregate signature, but we - /// recommend instead using a custom types like `BitPoPSignedMessage`. - pub fn add_publickey(&mut self, publickey: &PublicKey) { - self.aggregated_publickey.0 += publickey.0; - } - - /// Aggregate the auxiliary public keys in the signature group to be used verification using aux key - pub fn add_auxiliary_public_key( - &mut self, - publickey_in_signature_group: &PublicKeyInSignatureGroup, - ) { - self.aggregated_auxiliary_public_key.0 += publickey_in_signature_group.0; - } - - /// Returns the aggergated public key. - /// - pub fn aggregated_publickey(&self) -> PublicKey { - self.aggregated_publickey - } - - // /// Aggregage BLS signatures assuming they have proofs-of-possession - // /// TODO this function should return Result refusing to aggregate messages - // /// different than the message the aggregator is initiated at - // pub fn aggregate<'a,S>(&mut self, signed: &'a S) - // where - // &'a S: Signed, - // <&'a S as Signed>::PKG: Borrow>, - // { - // let signature = signed.signature(); - // for (message,pubickey) in signed.messages_and_publickeys() { - // self.add_message_n_publickey(message.borrow(),pubickey.borrow()); - // } - // self.add_signature(&signature); - // } - - pub fn verify_using_aggregated_auxiliary_public_keys< - RandomOracle: FixedOutputReset + Default + Clone, - >( - &self, - ) -> bool { - verify_using_aggregated_auxiliary_public_keys::( - self, - true, - self.aggregated_auxiliary_public_key.0, - ) - } -} - -impl<'a, E: EngineBLS> Signed for &'a SignatureAggregatorAssumingPoP { - type E = E; - - type M = Message; - type PKG = PublicKey; - type PKnM = ::core::iter::Once<(Message, PublicKey)>; - - fn messages_and_publickeys(self) -> Self::PKnM { - once((self.message.clone(), self.aggregated_publickey)) // TODO: Avoid clone - } - - fn signature(&self) -> Signature { - self.signature - } - - fn verify(self) -> bool { - // We have already aggregated distinct messages, so our distinct - // message verification code provides reasonable optimizations, - // except the public keys might not be normalized here. - // We foresee verification via gaussian elimination being faster, - // but requires affine keys or normalization. - verify_with_distinct_messages(self, true) - // TODO: verify_with_gaussian_elimination(self) - } -} - -#[cfg(test)] -mod tests { - - use crate::EngineBLS; - use crate::Keypair; - use crate::Message; - use crate::TinyBLS; - use crate::UsualBLS; - use rand::SeedableRng; - use rand::rngs::StdRng; - use sha2::Sha256; - - use ark_bls12_377::Bls12_377; - use ark_bls12_381::Bls12_381; - - use super::*; - - #[test] - fn verify_aggregate_single_message_single_signer() { - let good = Message::new(b"ctx", b"test message"); - - let mut keypair = - Keypair::>::generate(StdRng::from_seed([0u8; 32])); - let good_sig0 = keypair.sign(&good); - assert!(good_sig0.verify(&good, &keypair.public)); - } - - #[test] - fn verify_aggregate_single_message_multi_signers() { - let good = Message::new(b"ctx", b"test message"); - - let mut keypair0 = - Keypair::>::generate(StdRng::from_seed([0u8; 32])); - let good_sig0 = keypair0.sign(&good); - - let mut keypair1 = - Keypair::>::generate(StdRng::from_seed([1u8; 32])); - let good_sig1 = keypair1.sign(&good); - - let mut aggregated_sigs = - SignatureAggregatorAssumingPoP::>::new(good); - aggregated_sigs.add_signature(&good_sig0); - aggregated_sigs.add_signature(&good_sig1); - - aggregated_sigs.add_publickey(&keypair0.public); - aggregated_sigs.add_publickey(&keypair1.public); - - assert!( - aggregated_sigs.verify() == true, - "good aggregated signature of a single message with multiple key does not verify" - ); - } - - #[test] - fn verify_aggregate_single_message_repetative_signers() { - let good = Message::new(b"ctx", b"test message"); - - let mut keypair = - Keypair::>::generate(StdRng::from_seed([0u8; 32])); - let good_sig = keypair.sign(&good); - - let mut aggregated_sigs = - SignatureAggregatorAssumingPoP::>::new(good); - aggregated_sigs.add_signature(&good_sig); - aggregated_sigs.add_signature(&good_sig); - - aggregated_sigs.add_publickey(&keypair.public); - aggregated_sigs.add_publickey(&keypair.public); - - assert!( - aggregated_sigs.verify() == true, - "good aggregate of a repetitive signature does not verify" - ); - } - - #[test] - fn aggregate_of_signature_of_a_wrong_message_should_not_verify() { - let good0 = Message::new(b"ctx", b"Space over Tab"); - let bad1 = Message::new(b"ctx", b"Tab over Space"); - - let mut keypair0 = - Keypair::>::generate(StdRng::from_seed([0u8; 32])); - let good_sig0 = keypair0.sign(&good0); - - let mut keypair1 = - Keypair::>::generate(StdRng::from_seed([1u8; 32])); - let bad_sig1 = keypair1.sign(&bad1); - - let mut aggregated_sigs = SignatureAggregatorAssumingPoP::< - UsualBLS, - >::new(good0); - aggregated_sigs.add_signature(&good_sig0); - aggregated_sigs.add_signature(&bad_sig1); - - aggregated_sigs.add_publickey(&keypair0.public); - aggregated_sigs.add_publickey(&keypair1.public); - - assert!( - aggregated_sigs.verify() == false, - "aggregated signature of a wrong message should not verify" - ); - } - - #[test] - fn test_aggregate_tiny_sigs_and_verify_in_g1() { - let message = Message::new(b"ctx", b"test message"); - let mut keypairs: Vec<_> = (0..3) - .into_iter() - .map(|i| Keypair::>::generate(StdRng::from_seed([i; 32]))) - .collect(); - let pub_keys_in_sig_grp: Vec> = keypairs - .iter() - .map(|k| { - nugget::NuggetBLS::< - TinyBLS, - as EngineBLS>::SignatureGroup, - >::into_public_key_in_signature_group(k) - }) - .collect(); - - let mut aggregator = SignatureAggregatorAssumingPoP::::new(message.clone()); - let mut aggregated_public_key = - PublicKey::(::PublicKeyGroup::zero()); - - for k in &mut keypairs { - aggregator.add_signature(&k.sign(&message)); - aggregated_public_key.0 += k.public.0; - } - - let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(message); - - verifier_aggregator.add_signature(&aggregator.signature); - verifier_aggregator.add_publickey(&aggregated_public_key); - - for k in &pub_keys_in_sig_grp { - verifier_aggregator.add_auxiliary_public_key(k); - } - - assert!( - verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), - "verifying with honest auxilary public key should pass" - ); - - //false aggregation in signature group should fails verification. - verifier_aggregator.add_auxiliary_public_key(&nugget::NuggetBLS::< - TinyBLS, - as EngineBLS>::SignatureGroup, - >::into_public_key_in_signature_group( - &keypairs[0] - )); - assert!( - !verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), - "verification using non-matching auxilary public key should fail" - ); - } -} diff --git a/src/verifiers.rs b/src/verifiers.rs index 158e8d6..1e9b8d5 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -82,7 +82,7 @@ fn collect_messages_and_publickeys( let mut publickeys = Vec::with_capacity(l); let mut messages = Vec::with_capacity(l); for (message, publickey) in itr { - publickeys.push(publickey.borrow().0); + publickeys.push(publickey.public_key().0); messages.push(message.borrow().hash_to_signature_curve::()); } (signature, publickeys, messages) @@ -114,6 +114,58 @@ fn merge_by_signer( pks_n_ms.into_values().unzip() } +/// Like `merge_by_signer` but keyed on `(public_key, aux_public_key)`. +/// Only message points are merged for entries sharing the same signer pair. +/// Like `merge_by_signer` but also carries auxiliary public keys. +/// Keyed on the public key; returns `None` if the same public key +/// appears with conflicting auxiliary keys. +fn merge_by_signer_with_aux( + affine_publickeys: Vec>, + aux_keys: Vec>, + messages: Vec>, +) -> Option<( + Vec>, + Vec>, + Vec>, +)> { + type PkAuxMsg = ( + PublicKeyAffine, + SignatureAffine, + SignatureProjective, + ); + let mut map: BTreeMap, PkAuxMsg> = BTreeMap::new(); + for ((pk, aux), m) in affine_publickeys + .into_iter() + .zip(aux_keys) + .zip(messages) + { + let aux_affine = aux.into_affine(); + let mut pk_bytes = vec![0; pk.uncompressed_size()]; + pk.serialize_uncompressed(&mut pk_bytes[..]).unwrap(); + match map.entry(pk_bytes) { + alloc::collections::btree_map::Entry::Occupied(mut e) => { + let (_, existing_aux, existing_msg) = e.get_mut(); + if *existing_aux != aux_affine { + return None; + } + *existing_msg += m; + } + alloc::collections::btree_map::Entry::Vacant(e) => { + e.insert((pk, aux_affine, m)); + } + } + } + let mut pks = Vec::with_capacity(map.len()); + let mut auxs = Vec::with_capacity(map.len()); + let mut msgs = Vec::with_capacity(map.len()); + for (pk, aux, m) in map.into_values() { + pks.push(pk); + auxs.push(aux); + msgs.push(m); + } + Some((pks, auxs, msgs)) +} + /// Batch-normalize projective public keys, or convert to affine individually /// when they are already expected to be normalized. fn normalize_publickeys( @@ -154,7 +206,7 @@ pub fn verify_unoptimized(s: S) -> bool { let signature = S::E::prepare_signature(affine_signature); let mut prepared = Vec::new(); for (message, public_key) in s.messages_and_publickeys() { - let pk_affine: PublicKeyAffine = public_key.borrow().0.into(); + let pk_affine: PublicKeyAffine = public_key.public_key().0.into(); if !S::E::verify_public_key_in_public_key_subgroup(&pk_affine) { return false; } @@ -209,20 +261,22 @@ pub fn verify_with_distinct_messages(signed: S, normalize_public_keys } /// BLS signature verification optimized for all unique messages -/// with aggregated auxiliary public keys. +/// with auxiliary public keys. /// -/// Similar to `verify_with_distinct_messages` but adds a randomized -/// auxiliary public key component to each message point and the -/// signature, using deterministic randomness derived from the inputs. +/// Similar to `verify_with_distinct_messages` but for each merged +/// (signer, message) entry, derives a per-entry pseudo-random scalar +/// and folds the auxiliary key contribution into the message point +/// and signature. +// e(asig + \sum_i t_i apk_i,1 , g_2) = \sum_i e (H(m_i) + t_i g_1,apk_i,2) + pub fn verify_using_aggregated_auxiliary_public_keys< E: EngineBLS, H: FixedOutputReset + Default + Clone, >( - signed: &single_pop_aggregator::SignatureAggregatorAssumingPoP, + signed: &pop_aggregator::SignatureAggregatorAssumingPoP, normalize_public_keys: bool, - aggregated_aux_pub_key: ::SignatureGroup, ) -> bool { - let signature = Signed::signature(&signed).0; + let mut signature = Signed::signature(&signed).0; let mut signature_as_bytes = vec![0; signature.compressed_size()]; signature @@ -234,62 +288,54 @@ pub fn verify_using_aggregated_auxiliary_public_keys< let (lower, upper) = itr.size_hint(); upper.unwrap_or(lower) }; - let (first_message, first_public_key) = match signed.messages_and_publickeys().next() { - Some((first_message, first_public_key)) => (first_message, first_public_key), - None => return false, - }; - let mut first_public_key_as_bytes = vec![0; first_public_key.compressed_size()]; - first_public_key - .serialize_compressed(&mut first_public_key_as_bytes[..]) - .expect("compressed size has been alocated"); - - let first_message_point = first_message.hash_to_signature_curve::(); - let first_message_point_as_bytes = E::signature_point_to_byte(&first_message_point); + // Collect public keys, auxiliary keys, and message points. + let mut publickeys = Vec::with_capacity(l); + let mut aux_keys = Vec::with_capacity(l); + let mut messages = Vec::with_capacity(l); + for (m, pk) in itr { + publickeys.push(pk.0.0); + aux_keys.push(pk.1.0); + messages.push(m.hash_to_signature_curve::()); + } - let mut aggregated_aux_pub_key_as_bytes = vec![0; aggregated_aux_pub_key.compressed_size()]; - aggregated_aux_pub_key - .serialize_compressed(&mut aggregated_aux_pub_key_as_bytes[..]) - .expect("compressed size has been alocated"); + let affine_publickeys = normalize_publickeys::(&publickeys, normalize_public_keys); - // We first hash the messages to the signature curve and - // normalize the public keys to operate on them as bytes. - // TODO: Assess if we should mutate in place using interior - // mutability, maybe using `BorrowMut` support in - // `batch_normalization`. + // Merge message points that share the same signer. + // Returns None if same public key appears with conflicting aux keys. + let (merged_pks, merged_aux, mut merged_msgs) = match + merge_by_signer_with_aux::(affine_publickeys, aux_keys, messages) + { + Some(v) => v, + None => return false, + }; - // deterministic randomness for adding aggregated auxiliary pub keys - //TODO you can't just assume that there is one pubickey you need to stop if they were more or aggregate them + // For each merged entry, compute a per-entry pseudo-random scalar + // and fold the auxiliary key contribution into message and signature. + let hasher = as HashToField>::new(&[]); - let pseudo_random_scalar_seed = [ - first_message_point_as_bytes, - first_public_key_as_bytes, - aggregated_aux_pub_key_as_bytes, - signature_as_bytes, - ] - .concat(); + for i in 0..merged_pks.len() { + let mut pk_bytes = vec![0; merged_pks[i].compressed_size()]; + merged_pks[i] + .serialize_compressed(&mut pk_bytes[..]) + .expect("compressed size has been alocated"); - let hasher = as HashToField>::new(&[]); - let pseudo_random_scalar: E::Scalar = - hasher.hash_to_field::<1>(&pseudo_random_scalar_seed[..])[0]; + let mut aux_pk_bytes = vec![0; merged_aux[i].compressed_size()]; + merged_aux[i] + .serialize_compressed(&mut aux_pk_bytes[..]) + .expect("compressed size has been alocated"); - let signature = signature + aggregated_aux_pub_key * pseudo_random_scalar; + let msg_bytes = E::signature_point_to_byte(&merged_msgs[i]); - //Simplify from here on. - let mut publickeys = Vec::with_capacity(l); - let mut messages = Vec::with_capacity(l); - for (m, pk) in itr { - publickeys.push(pk.0); - messages.push( - m.hash_to_signature_curve::() - + E::SignatureGroupAffine::generator() * pseudo_random_scalar, - ); - } + let pseudo_random_scalar_seed = + [msg_bytes, pk_bytes, aux_pk_bytes, signature_as_bytes.clone()].concat(); - let affine_publickeys = normalize_publickeys::(&publickeys, normalize_public_keys); + let pseudo_random_scalar: E::Scalar = + hasher.hash_to_field::<1>(&pseudo_random_scalar_seed[..])[0]; - // We next accumulate message points with the same signer. - let (merged_pks, merged_msgs) = merge_by_signer::(affine_publickeys, messages); + signature += merged_aux[i] * pseudo_random_scalar; + merged_msgs[i] += E::SignatureGroupAffine::generator() * pseudo_random_scalar; + } // And verify the aggregate signature. let (affine_msgs, affine_sig) = From 4275a3e7a8c21c3460c5c12ae43fc5019c5880be Mon Sep 17 00:00:00 2001 From: Skalman Date: Fri, 24 Apr 2026 10:29:21 -0400 Subject: [PATCH 09/14] - Implement `aggregate_aux_publickey_for_message_n_publickey` for nugget verifier. - Add separate aggregator for the verifier and the prover. --- src/pop_aggregator.rs | 196 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 158 insertions(+), 38 deletions(-) diff --git a/src/pop_aggregator.rs b/src/pop_aggregator.rs index 5aee8d8..a222d80 100644 --- a/src/pop_aggregator.rs +++ b/src/pop_aggregator.rs @@ -124,6 +124,39 @@ impl SignatureAggregatorAssumingPoP { .or_insert((pk, aux)); } + /// Add an auxiliary public key for an existing `(message, publickey)` entry. + /// Used by the verifier to aggregate a public key in the signature group + /// for a message signed by an already aggregated public key. + /// + /// If the message already exists with the same public key, the auxiliary + /// key is aggregated into the existing entry. If the message does not + /// exist yet, inserts a new entry. + /// + /// Returns an error if the message already exists with a different + /// public key — the main public key for each message should have been + /// completely aggregated before calling this function. + pub fn aggregate_aux_publickey_for_message_n_publickey( + &mut self, + message: &Message, + publickey: &PublicKey, + aux: &PublicKeyInSignatureGroup, + ) -> Result<(), &'static str> { + match self.messages_n_publickeys.get_mut(message) { + Some((existing_pk, existing_aux)) if existing_pk.0 == publickey.0 => { + existing_aux.0 += &aux.0; + Ok(()) + } + Some(_) => { + Err("message already exists with a different public key") + } + None => { + self.messages_n_publickeys + .insert(message.clone(), (*publickey, *aux)); + Ok(()) + } + } + } + /// Aggregate BLS signatures assuming they have proofs-of-possession. /// /// Folds in every `(message, publickey)` pair carried by `signed` @@ -354,33 +387,76 @@ mod tests { }) .collect(); - let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + // Prover: knows individual keys, aggregates signatures and (pk, aux) pairs. + let mut prover_aggregator = SignatureAggregatorAssumingPoP::::new(); for (k, aux) in keypairs.iter_mut().zip(pub_keys_in_sig_grp.iter()) { - verifier_aggregator.add_signature(&k.sign(&message)); - verifier_aggregator.add_message_n_publickey(&message, &(k.public, *aux)); + prover_aggregator.add_signature(&k.sign(&message)); + prover_aggregator.add_message_n_publickey(&message, &(k.public, *aux)); } assert!( - verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), - "verifying with honest auxilary public key should pass" + prover_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "prover: verifying with honest auxilary public key should pass" ); - // Pair signer 0's public key with signer 1's aux key — should fail. - let mut bad_aggregator = SignatureAggregatorAssumingPoP::::new(); + // Verifier: receives aggregated signature + per-message aggregated pk + // from the prover, then adds individual aux keys separately. + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + verifier_aggregator.add_signature(&(&prover_aggregator).signature()); - for (k, _aux) in keypairs.iter_mut().zip(pub_keys_in_sig_grp.iter()) { - bad_aggregator.add_signature(&k.sign(&message)); + for (msg, (pk, _aux)) in (&prover_aggregator).messages_and_publickeys() { + verifier_aggregator.add_message_n_publickey(msg, pk); + } + + let aggregated_pk = (&prover_aggregator).messages_and_publickeys().next().unwrap().1.0; + for aux in &pub_keys_in_sig_grp { + verifier_aggregator + .aggregate_aux_publickey_for_message_n_publickey(&message, &aggregated_pk, aux) + .expect("public key should match"); } - // signer 0 gets signer 1's aux key - bad_aggregator.add_message_n_publickey(&message, &(keypairs[0].public, pub_keys_in_sig_grp[1])); - // signers 1 and 2 get their correct aux keys - bad_aggregator.add_message_n_publickey(&message, &(keypairs[1].public, pub_keys_in_sig_grp[1])); - bad_aggregator.add_message_n_publickey(&message, &(keypairs[2].public, pub_keys_in_sig_grp[2])); assert!( - !bad_aggregator.verify_using_aggregated_auxiliary_public_keys::(), - "verification using non-matching auxilary public key should fail" + verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: verifying with honest auxilary public key should pass" + ); + + // Verifier with wrong aux: signer 1's aux used in place of signer 0's. + let mut bad_verifier = SignatureAggregatorAssumingPoP::::new(); + bad_verifier.add_signature(&(&prover_aggregator).signature()); + for (msg, (pk, _aux)) in (&prover_aggregator).messages_and_publickeys() { + bad_verifier.add_message_n_publickey(msg, pk); + } + bad_verifier + .aggregate_aux_publickey_for_message_n_publickey(&message, &aggregated_pk, &pub_keys_in_sig_grp[1]) + .unwrap(); + bad_verifier + .aggregate_aux_publickey_for_message_n_publickey(&message, &aggregated_pk, &pub_keys_in_sig_grp[1]) + .unwrap(); + bad_verifier + .aggregate_aux_publickey_for_message_n_publickey(&message, &aggregated_pk, &pub_keys_in_sig_grp[2]) + .unwrap(); + + assert!( + !bad_verifier.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: non-matching auxilary public key should fail" + ); + + // Verifier tries to add aux with an individual signer's pk instead of + // the aggregated pk — should error because the message already has a + // different (aggregated) public key. + let mut mismatched_verifier = SignatureAggregatorAssumingPoP::::new(); + mismatched_verifier.add_signature(&(&prover_aggregator).signature()); + mismatched_verifier.add_message_n_publickey(&message, &aggregated_pk); + + let result = mismatched_verifier.aggregate_aux_publickey_for_message_n_publickey( + &message, + &keypairs[0].public, // individual pk, not the aggregated one + &pub_keys_in_sig_grp[0], + ); + assert!( + result.is_err(), + "aggregate_aux should error when public key does not match existing entry" ); } @@ -402,34 +478,60 @@ mod tests { }) .collect(); - // Each signer signs their own distinct message, then we aggregate - // signatures, (message, publickey+aux) pairs into a single verifier aggregator. - let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + // Prover: each signer signs their own distinct message. + let mut prover_aggregator = SignatureAggregatorAssumingPoP::::new(); for ((k, m), aux) in keypairs.iter_mut().zip(messages.iter()).zip(pub_keys_in_sig_grp.iter()) { - verifier_aggregator.add_signature(&k.sign(m)); - verifier_aggregator.add_message_n_publickey(m, &(k.public, *aux)); + prover_aggregator.add_signature(&k.sign(m)); + prover_aggregator.add_message_n_publickey(m, &(k.public, *aux)); } assert!( - verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), - "verifying multi-message aggregate with honest auxilary public keys should pass" + prover_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "prover: multi-message aggregate with honest auxilary public keys should pass" ); - // Rebuild with signer 0 having signer 1's aux key — should fail. - let mut bad_aggregator = SignatureAggregatorAssumingPoP::::new(); + // Verifier: receives aggregated data from prover, adds aux keys separately. + // Collect per-message (msg, pk, aux) from the prover to preserve association. + let prover_entries: Vec<_> = (&prover_aggregator) + .messages_and_publickeys() + .map(|(m, (pk, aux))| (m.clone(), *pk, *aux)) + .collect(); + + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + verifier_aggregator.add_signature(&(&prover_aggregator).signature()); + + for (msg, pk, _) in &prover_entries { + verifier_aggregator.add_message_n_publickey(msg, pk); + } - for ((k, m), _aux) in keypairs.iter_mut().zip(messages.iter()).zip(pub_keys_in_sig_grp.iter()) { - bad_aggregator.add_signature(&k.sign(m)); + for (msg, pk, aux) in &prover_entries { + verifier_aggregator + .aggregate_aux_publickey_for_message_n_publickey(msg, pk, aux) + .expect("public key should match"); } - // signer 0 gets signer 1's aux key - bad_aggregator.add_message_n_publickey(&messages[0], &(keypairs[0].public, pub_keys_in_sig_grp[1])); - bad_aggregator.add_message_n_publickey(&messages[1], &(keypairs[1].public, pub_keys_in_sig_grp[1])); - bad_aggregator.add_message_n_publickey(&messages[2], &(keypairs[2].public, pub_keys_in_sig_grp[2])); assert!( - !bad_aggregator.verify_using_aggregated_auxiliary_public_keys::(), - "multi-message verification using non-matching auxilary public key should fail" + verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: multi-message aggregate with honest auxilary public keys should pass" + ); + + // Verifier with wrong aux: rotate aux keys so each entry gets the wrong one. + let mut bad_verifier = SignatureAggregatorAssumingPoP::::new(); + bad_verifier.add_signature(&(&prover_aggregator).signature()); + for (msg, pk, _) in &prover_entries { + bad_verifier.add_message_n_publickey(msg, pk); + } + for (i, (msg, pk, _)) in prover_entries.iter().enumerate() { + let wrong_aux = &prover_entries[(i + 1) % prover_entries.len()].2; + bad_verifier + .aggregate_aux_publickey_for_message_n_publickey(msg, pk, wrong_aux) + .unwrap(); + } + + assert!( + !bad_verifier.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: non-matching auxilary public key should fail" ); } @@ -456,14 +558,32 @@ mod tests { }) .collect(); + // Prover: signs real messages honestly. + let mut prover_aggregator = SignatureAggregatorAssumingPoP::::new(); + for ((i, k), aux) in keypairs.iter_mut().enumerate().zip(pub_keys_in_sig_grp.iter()) { + prover_aggregator.add_signature(&k.sign(&real_messages[i])); + prover_aggregator.add_message_n_publickey(&real_messages[i], &(k.public, *aux)); + } + + // Verifier: receives aggregated data from prover but pairs keys with wrong messages. + let prover_entries: Vec<_> = (&prover_aggregator) + .messages_and_publickeys() + .map(|(m, (pk, _))| (m.clone(), *pk)) + .collect(); + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + verifier_aggregator.add_signature(&(&prover_aggregator).signature()); - for ((i, k), aux) in keypairs.iter_mut().enumerate().zip(pub_keys_in_sig_grp.iter()) { - // Sign the real message... - verifier_aggregator.add_signature(&k.sign(&real_messages[i])); - // ...but lie to the verifier about which message this key signed. + // Deliberately rotate messages so each pk is paired with the wrong one. + for (i, (_msg, pk)) in prover_entries.iter().enumerate() { + let wrong_message = &real_messages[(i + 1) % real_messages.len()]; + verifier_aggregator.add_message_n_publickey(wrong_message, pk); + } + for (i, (_msg, pk)) in prover_entries.iter().enumerate() { let wrong_message = &real_messages[(i + 1) % real_messages.len()]; - verifier_aggregator.add_message_n_publickey(wrong_message, &(k.public, *aux)); + verifier_aggregator + .aggregate_aux_publickey_for_message_n_publickey(wrong_message, pk, &pub_keys_in_sig_grp[i]) + .expect("public key should match"); } assert!( From d9d0892c443db1600d84c45cc99d3cafa3c8bc4d Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 2 Jun 2026 13:02:53 -0400 Subject: [PATCH 10/14] Address review suggestions: - verify_using_aggregated_auxiliary_public_keys accepted signed. - Rejects an empty `(message, publickey)` set. - Pre-allocate seed and reuse it for each aux key. - fix docs --- src/pop_aggregator.rs | 71 +++++++++++++++++++++++++++++++-- src/verifiers.rs | 91 +++++++++++++++++++++++++++---------------- 2 files changed, 125 insertions(+), 37 deletions(-) diff --git a/src/pop_aggregator.rs b/src/pop_aggregator.rs index a222d80..2d4920c 100644 --- a/src/pop_aggregator.rs +++ b/src/pop_aggregator.rs @@ -178,10 +178,7 @@ impl SignatureAggregatorAssumingPoP { >( &self, ) -> bool { - verify_using_aggregated_auxiliary_public_keys::( - self, - true, - ) + verify_using_aggregated_auxiliary_public_keys::<_, RandomOracle>(self, true) } } @@ -591,4 +588,70 @@ mod tests { "verification must fail when public keys are paired with the wrong messages" ); } + + /// An empty aggregator paired with an identity signature would + /// otherwise satisfy `e(-g₁, O) == 1` and slip past every verifier. + /// Each high-level entry point must reject `n == 0`. + /// IETF `CoreAggregateVerify` mandates `n ≥ 1 + #[test] + fn empty_aggregator_with_identity_signature_is_rejected() { + use crate::verifiers::{verify_simple, verify_unoptimized, verify_with_distinct_messages}; + + // Build an aggregator with no signers and an identity signature. + let aggregator = SignatureAggregatorAssumingPoP::::new(); + // SignatureAggregatorAssumingPoP::new sets the signature to `O`. + assert_eq!((&aggregator).messages_and_publickeys().count(), 0); + + assert!(!verify_simple(&aggregator), "verify_simple must reject n=0"); + assert!( + !verify_unoptimized(&aggregator), + "verify_unoptimized must reject n=0" + ); + assert!( + !verify_with_distinct_messages(&aggregator, true), + "verify_with_distinct_messages must reject n=0" + ); + assert!( + !aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verify_using_aggregated_auxiliary_public_keys must reject n=0" + ); + } + + /// `verify_using_aggregated_auxiliary_public_keys` folds in + /// `[t_i]·aux_i` on the signature side and `[t_i]·g₁` on the message + /// side for each signer. If the aggregator is built from plain + /// public keys (no auxiliary key), every `aux_i = O`, so the + /// signature side is unchanged while the message side gains a + /// nonzero `[Σ t_i]·g₁` term. The pairing equation then becomes + /// `e(g₁, sig) + e(Σ pk_i, g₁·Σt_i) = e(g₁, sig)`, which forces + /// `Σ t_i·pk_i = O`. The t_i are pseudo-random, so this is + /// astronomically unlikely — verification must fail. + #[test] + fn aux_key_verifier_rejects_aggregator_without_aux_keys() { + let message = Message::new(b"ctx", b"test message"); + let mut keypairs: Vec<_> = (0..3) + .map(|i| Keypair::>::generate(StdRng::from_seed([i; 32]))) + .collect(); + + let mut aggregator = SignatureAggregatorAssumingPoP::::new(); + for k in keypairs.iter_mut() { + aggregator.add_signature(&k.sign(&message)); + // Plain PublicKey: GeneralizedBLSPublicKey impl returns + // `O` for `public_key_in_signature_group`, so no aux key. + aggregator.add_message_n_publickey(&message, &k.public); + } + + // Sanity: ordinary aggregate verification accepts. + assert!( + (&aggregator).verify(), + "the aggregate without aux keys is itself a valid BLS signature" + ); + + // The aux-key verifier must reject because the message-side + // offset is unbalanced when aux keys are zero. + assert!( + !aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "aux-key verification must reject an aggregator with no auxiliary keys" + ); + } } diff --git a/src/verifiers.rs b/src/verifiers.rs index 1e9b8d5..fa7cc47 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -40,11 +40,19 @@ pub type SignatureAffine = <::SignatureGroup as CurveGroup>:: /// Verify from fully normalized (affine) inputs. /// All public keys, messages, and the signature must already be in affine form. /// This prepares the pairing inputs and calls `verify_prepared`. +/// +/// Rejects an empty `(publickeys, messages)` pairing set: with no +/// signers, `verify_prepared` reduces to `e(-g1, sig) == 1`, which +/// any identity signature satisfies. IETF `CoreAggregateVerify` +/// mandates `n ≥ 1`; we enforce the same here. fn verify_normalized( affine_publickeys: &[PublicKeyAffine], affine_messages: &[SignatureAffine], affine_signature: SignatureAffine, ) -> bool { + if affine_publickeys.is_empty() { + return false; + } if !E::verify_signature_in_signature_subgroup(&affine_signature) { return false; } @@ -116,9 +124,8 @@ fn merge_by_signer( /// Like `merge_by_signer` but keyed on `(public_key, aux_public_key)`. /// Only message points are merged for entries sharing the same signer pair. -/// Like `merge_by_signer` but also carries auxiliary public keys. -/// Keyed on the public key; returns `None` if the same public key -/// appears with conflicting auxiliary keys. +/// returns `None` if the same public key appears with conflicting auxiliary +/// keys. fn merge_by_signer_with_aux( affine_publickeys: Vec>, aux_keys: Vec>, @@ -198,6 +205,9 @@ fn normalize_messages_and_signature( // ── Public verification functions ─────────────────────────────────── /// Simple unoptimized BLS signature verification. Useful for testing. +/// +/// Rejects an empty `(message, publickey)` set — see `verify_normalized` +/// for the rationale. pub fn verify_unoptimized(s: S) -> bool { let affine_signature = s.signature().0.into(); if !S::E::verify_signature_in_signature_subgroup(&affine_signature) { @@ -215,6 +225,9 @@ pub fn verify_unoptimized(s: S) -> bool { S::E::prepare_signature(message.borrow().hash_to_signature_curve::()), )); } + if prepared.is_empty() { + return false; + } S::E::verify_prepared(signature, prepared.iter()) } @@ -270,13 +283,13 @@ pub fn verify_with_distinct_messages(signed: S, normalize_public_keys // e(asig + \sum_i t_i apk_i,1 , g_2) = \sum_i e (H(m_i) + t_i g_1,apk_i,2) pub fn verify_using_aggregated_auxiliary_public_keys< - E: EngineBLS, + S: Signed, H: FixedOutputReset + Default + Clone, >( - signed: &pop_aggregator::SignatureAggregatorAssumingPoP, + signed: S, normalize_public_keys: bool, ) -> bool { - let mut signature = Signed::signature(&signed).0; + let mut signature = signed.signature().0; let mut signature_as_bytes = vec![0; signature.compressed_size()]; signature @@ -294,17 +307,17 @@ pub fn verify_using_aggregated_auxiliary_public_keys< let mut aux_keys = Vec::with_capacity(l); let mut messages = Vec::with_capacity(l); for (m, pk) in itr { - publickeys.push(pk.0.0); - aux_keys.push(pk.1.0); - messages.push(m.hash_to_signature_curve::()); + publickeys.push(pk.public_key().0); + aux_keys.push(pk.public_key_in_signature_group().0); + messages.push(m.borrow().hash_to_signature_curve::()); } - let affine_publickeys = normalize_publickeys::(&publickeys, normalize_public_keys); + let affine_publickeys = normalize_publickeys::(&publickeys, normalize_public_keys); // Merge message points that share the same signer. // Returns None if same public key appears with conflicting aux keys. let (merged_pks, merged_aux, mut merged_msgs) = match - merge_by_signer_with_aux::(affine_publickeys, aux_keys, messages) + merge_by_signer_with_aux::(affine_publickeys, aux_keys, messages) { Some(v) => v, None => return false, @@ -312,41 +325,53 @@ pub fn verify_using_aggregated_auxiliary_public_keys< // For each merged entry, compute a per-entry pseudo-random scalar // and fold the auxiliary key contribution into message and signature. - let hasher = as HashToField>::new(&[]); + let hasher = + as HashToField<::Scalar>>::new(&[]); + + // Seed layout: `[msg | pk | aux_pk | signature]`. The message, aux + // public key, and signature all live in the signature group; the + // public key lives in the public-key group. Sizes are fixed by the + // engine, so allocate the buffer once and refill it each iteration + // instead of allocating four vecs + concatenating per loop. + let seed_size = 3 * ::SIGNATURE_SERIALIZED_SIZE + + ::PUBLICKEY_SERIALIZED_SIZE; + let mut seed = Vec::with_capacity(seed_size); for i in 0..merged_pks.len() { - let mut pk_bytes = vec![0; merged_pks[i].compressed_size()]; + seed.clear(); + merged_msgs[i] + .into_affine() + .serialize_compressed(&mut seed) + .expect("compressed size known"); merged_pks[i] - .serialize_compressed(&mut pk_bytes[..]) - .expect("compressed size has been alocated"); - - let mut aux_pk_bytes = vec![0; merged_aux[i].compressed_size()]; + .serialize_compressed(&mut seed) + .expect("compressed size known"); merged_aux[i] - .serialize_compressed(&mut aux_pk_bytes[..]) - .expect("compressed size has been alocated"); - - let msg_bytes = E::signature_point_to_byte(&merged_msgs[i]); + .serialize_compressed(&mut seed) + .expect("compressed size known"); + seed.extend_from_slice(&signature_as_bytes); - let pseudo_random_scalar_seed = - [msg_bytes, pk_bytes, aux_pk_bytes, signature_as_bytes.clone()].concat(); - - let pseudo_random_scalar: E::Scalar = - hasher.hash_to_field::<1>(&pseudo_random_scalar_seed[..])[0]; + let pseudo_random_scalar: ::Scalar = + hasher.hash_to_field::<1>(&seed[..])[0]; signature += merged_aux[i] * pseudo_random_scalar; - merged_msgs[i] += E::SignatureGroupAffine::generator() * pseudo_random_scalar; + merged_msgs[i] += + ::SignatureGroupAffine::generator() * pseudo_random_scalar; } // And verify the aggregate signature. let (affine_msgs, affine_sig) = - normalize_messages_and_signature::(merged_msgs, signature); + normalize_messages_and_signature::(merged_msgs, signature); // `verify_normalized` runs `verify_public_key_in_public_key_subgroup` // on every entry of `merged_pks`. That subgroup check is critical for - // this scheme: the auxiliary-key construction binds `aggregated_aux_pub_key` - // to the signers via the pseudo-random scalar, and a public key that - // sits outside the prime-order subgroup would let an attacker forge a - // matching `aggregated_aux_pub_key` and pass verification. Do not drop it. - verify_normalized::(&merged_pks, &affine_msgs, affine_sig) + // this scheme: the auxiliary-key construction binds each per-entry + // aux key to the corresponding signer via the pseudo-random scalar, + // . A public key that sits outside the prime-order subgroup would + // let an attacker choose an aux key with a small-subgroup component + // that cancels against the mismatched public-key component in the + // pairing, forging a matching aggregated aux key and passing + // verification. Do not drop it. + verify_normalized::(&merged_pks, &affine_msgs, affine_sig) } /* From 989afd50a582aed74f0025598cf1e70e1eba9fcc Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 9 Jun 2026 13:21:12 -0400 Subject: [PATCH 11/14] - make the multi message aux aggegation folmula readable in the comment. - add more example (as tests) on how prover and verifier are using the aggregator --- src/pop_aggregator.rs | 261 ++++++++++++++++++++++++++++++++++++++++++ src/verifiers.rs | 2 +- 2 files changed, 262 insertions(+), 1 deletion(-) diff --git a/src/pop_aggregator.rs b/src/pop_aggregator.rs index 2d4920c..fda966f 100644 --- a/src/pop_aggregator.rs +++ b/src/pop_aggregator.rs @@ -589,6 +589,267 @@ mod tests { ); } + /// Multi-message aggregation where **multiple signers share each + /// message**, with the verifier driven by a per-message bitfield + /// that names the participants. This is the realistic shape used + /// in a consensus protocol: the verifier knows the full signer + /// list (their `(pk, aux)` pairs) and is told, per message, which + /// subset signed. + /// + /// Three signers, two messages: + /// m0 ← signed by k0, k1 (bitfield 0b011) + /// m1 ← signed by k1, k2 (bitfield 0b110) + /// + /// k1 contributes to both messages, so the prover-side merge does + /// real work and the verifier-side reconstruction must reproduce + /// `pk_0+pk_1` and `pk_1+pk_2` from the bitfields. + #[test] + fn aggregate_tiny_sigs_overlapping_signers_multi_messages_in_g1() { + let messages: Vec = (0..2) + .map(|i| Message::new(b"ctx", &[b'm', b'0' + i as u8])) + .collect(); + let mut keypairs: Vec<_> = (0..3) + .map(|i| Keypair::>::generate(StdRng::from_seed([i; 32]))) + .collect(); + // The verifier's list: every signer's public key together with + // its auxiliary key in the signature group. + let signer_list: Vec<(PublicKey, PublicKeyInSignatureGroup)> = keypairs + .iter() + .map(|k| { + let aux = nugget::NuggetBLS::< + TinyBLS, + as EngineBLS>::SignatureGroup, + >::into_public_key_in_signature_group(k); + (k.public, aux) + }) + .collect(); + + // Per-message participation bitfield: bit i set iff signer i + // participated. m0 ← {0,1} = 0b011 ; m1 ← {1,2} = 0b110. + let bitfields: [u8; 2] = [0b011, 0b110]; + + // Prover: each named signer signs the corresponding message; the + // aggregator merges pk and aux for repeated messages. + let mut prover_aggregator = SignatureAggregatorAssumingPoP::::new(); + for (msg, &bits) in messages.iter().zip(bitfields.iter()) { + for i in 0..signer_list.len() { + if bits & (1 << i) != 0 { + prover_aggregator.add_signature(&keypairs[i].sign(msg)); + prover_aggregator + .add_message_n_publickey(msg, &(signer_list[i].0, signer_list[i].1)); + } + } + } + + assert!( + prover_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "prover: overlapping-signer multi-message aggregate must verify" + ); + + // What the prover transmits to the verifier: + // - the aggregated signature, + // - per message: (msg, aggregated_pk, participation_bitfield). + // The verifier only needs to be aware of the signer's aux keys. + let aux_key_list: Vec> = + signer_list.iter().map(|(_, aux)| *aux).collect(); + let prover_signature = (&prover_aggregator).signature(); + // For each protocol message, look up the merged pk that the + // prover computed. (We can't just index `prover_entries[i]` + // because the aggregator's BTreeMap orders by Message hash, + // which is unrelated to our `messages[i]` order.) + let lookup_agg_pk = |target: &Message| -> PublicKey { + (&prover_aggregator) + .messages_and_publickeys() + .find(|(m, _)| *m == target) + .map(|(_, (pk, _aux))| *pk) + .expect("prover must have an entry for every transmitted message") + }; + + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + verifier_aggregator.add_signature(&prover_signature); + for (msg, &bits) in messages.iter().zip(bitfields.iter()) { + let agg_pk = lookup_agg_pk(msg); + verifier_aggregator.add_message_n_publickey(msg, &agg_pk); + for i in 0..aux_key_list.len() { + if bits & (1 << i) != 0 { + verifier_aggregator + .aggregate_aux_publickey_for_message_n_publickey( + msg, + &agg_pk, + &aux_key_list[i], + ) + .expect("aggregated pk must match the existing entry"); + } + } + } + + assert!( + verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: overlapping-signer multi-message aggregate must verify after attaching aux keys from bitfield" + ); + + // Negative control: verifier uses a bitfield that doesn't match + // the actual participation pattern. The aggregated pk per + // message still comes from the prover and is unchanged, but the + // aux keys attached now correspond to the wrong signer set, so + // the pairing equation must reject. + let bad_bitfields: [u8; 2] = [0b110, 0b011]; + let mut bad_verifier = SignatureAggregatorAssumingPoP::::new(); + bad_verifier.add_signature(&prover_signature); + for (msg, &bits) in messages.iter().zip(bad_bitfields.iter()) { + let agg_pk = lookup_agg_pk(msg); + bad_verifier.add_message_n_publickey(msg, &agg_pk); + for i in 0..aux_key_list.len() { + if bits & (1 << i) != 0 { + bad_verifier + .aggregate_aux_publickey_for_message_n_publickey( + msg, + &agg_pk, + &aux_key_list[i], + ) + .expect("aggregated pk must match the existing entry"); + } + } + } + assert!( + !bad_verifier.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: mismatched bitfield must fail" + ); + } + + /// **Triggers `merge_by_signer_with_aux`'s message-summing path.** + /// + /// Two signers, two messages, *everyone signs everything*: + /// m0 ← {k0, k1} (bitfield 0b11) + /// m1 ← {k0, k1} (bitfield 0b11) + /// + /// Both `(msg, agg_pk)` entries end up with the *same* aggregated + /// pk `pk_0+pk_1`. Inside `verify_using_aggregated_auxiliary_public_keys`, + /// `merge_by_signer_with_aux` keys by pk bytes and therefore collapses + /// the two entries into one with `msg = H(m0) + H(m1)` — the only + /// place where messages are actually summed. + /// + /// The prover transmits one bitfield per *protocol* message; the + /// verifier records `(msg_i, agg_pk_i, aux_for_msg_i)` for each; + /// only then does `merge_by_signer_with_aux` collapse entries by pk + /// for the pairing optimization. Per-message accountability lives + /// in the bitfield, not in the merged structure. + #[test] + fn aggregate_tiny_sigs_full_overlap_triggers_message_sum_in_g1() { + let messages: Vec = (0..2) + .map(|i| Message::new(b"ctx", &[b'm', b'0' + i as u8])) + .collect(); + let mut keypairs: Vec<_> = (0..2) + .map(|i| Keypair::>::generate(StdRng::from_seed([i; 32]))) + .collect(); + let signer_list: Vec<(PublicKey, PublicKeyInSignatureGroup)> = keypairs + .iter() + .map(|k| { + let aux = nugget::NuggetBLS::< + TinyBLS, + as EngineBLS>::SignatureGroup, + >::into_public_key_in_signature_group(k); + (k.public, aux) + }) + .collect(); + + // Same signers participate in every message. + let bitfields: [u8; 2] = [0b11, 0b11]; + + // Prover: 4 signing events (2 signers × 2 messages). + let mut prover_aggregator = SignatureAggregatorAssumingPoP::::new(); + for (msg, &bits) in messages.iter().zip(bitfields.iter()) { + for i in 0..signer_list.len() { + if bits & (1 << i) != 0 { + prover_aggregator.add_signature(&keypairs[i].sign(msg)); + prover_aggregator + .add_message_n_publickey(msg, &(signer_list[i].0, signer_list[i].1)); + } + } + } + + // Sanity: both entries should expose the same aggregated pk — + // this is what causes `merge_by_signer_with_aux` to collapse them. + let pks: Vec<_> = (&prover_aggregator) + .messages_and_publickeys() + .map(|(_, (pk, _))| pk.0) + .collect(); + assert_eq!(pks.len(), 2); + assert_eq!( + pks[0], pks[1], + "both messages should aggregate to the same pk so the verifier-side merge sums their hashes" + ); + + assert!( + prover_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "prover: full-overlap aggregate must verify (exercises message-summing merge)" + ); + + // Verifier: receives signature + per-message (agg_pk, bitfield); + // has the aux list. + let aux_key_list: Vec> = + signer_list.iter().map(|(_, aux)| *aux).collect(); + let prover_signature = (&prover_aggregator).signature(); + let lookup_agg_pk = |target: &Message| -> PublicKey { + (&prover_aggregator) + .messages_and_publickeys() + .find(|(m, _)| *m == target) + .map(|(_, (pk, _aux))| *pk) + .expect("prover must have an entry for every transmitted message") + }; + + let mut verifier_aggregator = SignatureAggregatorAssumingPoP::::new(); + verifier_aggregator.add_signature(&prover_signature); + for (msg, &bits) in messages.iter().zip(bitfields.iter()) { + let agg_pk = lookup_agg_pk(msg); + verifier_aggregator.add_message_n_publickey(msg, &agg_pk); + for i in 0..aux_key_list.len() { + if bits & (1 << i) != 0 { + verifier_aggregator + .aggregate_aux_publickey_for_message_n_publickey( + msg, + &agg_pk, + &aux_key_list[i], + ) + .expect("aggregated pk must match the existing entry"); + } + } + } + + assert!( + verifier_aggregator.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: full-overlap aggregate must verify" + ); + + // Negative control: drop signer 1 from m0's bitfield. The + // verifier-side merge now sees two entries with the same pk but + // *different* aggregated aux (aux_0 for m0, aux_0+aux_1 for m1). + // `merge_by_signer_with_aux` rejects on the aux mismatch and + // returns `None`, so verification fails before any pairing runs. + let bad_bitfields: [u8; 2] = [0b01, 0b11]; + let mut bad_verifier = SignatureAggregatorAssumingPoP::::new(); + bad_verifier.add_signature(&prover_signature); + for (msg, &bits) in messages.iter().zip(bad_bitfields.iter()) { + let agg_pk = lookup_agg_pk(msg); + bad_verifier.add_message_n_publickey(msg, &agg_pk); + for i in 0..aux_key_list.len() { + if bits & (1 << i) != 0 { + bad_verifier + .aggregate_aux_publickey_for_message_n_publickey( + msg, + &agg_pk, + &aux_key_list[i], + ) + .expect("aggregated pk must match the existing entry"); + } + } + } + assert!( + !bad_verifier.verify_using_aggregated_auxiliary_public_keys::(), + "verifier: a bitfield that yields conflicting aux per pk must fail" + ); + } + /// An empty aggregator paired with an identity signature would /// otherwise satisfy `e(-g₁, O) == 1` and slip past every verifier. /// Each high-level entry point must reject `n == 0`. diff --git a/src/verifiers.rs b/src/verifiers.rs index fa7cc47..17286d1 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -280,7 +280,7 @@ pub fn verify_with_distinct_messages(signed: S, normalize_public_keys /// (signer, message) entry, derives a per-entry pseudo-random scalar /// and folds the auxiliary key contribution into the message point /// and signature. -// e(asig + \sum_i t_i apk_i,1 , g_2) = \sum_i e (H(m_i) + t_i g_1,apk_i,2) +// e(asig + \sum_i ( t_i*apk1_i) , g_2) = \sum_i (e(H(m_i) + (t_i*g_1), apk2_i)) pub fn verify_using_aggregated_auxiliary_public_keys< S: Signed, From b484cec38dfc75940ded3fc3ccca26f1e69f927e Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 23 Jun 2026 11:47:43 -0400 Subject: [PATCH 12/14] make every t_i in multi-message aux verification depends on all messages and pub keys preventing subset sum attack. --- src/verifiers.rs | 72 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/verifiers.rs b/src/verifiers.rs index 17286d1..a3a878d 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -280,8 +280,30 @@ pub fn verify_with_distinct_messages(signed: S, normalize_public_keys /// (signer, message) entry, derives a per-entry pseudo-random scalar /// and folds the auxiliary key contribution into the message point /// and signature. +/// +/// Each `t_i` is derived from a global transcript that covers the +/// aggregated signature and every merged `(message, apk1, apk2)` +/// triple, so any change to one entry changes every `t_i`: +/// +/// ```text +/// transcript = asig || (m_1 || apk1_1 || apk2_1) || ... || (m_n || apk1_n || apk2_n) +/// t_1 = H(transcript) +/// t_i = H(transcript || i) for i >= 2 +/// ``` +/// +/// `apk1_i` is the auxiliary public key (in the signature group), +/// `apk2_i` is the public key (in the public-key group). +/// +/// **Entry ordering is part of the spec.** Entries `1..=n` are emitted +/// in **ascending lexicographic order of the affine, uncompressed +/// serialization of `apk2_i`** (the public key). This is the order +/// imposed by `merge_by_signer_with_aux`'s internal `BTreeMap` keyed +/// on `pk.serialize_uncompressed(..)`. Prover and verifier must both +/// follow this canonical ordering — otherwise their `t_i` indices +/// disagree and verification fails on honest inputs. The merge step +/// also collapses any entries sharing the same `apk2` into a single +/// transcript slot (with summed messages and a shared `apk1`). // e(asig + \sum_i ( t_i*apk1_i) , g_2) = \sum_i (e(H(m_i) + (t_i*g_1), apk2_i)) - pub fn verify_using_aggregated_auxiliary_public_keys< S: Signed, H: FixedOutputReset + Default + Clone, @@ -323,33 +345,47 @@ pub fn verify_using_aggregated_auxiliary_public_keys< None => return false, }; - // For each merged entry, compute a per-entry pseudo-random scalar - // and fold the auxiliary key contribution into message and signature. let hasher = as HashToField<::Scalar>>::new(&[]); - // Seed layout: `[msg | pk | aux_pk | signature]`. The message, aux - // public key, and signature all live in the signature group; the - // public key lives in the public-key group. Sizes are fixed by the - // engine, so allocate the buffer once and refill it each iteration - // instead of allocating four vecs + concatenating per loop. - let seed_size = 3 * ::SIGNATURE_SERIALIZED_SIZE - + ::PUBLICKEY_SERIALIZED_SIZE; - let mut seed = Vec::with_capacity(seed_size); + // Build the transcript once: + // asig || (msg_1 || apk1_1 || apk2_1) || ... || (msg_n || apk1_n || apk2_n) + // We affine-batch the message points up front because the per-entry + // loop below mutates `merged_msgs`, so we cannot read their values + // again after we have started folding in the `t_i * g_1` terms. + let n = merged_pks.len(); + let affine_merged_msgs = + <::SignatureGroup as CurveGroup>::normalize_batch(&merged_msgs); - for i in 0..merged_pks.len() { - seed.clear(); - merged_msgs[i] - .into_affine() + let entry_size = 2 * ::SIGNATURE_SERIALIZED_SIZE + + ::PUBLICKEY_SERIALIZED_SIZE; + let transcript_size = + ::SIGNATURE_SERIALIZED_SIZE + n * entry_size; + // Reserve a little extra for the per-entry index suffix used by t_i (i >= 2). + let mut seed = Vec::with_capacity(transcript_size + core::mem::size_of::()); + seed.extend_from_slice(&signature_as_bytes); + for i in 0..n { + affine_merged_msgs[i] .serialize_compressed(&mut seed) .expect("compressed size known"); - merged_pks[i] + merged_aux[i] .serialize_compressed(&mut seed) .expect("compressed size known"); - merged_aux[i] + merged_pks[i] .serialize_compressed(&mut seed) .expect("compressed size known"); - seed.extend_from_slice(&signature_as_bytes); + } + let transcript_len = seed.len(); + + for i in 0..n { + // t_1 hashes the bare transcript; t_i for i >= 2 appends the + // 1-based index so each scalar is domain-separated by position + // while still binding the whole transcript. + seed.truncate(transcript_len); + if i > 0 { + let index = (i as u64) + 1; + seed.extend_from_slice(&index.to_be_bytes()); + } let pseudo_random_scalar: ::Scalar = hasher.hash_to_field::<1>(&seed[..])[0]; From 6ebc19c1f7d882c33de7f8fd370577171ff71739 Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 23 Jun 2026 12:30:55 -0400 Subject: [PATCH 13/14] Add index encoding to the spec comment --- src/verifiers.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/verifiers.rs b/src/verifiers.rs index a3a878d..bfdbd98 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -288,12 +288,17 @@ pub fn verify_with_distinct_messages(signed: S, normalize_public_keys /// ```text /// transcript = asig || (m_1 || apk1_1 || apk2_1) || ... || (m_n || apk1_n || apk2_n) /// t_1 = H(transcript) -/// t_i = H(transcript || i) for i >= 2 +/// t_i = H(transcript || I2OSP(i, 8)) for i >= 2 /// ``` /// /// `apk1_i` is the auxiliary public key (in the signature group), /// `apk2_i` is the public key (in the public-key group). /// +/// The index `i` is encoded as `I2OSP(i, 8)` — an 8-byte big-endian +/// (network-byte-order) unsigned integer, exactly as defined in +/// RFC 8017 §4.1 and re-used by the IETF hash-to-curve draft +/// (RFC 9380). In Rust this is `(i as u64).to_be_bytes()`. +/// /// **Entry ordering is part of the spec.** Entries `1..=n` are emitted /// in **ascending lexicographic order of the affine, uncompressed /// serialization of `apk2_i`** (the public key). This is the order From 8c65b3f911f6e749bd8067dc057cba03e7a689ce Mon Sep 17 00:00:00 2001 From: Skalman Date: Wed, 1 Jul 2026 09:36:47 -0400 Subject: [PATCH 14/14] add more detail on the attack when signature subgroup hasn't been check in aux pub verify --- src/verifiers.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/verifiers.rs b/src/verifiers.rs index bfdbd98..df90210 100644 --- a/src/verifiers.rs +++ b/src/verifiers.rs @@ -406,12 +406,17 @@ pub fn verify_using_aggregated_auxiliary_public_keys< // `verify_normalized` runs `verify_public_key_in_public_key_subgroup` // on every entry of `merged_pks`. That subgroup check is critical for // this scheme: the auxiliary-key construction binds each per-entry - // aux key to the corresponding signer via the pseudo-random scalar, - // . A public key that sits outside the prime-order subgroup would - // let an attacker choose an aux key with a small-subgroup component - // that cancels against the mismatched public-key component in the - // pairing, forging a matching aggregated aux key and passing - // verification. Do not drop it. + // aux key to the corresponding signer via the pseudo-random scalar. + // Without the check, given an honest signer's (pk, aux) and a valid + // signature σ on m, an attacker can register a rogue + // pk' = pk + T₁, aux' = aux + T₂ + // where T₁, T₂ are small-subgroup (non-prime-order) elements. + // Pairings annihilate small-subgroup components against prime-order + // ones, so the T₁, T₂ terms drop out of the verification equation + // and the same σ still verifies against (pk', aux'). The attacker + // thus obtains a distinct public key that accepts a signature they + // did not produce with the corresponding secret — a rogue-key / + // strict-unforgeability break. Do not drop it. verify_normalized::(&merged_pks, &affine_msgs, affine_sig) } @@ -498,7 +503,7 @@ mod tests { use crate::{Keypair, Message, PublicKey, Signature, UsualBLS}; use ark_bls12_381::{Bls12_381, Fq, Fq2, G1Affine, G2Affine}; use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; - use ark_ff::{BitIteratorBE, PrimeField, UniformRand}; + use ark_ff::{BitIteratorBE, One, PrimeField, UniformRand}; use rand::rngs::StdRng; use rand::SeedableRng;