Skip to content

Commit 9f5d9fa

Browse files
core: replace secp256k with k256 in crypto::ecdsa (#3525)
This PR replaces the usage of [secp256k](https://crates.io/crates/secp256k1) crate with [k256](https://crates.io/crates/k256) in `core::crypto::ecdsa` for `non-std` environments as outcome of discussion in #3448. `secp256k1` is used in `std`, meaning that we should not affect host performance with this PR. `k256` is enabled in runtimes (`no-std`), and is required to proceed with #2044. If desirable, in future we can switch to `k256` also for `std`. That would require some performance evaluation (e.g. for EVM chains as per #3448 (comment)). Closes #3448 --------- Co-authored-by: command-bot <> Co-authored-by: Davide Galassi <[email protected]>
1 parent ea458d0 commit 9f5d9fa

File tree

3 files changed

+129
-52
lines changed

3 files changed

+129
-52
lines changed

Cargo.lock

Lines changed: 21 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

substrate/primitives/core/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ blake2 = { version = "0.10.4", default-features = false, optional = true }
5252
libsecp256k1 = { version = "0.7", default-features = false, features = ["static-context"], optional = true }
5353
schnorrkel = { version = "0.11.4", features = ["preaudit_deprecated"], default-features = false }
5454
merlin = { version = "3.0", default-features = false }
55-
secp256k1 = { version = "0.28.0", default-features = false, features = ["alloc", "recovery"], optional = true }
5655
sp-crypto-hashing = { path = "../crypto/hashing", default-features = false, optional = true }
5756
sp-runtime-interface = { path = "../runtime-interface", default-features = false }
57+
# k256 crate, better portability, intended to be used in substrate-runtimes (no-std)
58+
k256 = { version = "0.13.3", features = ["alloc", "ecdsa"], default-features = false }
59+
# secp256k1 crate, better performance, intended to be used on host side (std)
60+
secp256k1 = { version = "0.28.0", default-features = false, features = ["alloc", "recovery"], optional = true }
5861

5962
# bls crypto
6063
w3f-bls = { version = "0.1.3", default-features = false, optional = true }
@@ -76,6 +79,7 @@ bench = false
7679

7780
[features]
7881
default = ["std"]
82+
7983
std = [
8084
"array-bytes",
8185
"bandersnatch_vrfs?/std",
@@ -94,6 +98,7 @@ std = [
9498
"hash256-std-hasher/std",
9599
"impl-serde/std",
96100
"itertools",
101+
"k256/std",
97102
"libsecp256k1/std",
98103
"log/std",
99104
"merlin/std",
@@ -132,6 +137,7 @@ serde = [
132137
"bs58/alloc",
133138
"dep:serde",
134139
"impl-serde",
140+
"k256/serde",
135141
"primitive-types/serde_no_std",
136142
"scale-info/serde",
137143
"secrecy/alloc",
@@ -147,7 +153,6 @@ full_crypto = [
147153
"blake2",
148154
"ed25519-zebra",
149155
"libsecp256k1",
150-
"secp256k1",
151156
"sp-crypto-hashing",
152157
"sp-runtime-interface/disable_target_static_assertions",
153158
]

substrate/primitives/core/src/ecdsa.rs

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ use crate::crypto::{
2828
};
2929
#[cfg(feature = "full_crypto")]
3030
use crate::crypto::{DeriveError, DeriveJunction, Pair as TraitPair, SecretStringError};
31-
#[cfg(all(feature = "full_crypto", not(feature = "std")))]
32-
use secp256k1::Secp256k1;
33-
#[cfg(feature = "std")]
34-
use secp256k1::SECP256K1;
35-
#[cfg(feature = "full_crypto")]
31+
32+
#[cfg(all(not(feature = "std"), feature = "full_crypto"))]
33+
use k256::ecdsa::SigningKey as SecretKey;
34+
#[cfg(not(feature = "std"))]
35+
use k256::ecdsa::VerifyingKey;
36+
#[cfg(all(feature = "std", feature = "full_crypto"))]
3637
use secp256k1::{
3738
ecdsa::{RecoverableSignature, RecoveryId},
38-
Message, PublicKey, SecretKey,
39+
Message, PublicKey, SecretKey, SECP256K1,
3940
};
4041
#[cfg(feature = "serde")]
4142
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
@@ -53,9 +54,9 @@ pub const PUBLIC_KEY_SERIALIZED_SIZE: usize = 33;
5354
/// The byte length of signature
5455
pub const SIGNATURE_SERIALIZED_SIZE: usize = 65;
5556

56-
/// A secret seed (which is bytewise essentially equivalent to a SecretKey).
57+
/// The secret seed.
5758
///
58-
/// We need it as a different type because `Seed` is expected to be AsRef<[u8]>.
59+
/// The raw secret seed, which can be used to create the `Pair`.
5960
#[cfg(feature = "full_crypto")]
6061
type Seed = [u8; 32];
6162

@@ -96,18 +97,21 @@ impl Public {
9697
/// Create a new instance from the given full public key.
9798
///
9899
/// This will convert the full public key into the compressed format.
99-
#[cfg(feature = "std")]
100100
pub fn from_full(full: &[u8]) -> Result<Self, ()> {
101-
let pubkey = if full.len() == 64 {
101+
let mut tagged_full = [0u8; 65];
102+
let full = if full.len() == 64 {
102103
// Tag it as uncompressed public key.
103-
let mut tagged_full = [0u8; 65];
104104
tagged_full[0] = 0x04;
105105
tagged_full[1..].copy_from_slice(full);
106-
secp256k1::PublicKey::from_slice(&tagged_full)
106+
&tagged_full
107107
} else {
108-
secp256k1::PublicKey::from_slice(full)
108+
full
109109
};
110-
pubkey.map(|k| Self(k.serialize())).map_err(|_| ())
110+
#[cfg(feature = "std")]
111+
let pubkey = PublicKey::from_slice(&full);
112+
#[cfg(not(feature = "std"))]
113+
let pubkey = VerifyingKey::from_sec1_bytes(&full);
114+
pubkey.map(|k| k.into()).map_err(|_| ())
111115
}
112116
}
113117

@@ -131,6 +135,24 @@ impl AsMut<[u8]> for Public {
131135
}
132136
}
133137

138+
#[cfg(feature = "std")]
139+
impl From<PublicKey> for Public {
140+
fn from(pubkey: PublicKey) -> Self {
141+
Self(pubkey.serialize())
142+
}
143+
}
144+
145+
#[cfg(not(feature = "std"))]
146+
impl From<VerifyingKey> for Public {
147+
fn from(pubkey: VerifyingKey) -> Self {
148+
Self::unchecked_from(
149+
pubkey.to_sec1_bytes()[..]
150+
.try_into()
151+
.expect("valid key is serializable to [u8,33]. qed."),
152+
)
153+
}
154+
}
155+
134156
impl TryFrom<&[u8]> for Public {
135157
type Error = ();
136158

@@ -331,23 +353,34 @@ impl Signature {
331353
/// Recover the public key from this signature and a pre-hashed message.
332354
#[cfg(feature = "full_crypto")]
333355
pub fn recover_prehashed(&self, message: &[u8; 32]) -> Option<Public> {
334-
let rid = RecoveryId::from_i32(self.0[64] as i32).ok()?;
335-
let sig = RecoverableSignature::from_compact(&self.0[..64], rid).ok()?;
336-
let message = Message::from_digest_slice(message).expect("Message is 32 bytes; qed");
337-
338356
#[cfg(feature = "std")]
339-
let context = SECP256K1;
357+
{
358+
let rid = RecoveryId::from_i32(self.0[64] as i32).ok()?;
359+
let sig = RecoverableSignature::from_compact(&self.0[..64], rid).ok()?;
360+
let message = Message::from_digest_slice(message).expect("Message is 32 bytes; qed");
361+
SECP256K1.recover_ecdsa(&message, &sig).ok().map(Public::from)
362+
}
363+
340364
#[cfg(not(feature = "std"))]
341-
let context = Secp256k1::verification_only();
365+
{
366+
let rid = k256::ecdsa::RecoveryId::from_byte(self.0[64])?;
367+
let sig = k256::ecdsa::Signature::from_bytes((&self.0[..64]).into()).ok()?;
368+
VerifyingKey::recover_from_prehash(message, &sig, rid).map(Public::from).ok()
369+
}
370+
}
371+
}
342372

343-
context
344-
.recover_ecdsa(&message, &sig)
345-
.ok()
346-
.map(|pubkey| Public(pubkey.serialize()))
373+
#[cfg(not(feature = "std"))]
374+
impl From<(k256::ecdsa::Signature, k256::ecdsa::RecoveryId)> for Signature {
375+
fn from(recsig: (k256::ecdsa::Signature, k256::ecdsa::RecoveryId)) -> Signature {
376+
let mut r = Self::default();
377+
r.0[..64].copy_from_slice(&recsig.0.to_bytes());
378+
r.0[64] = recsig.1.to_byte();
379+
r
347380
}
348381
}
349382

350-
#[cfg(feature = "full_crypto")]
383+
#[cfg(all(feature = "std", feature = "full_crypto"))]
351384
impl From<RecoverableSignature> for Signature {
352385
fn from(recsig: RecoverableSignature) -> Signature {
353386
let mut r = Self::default();
@@ -384,17 +417,19 @@ impl TraitPair for Pair {
384417
///
385418
/// You should never need to use this; generate(), generate_with_phrase
386419
fn from_seed_slice(seed_slice: &[u8]) -> Result<Pair, SecretStringError> {
387-
let secret =
388-
SecretKey::from_slice(seed_slice).map_err(|_| SecretStringError::InvalidSeedLength)?;
389-
390420
#[cfg(feature = "std")]
391-
let context = SECP256K1;
392-
#[cfg(not(feature = "std"))]
393-
let context = Secp256k1::signing_only();
421+
{
422+
let secret = SecretKey::from_slice(seed_slice)
423+
.map_err(|_| SecretStringError::InvalidSeedLength)?;
424+
Ok(Pair { public: PublicKey::from_secret_key(&SECP256K1, &secret).into(), secret })
425+
}
394426

395-
let public = PublicKey::from_secret_key(&context, &secret);
396-
let public = Public(public.serialize());
397-
Ok(Pair { public, secret })
427+
#[cfg(not(feature = "std"))]
428+
{
429+
let secret = SecretKey::from_slice(seed_slice)
430+
.map_err(|_| SecretStringError::InvalidSeedLength)?;
431+
Ok(Pair { public: VerifyingKey::from(&secret).into(), secret })
432+
}
398433
}
399434

400435
/// Derive a child key from a series of given junctions.
@@ -438,7 +473,14 @@ impl TraitPair for Pair {
438473
impl Pair {
439474
/// Get the seed for this key.
440475
pub fn seed(&self) -> Seed {
441-
self.secret.secret_bytes()
476+
#[cfg(feature = "std")]
477+
{
478+
self.secret.secret_bytes()
479+
}
480+
#[cfg(not(feature = "std"))]
481+
{
482+
self.secret.to_bytes().into()
483+
}
442484
}
443485

444486
/// Exactly as `from_string` except that if no matches are found then, the the first 32
@@ -455,14 +497,19 @@ impl Pair {
455497

456498
/// Sign a pre-hashed message
457499
pub fn sign_prehashed(&self, message: &[u8; 32]) -> Signature {
458-
let message = Message::from_digest_slice(message).expect("Message is 32 bytes; qed");
459-
460500
#[cfg(feature = "std")]
461-
let context = SECP256K1;
462-
#[cfg(not(feature = "std"))]
463-
let context = Secp256k1::signing_only();
501+
{
502+
let message = Message::from_digest_slice(message).expect("Message is 32 bytes; qed");
503+
SECP256K1.sign_ecdsa_recoverable(&message, &self.secret).into()
504+
}
464505

465-
context.sign_ecdsa_recoverable(&message, &self.secret).into()
506+
#[cfg(not(feature = "std"))]
507+
{
508+
self.secret
509+
.sign_prehash_recoverable(message)
510+
.expect("signing may not fail (???). qed.")
511+
.into()
512+
}
466513
}
467514

468515
/// Verify a signature on a pre-hashed message. Return `true` if the signature is valid
@@ -503,7 +550,7 @@ impl Pair {
503550
// NOTE: this solution is not effective when `Pair` is moved around memory.
504551
// The very same problem affects other cryptographic backends that are just using
505552
// `zeroize`for their secrets.
506-
#[cfg(feature = "full_crypto")]
553+
#[cfg(all(feature = "std", feature = "full_crypto"))]
507554
impl Drop for Pair {
508555
fn drop(&mut self) {
509556
self.secret.non_secure_erase()
@@ -770,8 +817,18 @@ mod test {
770817
let msg = [0u8; 32];
771818
let sig1 = pair.sign_prehashed(&msg);
772819
let sig2: Signature = {
773-
let message = Message::from_digest_slice(&msg).unwrap();
774-
SECP256K1.sign_ecdsa_recoverable(&message, &pair.secret).into()
820+
#[cfg(feature = "std")]
821+
{
822+
let message = Message::from_digest_slice(&msg).unwrap();
823+
SECP256K1.sign_ecdsa_recoverable(&message, &pair.secret).into()
824+
}
825+
#[cfg(not(feature = "std"))]
826+
{
827+
pair.secret
828+
.sign_prehash_recoverable(&msg)
829+
.expect("signing may not fail (???). qed.")
830+
.into()
831+
}
775832
};
776833
assert_eq!(sig1, sig2);
777834

0 commit comments

Comments
 (0)