Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Replace libsecp256k1 with secp265k1 #8100

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ integer-sqrt = { opt-level = 3 }
keccak = { opt-level = 3 }
libm = { opt-level = 3 }
librocksdb-sys = { opt-level = 3 }
libsecp256k1 = { opt-level = 3 }
secp256k1 = { opt-level = 3 }
libz-sys = { opt-level = 3 }
mio = { opt-level = 3 }
nalgebra = { opt-level = 3 }
Expand Down
1 change: 0 additions & 1 deletion client/executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ sc-executor-wasmi = { version = "0.9.0", path = "wasmi" }
sc-executor-wasmtime = { version = "0.9.0", path = "wasmtime", optional = true }
parking_lot = "0.11.1"
log = "0.4.8"
libsecp256k1 = "0.3.4"

[dev-dependencies]
assert_matches = "1.3.0"
Expand Down
6 changes: 3 additions & 3 deletions primitives/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated", "u64_backen
sha2 = { version = "0.9.2", default-features = false, optional = true }
hex = { version = "0.4", default-features = false, optional = true }
twox-hash = { version = "1.5.0", default-features = false, optional = true }
libsecp256k1 = { version = "0.3.2", default-features = false, features = ["hmac"], optional = true }
secp256k1 = { version = "0.20.1", optional = true, default-features = false, features = [ "recovery" ] }
merlin = { version = "2.0", default-features = false, optional = true }

sp-runtime-interface = { version = "3.0.0", default-features = false, path = "../runtime-interface" }
Expand Down Expand Up @@ -112,7 +112,7 @@ std = [
"secrecy/alloc",
"futures",
"futures/thread-pool",
"libsecp256k1/std",
"secp256k1/std",
"dyn-clonable",
]

Expand All @@ -127,7 +127,7 @@ full_crypto = [
"hex",
"sha2",
"twox-hash",
"libsecp256k1",
"secp256k1",
"sp-runtime-interface/disable_target_static_assertions",
"merlin",
]
88 changes: 53 additions & 35 deletions primitives/core/src/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use sp_std::cmp::Ordering;
use codec::{Encode, Decode};

#[cfg(feature = "full_crypto")]
use core::convert::{TryFrom, TryInto};
use core::convert::TryFrom;
#[cfg(feature = "std")]
use substrate_bip39::seed_from_entropy;
#[cfg(feature = "std")]
Expand Down Expand Up @@ -103,8 +103,18 @@ impl Public {
/// This will convert the full public key into the compressed format.
#[cfg(feature = "std")]
pub fn from_full(full: &[u8]) -> Result<Self, ()> {
secp256k1::PublicKey::parse_slice(full, None)
.map(|k| k.serialize_compressed())
let pubkey = if full.len() == 64 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The libsecp256k1 library is handling 3 kind of keys:
full (65 bytes), raw (64 bytes; no tag) and compressed.

Here we only handle the raw case, are you sure that secp256k1 is handling both full and compressed case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The C lib handles 33 and 65 byte signatures. But no 64 byte signatures:
https://github.com/bitcoin-core/secp256k1/blob/9a5a87e0f1276e0284446af1172056ea4693737f/src/eckey_impl.h#L17-L35

let mut full_with_tag = [0u8; 65];
full_with_tag[1..].copy_from_slice(full);
// Tag it as uncompressed public key
full_with_tag[0] = 0x04;
secp256k1::PublicKey::from_slice(&full_with_tag)
} else {
secp256k1::PublicKey::from_slice(&full)
};

pubkey
.map(|k| k.serialize())
.map(Self)
.map_err(|_| ())
}
Expand Down Expand Up @@ -165,7 +175,6 @@ impl sp_std::convert::TryFrom<&[u8]> for Public {
if data.len() == 33 {
Ok(Self::from_slice(data))
} else {

Err(())
}
}
Expand Down Expand Up @@ -348,31 +357,34 @@ impl Signature {
/// Recover the public key from this signature and a message.
#[cfg(feature = "full_crypto")]
pub fn recover<M: AsRef<[u8]>>(&self, message: M) -> Option<Public> {
let message = secp256k1::Message::parse(&blake2_256(message.as_ref()));
let sig: (_, _) = self.try_into().ok()?;
secp256k1::recover(&message, &sig.0, &sig.1)
let message = secp256k1::Message::from_slice(&blake2_256(message.as_ref())).ok()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be an expect, since we know it's 32-bytes


let id = secp256k1::recovery::RecoveryId::from_i32(self.0[64] as i32).ok()?;
let sig = secp256k1::recovery::RecoverableSignature::from_compact(&self.0[..64], id).ok()?;

secp256k1::Secp256k1::verification_only().recover(&message, &sig)
Copy link
Contributor

@tomusdrw tomusdrw Feb 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating the Secp256k1 is super heavy and should be cached in a lazy_static or sth. AFAIR they are considering making the required data static, but it's not there yet in the C library.
Also see: tomusdrw/ethsign#37

.ok()
.map(|recovered| Public(recovered.serialize_compressed()))
.map(|recovered| Public(recovered.serialize()))
}
}

#[cfg(feature = "full_crypto")]
impl From<(secp256k1::Signature, secp256k1::RecoveryId)> for Signature {
fn from(x: (secp256k1::Signature, secp256k1::RecoveryId)) -> Signature {
impl From<(secp256k1::Signature, secp256k1::recovery::RecoveryId)> for Signature {
fn from(x: (secp256k1::Signature, secp256k1::recovery::RecoveryId)) -> Signature {
let mut r = Self::default();
r.0[0..64].copy_from_slice(&x.0.serialize()[..]);
r.0[64] = x.1.serialize();
r.0[0..64].copy_from_slice(&x.0.serialize_compact()[..]);
r.0[64] = x.1.to_i32() as u8;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might cause issues in case of overflows - you can have multiple different RecoveryIds ending up with the same Signature type (malleability).

Seems that libsecp256k1 was simply storing u8, but the C library stores i32.

r
}
}

#[cfg(feature = "full_crypto")]
impl<'a> TryFrom<&'a Signature> for (secp256k1::Signature, secp256k1::RecoveryId) {
impl<'a> TryFrom<&'a Signature> for (secp256k1::Signature, secp256k1::recovery::RecoveryId) {
type Error = ();
fn try_from(x: &'a Signature) -> Result<(secp256k1::Signature, secp256k1::RecoveryId), Self::Error> {
fn try_from(x: &'a Signature) -> Result<(secp256k1::Signature, secp256k1::recovery::RecoveryId), Self::Error> {
Ok((
secp256k1::Signature::parse_slice(&x.0[0..64]).expect("hardcoded to 64 bytes; qed"),
secp256k1::RecoveryId::parse(x.0[64]).map_err(|_| ())?,
secp256k1::Signature::from_compact(&x.0[0..64]).map_err(|_| ())?,
secp256k1::recovery::RecoveryId::from_i32(x.0[64] as i32).map_err(|_| ())?,
))
}
}
Expand Down Expand Up @@ -450,9 +462,9 @@ impl TraitPair for Pair {
///
/// You should never need to use this; generate(), generate_with_phrase
fn from_seed_slice(seed_slice: &[u8]) -> Result<Pair, SecretStringError> {
let secret = SecretKey::parse_slice(seed_slice)
let secret = SecretKey::from_slice(seed_slice)
.map_err(|_| SecretStringError::InvalidSeedLength)?;
let public = PublicKey::from_secret_key(&secret);
let public = PublicKey::from_secret_key(&secp256k1::Secp256k1::signing_only(), &secret);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here (context to be cached)

Ok(Pair{ secret, public })
}

Expand All @@ -461,7 +473,7 @@ impl TraitPair for Pair {
path: Iter,
_seed: Option<Seed>
) -> Result<(Pair, Option<Seed>), DeriveError> {
let mut acc = self.secret.serialize();
let mut acc = self.seed();
for j in path {
match j {
DeriveJunction::Soft(_cc) => return Err(DeriveError::SoftKeyInPath),
Expand All @@ -473,22 +485,24 @@ impl TraitPair for Pair {

/// Get the public key.
fn public(&self) -> Public {
Public(self.public.serialize_compressed())
Public(self.public.serialize())
}

/// Sign a message.
fn sign(&self, message: &[u8]) -> Signature {
let message = secp256k1::Message::parse(&blake2_256(message));
secp256k1::sign(&message, &self.secret).into()
let message = secp256k1::Message::from_slice(&blake2_256(message))
.expect("Message is 32 byte; qed");
let sig = secp256k1::Secp256k1::signing_only().sign_recoverable(&message, &self.secret);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here :)


let id = sig.serialize_compact().0;
(sig.to_standard(), id).into()
}

/// Verify a signature on a message. Returns true if the signature is good.
fn verify<M: AsRef<[u8]>>(sig: &Self::Signature, message: M, pubkey: &Self::Public) -> bool {
let message = secp256k1::Message::parse(&blake2_256(message.as_ref()));
let sig: (_, _) = match sig.try_into() { Ok(x) => x, _ => return false };
match secp256k1::recover(&message, &sig.0, &sig.1) {
Ok(actual) => &pubkey.0[..] == &actual.serialize_compressed()[..],
_ => false,
match sig.recover(message) {
Some(actual) => actual == *pubkey,
None => false,
}
}

Expand All @@ -497,13 +511,17 @@ impl TraitPair for Pair {
/// This doesn't use the type system to ensure that `sig` and `pubkey` are the correct
/// size. Use it only if you're coming from byte buffers and need the speed.
fn verify_weak<P: AsRef<[u8]>, M: AsRef<[u8]>>(sig: &[u8], message: M, pubkey: P) -> bool {
let message = secp256k1::Message::parse(&blake2_256(message.as_ref()));
if sig.len() != 65 { return false }
let ri = match secp256k1::RecoveryId::parse(sig[64]) { Ok(x) => x, _ => return false };
let sig = match secp256k1::Signature::parse_slice(&sig[0..64]) { Ok(x) => x, _ => return false };
match secp256k1::recover(&message, &sig, &ri) {
Ok(actual) => pubkey.as_ref() == &actual.serialize()[1..],
_ => false,
if sig.len() != 65 {
return false
}

let mut signature = [0u8; 65];
signature.copy_from_slice(sig);
let sig = Signature(signature);

match sig.recover(message) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message is not blake2_256 hashed here (and it used to be)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact it wasn't caught by a test scares me.

None => false,
Some(actual) => pubkey.as_ref() == actual.as_ref(),
}
}

Expand All @@ -517,7 +535,7 @@ impl TraitPair for Pair {
impl Pair {
/// Get the seed for this key.
pub fn seed(&self) -> Seed {
self.secret.serialize()
*self.secret.as_ref()
}

/// Exactly as `from_string` except that if no matches are found then, the the first 32
Expand Down
4 changes: 2 additions & 2 deletions primitives/io/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ hash-db = { version = "0.15.2", default-features = false }
sp-core = { version = "3.0.0", default-features = false, path = "../core" }
sp-keystore = { version = "0.9.0", default-features = false, optional = true, path = "../keystore" }
sp-std = { version = "3.0.0", default-features = false, path = "../std" }
libsecp256k1 = { version = "0.3.4", optional = true }
secp256k1 = { version = "0.20.1", optional = true, features = [ "recovery" ] }
sp-state-machine = { version = "0.9.0", optional = true, path = "../state-machine" }
sp-wasm-interface = { version = "3.0.0", path = "../wasm-interface", default-features = false }
sp-runtime-interface = { version = "3.0.0", default-features = false, path = "../runtime-interface" }
Expand All @@ -43,7 +43,7 @@ std = [
"hash-db/std",
"sp-trie",
"sp-state-machine",
"libsecp256k1",
"secp256k1",
"sp-runtime-interface/std",
"sp-externalities",
"sp-wasm-interface/std",
Expand Down
32 changes: 22 additions & 10 deletions primitives/io/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,14 +691,20 @@ pub trait Crypto {
sig: &[u8; 65],
msg: &[u8; 32],
) -> Result<[u8; 64], EcdsaVerifyError> {
let rs = secp256k1::Signature::parse_slice(&sig[0..64])
let v = secp256k1::recovery::RecoveryId::from_i32(
if sig[64] > 26 { sig[64] - 27 } else { sig[64] } as i32
).map_err(|_| EcdsaVerifyError::BadV)?;

let rs = secp256k1::recovery::RecoverableSignature::from_compact(&sig[0..64], v)
.map_err(|_| EcdsaVerifyError::BadRS)?;
let v = secp256k1::RecoveryId::parse(if sig[64] > 26 { sig[64] - 27 } else { sig[64] } as u8)
.map_err(|_| EcdsaVerifyError::BadV)?;
let pubkey = secp256k1::recover(&secp256k1::Message::parse(msg), &rs, &v)

let msg = secp256k1::Message::from_slice(msg).map_err(|_| EcdsaVerifyError::BadSignature)?;

let pubkey = secp256k1::Secp256k1::verification_only().recover(&msg, &rs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context here again.

.map_err(|_| EcdsaVerifyError::BadSignature)?;

let mut res = [0u8; 64];
res.copy_from_slice(&pubkey.serialize()[1..65]);
res.copy_from_slice(&pubkey.serialize_uncompressed()[1..65]);
Ok(res)
}

Expand All @@ -712,13 +718,19 @@ pub trait Crypto {
sig: &[u8; 65],
msg: &[u8; 32],
) -> Result<[u8; 33], EcdsaVerifyError> {
let rs = secp256k1::Signature::parse_slice(&sig[0..64])
let v = secp256k1::recovery::RecoveryId::from_i32(
if sig[64] > 26 { sig[64] - 27 } else { sig[64] } as i32
).map_err(|_| EcdsaVerifyError::BadV)?;

let rs = secp256k1::recovery::RecoverableSignature::from_compact(&sig[0..64], v)
.map_err(|_| EcdsaVerifyError::BadRS)?;
let v = secp256k1::RecoveryId::parse(if sig[64] > 26 { sig[64] - 27 } else { sig[64] } as u8)
.map_err(|_| EcdsaVerifyError::BadV)?;
let pubkey = secp256k1::recover(&secp256k1::Message::parse(msg), &rs, &v)

let msg = secp256k1::Message::from_slice(msg).map_err(|_| EcdsaVerifyError::BadSignature)?;

let pubkey = secp256k1::Secp256k1::verification_only().recover(&msg, &rs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

.map_err(|_| EcdsaVerifyError::BadSignature)?;
Ok(pubkey.serialize_compressed())

Ok(pubkey.serialize())
}
}

Expand Down