Skip to content

Commit b371c35

Browse files
drskalmandavxyLederstrumpf
authored
Fix ecdsa_bls verify in BEEFY primitives (#2066)
BEEFY ECDSA signatures are on keccak has of the messages. As such we can not simply call `EcdsaBlsPair::verify(signature.as_inner_ref(), msg, self.as_inner_ref())` because that invokes ecdsa default verification which perfoms blake2 hash which we don't want. This bring up the second issue makes: This makes `sign` and `verify` function in `pair_crypto` useless, at least for BEEFY use case. Moreover, there is no obvious clean way to generate the signature given that pair_crypto does not exposes `sign_prehashed`. You could in theory query the keystore for the pair (could you?), invoke `to_raw` and re-generate each sub-pair and sign using each. But that sounds extremely anticlimactic and will be frow upon by auditors . So I appreciate any alternative suggestion. --------- Co-authored-by: Davide Galassi <[email protected]> Co-authored-by: Robert Hambrock <[email protected]>
1 parent 689b9d9 commit b371c35

File tree

2 files changed

+96
-14
lines changed

2 files changed

+96
-14
lines changed

substrate/primitives/consensus/beefy/src/lib.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ pub mod bls_crypto {
133133
<MsgHash as Hash>::Output: Into<[u8; 32]>,
134134
{
135135
fn verify(&self, signature: &<Self as RuntimeAppPublic>::Signature, msg: &[u8]) -> bool {
136-
// `w3f-bls` library uses IETF hashing standard and as such does not exposes
136+
// `w3f-bls` library uses IETF hashing standard and as such does not expose
137137
// a choice of hash to field function.
138138
// We are directly calling into the library to avoid introducing new host call.
139139
// and because BeefyAuthorityId::verify is being called in the runtime so we don't have
@@ -157,7 +157,7 @@ pub mod bls_crypto {
157157
pub mod ecdsa_bls_crypto {
158158
use super::{BeefyAuthorityId, Hash, RuntimeAppPublic, KEY_TYPE};
159159
use sp_application_crypto::{app_crypto, ecdsa_bls377};
160-
use sp_core::{crypto::Wraps, ecdsa_bls377::Pair as EcdsaBlsPair, Pair as _};
160+
use sp_core::{crypto::Wraps, ecdsa_bls377::Pair as EcdsaBlsPair};
161161

162162
app_crypto!(ecdsa_bls377, KEY_TYPE);
163163

@@ -167,17 +167,24 @@ pub mod ecdsa_bls_crypto {
167167
/// Signature for a BEEFY authority using (ECDSA,BLS) as its crypto.
168168
pub type AuthoritySignature = Signature;
169169

170-
impl<MsgHash: Hash> BeefyAuthorityId<MsgHash> for AuthorityId
170+
impl<H> BeefyAuthorityId<H> for AuthorityId
171171
where
172-
<MsgHash as Hash>::Output: Into<[u8; 32]>,
172+
H: Hash,
173+
H::Output: Into<[u8; 32]>,
173174
{
174175
fn verify(&self, signature: &<Self as RuntimeAppPublic>::Signature, msg: &[u8]) -> bool {
175-
// `w3f-bls` library uses IETF hashing standard and as such does not exposes
176-
// a choice of hash to field function.
177-
// We are directly calling into the library to avoid introducing new host call.
178-
// and because BeefyAuthorityId::verify is being called in the runtime so we don't have
179-
180-
EcdsaBlsPair::verify(signature.as_inner_ref(), msg, self.as_inner_ref())
176+
// We can not simply call
177+
// `EcdsaBlsPair::verify(signature.as_inner_ref(), msg, self.as_inner_ref())`
178+
// because that invokes ECDSA default verification which perfoms Blake2b hash
179+
// which we don't want. This is because ECDSA signatures are meant to be verified
180+
// on Ethereum network where Keccak hasher is significantly cheaper than Blake2b.
181+
// See Figure 3 of [OnSc21](https://www.scitepress.org/Papers/2021/106066/106066.pdf)
182+
// for comparison.
183+
EcdsaBlsPair::verify_with_hasher::<H>(
184+
signature.as_inner_ref(),
185+
msg,
186+
self.as_inner_ref(),
187+
)
181188
}
182189
}
183190
}
@@ -257,6 +264,7 @@ pub enum ConsensusLog<AuthorityId: Codec> {
257264
///
258265
/// A vote message is a direct vote created by a BEEFY node on every voting round
259266
/// and is gossiped to its peers.
267+
// TODO: Remove `Signature` generic type, instead get it from `Id::Signature`.
260268
#[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)]
261269
pub struct VoteMessage<Number, Id, Signature> {
262270
/// Commit to information extracted from a finalized block
@@ -507,11 +515,15 @@ mod tests {
507515
let msg = &b"test-message"[..];
508516
let (pair, _) = ecdsa_bls_crypto::Pair::generate();
509517

510-
let signature: ecdsa_bls_crypto::Signature = pair.as_inner_ref().sign(&msg).into();
518+
let signature: ecdsa_bls_crypto::Signature =
519+
pair.as_inner_ref().sign_with_hasher::<Keccak256>(&msg).into();
511520

512521
// Verification works if same hashing function is used when signing and verifying.
513522
assert!(BeefyAuthorityId::<Keccak256>::verify(&pair.public(), &signature, msg));
514523

524+
// Verification doesn't work if we verify function provided by pair_crypto implementation
525+
assert!(!ecdsa_bls_crypto::Pair::verify(&signature, msg, &pair.public()));
526+
515527
// Other public key doesn't work
516528
let (other_pair, _) = ecdsa_bls_crypto::Pair::generate();
517529
assert!(!BeefyAuthorityId::<Keccak256>::verify(&other_pair.public(), &signature, msg,));

substrate/primitives/core/src/paired_crypto.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ use sp_std::convert::TryFrom;
3939
/// ECDSA and BLS12-377 paired crypto scheme
4040
#[cfg(feature = "bls-experimental")]
4141
pub mod ecdsa_bls377 {
42-
use crate::{bls377, crypto::CryptoTypeId, ecdsa};
42+
#[cfg(feature = "full_crypto")]
43+
use crate::Hasher;
44+
use crate::{
45+
bls377,
46+
crypto::{CryptoTypeId, Pair as PairT, UncheckedFrom},
47+
ecdsa,
48+
};
4349

4450
/// An identifier used to match public keys against BLS12-377 keys
4551
pub const CRYPTO_ID: CryptoTypeId = CryptoTypeId(*b"ecb7");
@@ -71,6 +77,60 @@ pub mod ecdsa_bls377 {
7177
impl super::CryptoType for Pair {
7278
type Pair = Pair;
7379
}
80+
81+
#[cfg(feature = "full_crypto")]
82+
impl Pair {
83+
/// Hashes the `message` with the specified [`Hasher`] before signing sith the ECDSA secret
84+
/// component.
85+
///
86+
/// The hasher does not affect the BLS12-377 component. This generates BLS12-377 Signature
87+
/// according to IETF standard.
88+
pub fn sign_with_hasher<H>(&self, message: &[u8]) -> Signature
89+
where
90+
H: Hasher,
91+
H::Out: Into<[u8; 32]>,
92+
{
93+
let msg_hash = H::hash(message).into();
94+
95+
let mut raw: [u8; SIGNATURE_LEN] = [0u8; SIGNATURE_LEN];
96+
raw[..ecdsa::SIGNATURE_SERIALIZED_SIZE]
97+
.copy_from_slice(self.left.sign_prehashed(&msg_hash).as_ref());
98+
raw[ecdsa::SIGNATURE_SERIALIZED_SIZE..]
99+
.copy_from_slice(self.right.sign(message).as_ref());
100+
<Self as PairT>::Signature::unchecked_from(raw)
101+
}
102+
103+
/// Hashes the `message` with the specified [`Hasher`] before verifying with the ECDSA
104+
/// public component.
105+
///
106+
/// The hasher does not affect the the BLS12-377 component. This verifies whether the
107+
/// BLS12-377 signature was hashed and signed according to IETF standard
108+
pub fn verify_with_hasher<H>(sig: &Signature, message: &[u8], public: &Public) -> bool
109+
where
110+
H: Hasher,
111+
H::Out: Into<[u8; 32]>,
112+
{
113+
let msg_hash = H::hash(message).into();
114+
115+
let Ok(left_pub) = public.0[..ecdsa::PUBLIC_KEY_SERIALIZED_SIZE].try_into() else {
116+
return false
117+
};
118+
let Ok(left_sig) = sig.0[0..ecdsa::SIGNATURE_SERIALIZED_SIZE].try_into() else {
119+
return false
120+
};
121+
if !ecdsa::Pair::verify_prehashed(&left_sig, &msg_hash, &left_pub) {
122+
return false
123+
}
124+
125+
let Ok(right_pub) = public.0[ecdsa::PUBLIC_KEY_SERIALIZED_SIZE..].try_into() else {
126+
return false
127+
};
128+
let Ok(right_sig) = sig.0[ecdsa::SIGNATURE_SERIALIZED_SIZE..].try_into() else {
129+
return false
130+
};
131+
bls377::Pair::verify(&right_sig, message.as_ref(), &right_pub)
132+
}
133+
}
74134
}
75135

76136
/// Secure seed length.
@@ -455,12 +515,12 @@ where
455515
#[cfg(all(test, feature = "bls-experimental"))]
456516
mod test {
457517
use super::*;
458-
use crate::crypto::DEV_PHRASE;
518+
use crate::{crypto::DEV_PHRASE, KeccakHasher};
459519
use ecdsa_bls377::{Pair, Signature};
460520

461521
use crate::{bls377, ecdsa};
462-
#[test]
463522

523+
#[test]
464524
fn test_length_of_paired_ecdsa_and_bls377_public_key_and_signature_is_correct() {
465525
assert_eq!(
466526
<Pair as PairT>::Public::LEN,
@@ -617,6 +677,16 @@ mod test {
617677
assert_eq!(cmp, public);
618678
}
619679

680+
#[test]
681+
fn sign_and_verify_with_hasher_works() {
682+
let pair =
683+
Pair::from_seed(&(b"12345678901234567890123456789012".as_slice().try_into().unwrap()));
684+
let message = b"Something important";
685+
let signature = pair.sign_with_hasher::<KeccakHasher>(&message[..]);
686+
687+
assert!(Pair::verify_with_hasher::<KeccakHasher>(&signature, &message[..], &pair.public()));
688+
}
689+
620690
#[test]
621691
fn signature_serialization_works() {
622692
let pair =

0 commit comments

Comments
 (0)