feat(signer): ethereum implementation (#1501)

---------

Co-authored-by: James Wilson <james@jsdw.me>
This commit is contained in:
Ryan Lee
2024-04-15 08:56:10 -04:00
committed by GitHub
parent 159af2cfcd
commit b527c857ea
10 changed files with 862 additions and 46 deletions
+1
View File
@@ -189,6 +189,7 @@ jobs:
cargo check -p subxt-signer
cargo check -p subxt-signer --no-default-features --features sr25519
cargo check -p subxt-signer --no-default-features --features ecdsa
cargo check -p subxt-signer --no-default-features --features unstable-eth
# We can't enable web features here, so no cargo hack.
- name: Cargo check subxt-lightclient
Generated
+145
View File
@@ -571,6 +571,22 @@ dependencies = [
"serde",
]
[[package]]
name = "bip32"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e141fb0f8be1c7b45887af94c88b182472b57c96b56773250ae00cd6a14a164"
dependencies = [
"bs58",
"hmac 0.12.1",
"k256",
"rand_core 0.6.4",
"ripemd",
"sha2 0.10.8",
"subtle",
"zeroize",
]
[[package]]
name = "bip39"
version = "2.0.0"
@@ -582,6 +598,21 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitcoin-internals"
version = "0.2.0"
@@ -710,6 +741,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896"
dependencies = [
"sha2 0.10.8",
"tinyvec",
]
@@ -2021,6 +2053,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
[[package]]
name = "hex-literal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "hmac"
version = "0.8.1"
@@ -2472,6 +2510,16 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "keccak-hash"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b286e6b663fb926e1eeb68528e69cb70ed46c6d65871a21b2215ae8154c6d3c"
dependencies = [
"primitive-types",
"tiny-keccak",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -2751,6 +2799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -3210,6 +3259,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proptest"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf"
dependencies = [
"bit-set",
"bit-vec",
"bitflags 2.4.2",
"lazy_static",
"num-traits",
"rand",
"rand_chacha",
"rand_xorshift",
"regex-syntax 0.8.2",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "psm"
version = "0.1.21"
@@ -3219,6 +3288,12 @@ dependencies = [
"cc",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.35"
@@ -3270,6 +3345,15 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rayon"
version = "1.8.1"
@@ -3403,6 +3487,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "ripemd"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@@ -3560,6 +3653,18 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]]
name = "ruzstd"
version = "0.5.0"
@@ -4805,14 +4910,18 @@ dependencies = [
name = "subxt-signer"
version = "0.35.0"
dependencies = [
"bip32",
"bip39",
"cfg-if",
"derive_more",
"getrandom",
"hex",
"hex-literal",
"hmac 0.12.1",
"keccak-hash",
"parity-scale-codec",
"pbkdf2",
"proptest",
"regex",
"schnorrkel",
"secp256k1",
@@ -4867,6 +4976,18 @@ version = "0.12.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae"
[[package]]
name = "tempfile"
version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"rustix 0.38.31",
"windows-sys 0.52.0",
]
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -4928,6 +5049,15 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
@@ -5331,6 +5461,12 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@@ -5470,6 +5606,15 @@ dependencies = [
"glob 0.2.11",
]
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.4.0"
+4
View File
@@ -111,6 +111,8 @@ wabt = "0.10.0"
wasm-bindgen-test = "0.3.24"
which = "5.0.0"
strip-ansi-escapes = "0.2.0"
proptest = "1.4.0"
hex-literal = "0.4.1"
# Light client support:
smoldot = { version = "0.16.0", default-features = false }
@@ -149,10 +151,12 @@ substrate-runner = { path = "testing/substrate-runner" }
# subxt-signer deps that I expect aren't useful anywhere else:
bip39 = { version = "2.0.0", default-features = false }
bip32 = { version = "0.5.1", default-features = false }
hmac = { version = "0.12.1", default-features = false }
pbkdf2 = { version = "0.12.2", default-features = false }
schnorrkel = { version = "0.11.4", default-features = false }
secp256k1 = { version = "0.28.2", default-features = false }
keccak-hash = { version = "0.10.0", default-features = false }
secrecy = "0.8.0"
sha2 = { version = "0.10.8", default-features = false }
zeroize = { version = "1", default-features = false }
+17 -3
View File
@@ -16,7 +16,17 @@ keywords = ["parity", "subxt", "extrinsic", "signer"]
[features]
default = ["sr25519", "ecdsa", "subxt", "std"]
std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std", "bip39/std", "schnorrkel/std", "secp256k1/std", "sp-core/std"]
std = [
"regex/std",
"sp-crypto-hashing/std",
"pbkdf2/std",
"sha2/std",
"hmac/std",
"bip39/std",
"schnorrkel/std",
"secp256k1/std",
"sp-core/std"
]
# Pick the signer implementation(s) you need by enabling the
# corresponding features. Note: I had more difficulties getting
@@ -24,6 +34,7 @@ std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std
# https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699
sr25519 = ["schnorrkel"]
ecdsa = ["secp256k1"]
unstable-eth = ["keccak-hash", "ecdsa", "secp256k1", "bip32"]
# Make the keypair algorithms here compatible with Subxt's Signer trait,
# so that they can be used to sign transactions for compatible chains.
@@ -37,7 +48,7 @@ web = ["getrandom/js"]
subxt-core = { workspace = true, optional = true, default-features = false }
secrecy = { workspace = true }
regex = { workspace = true, features = ["unicode"] }
hex = { workspace = true }
hex = { workspace = true, features = ["alloc"] }
cfg-if = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
sp-crypto-hashing = { workspace = true }
@@ -47,15 +58,18 @@ sha2 = { workspace = true }
hmac = { workspace = true }
zeroize = { workspace = true }
bip39 = { workspace = true }
bip32 = { workspace = true, features = ["alloc", "secp256k1"], optional = true }
schnorrkel = { workspace = true, optional = true }
secp256k1 = { workspace = true, optional = true, features = ["alloc", "recovery"] }
keccak-hash = { workspace = true, optional = true }
# We only pull this in to enable the JS flag for schnorrkel to use.
getrandom = { workspace = true, optional = true }
[dev-dependencies]
sp-keyring = { workspace = true }
proptest = { workspace = true }
hex-literal = { workspace = true }
sp-core = { workspace = true }
[package.metadata.cargo-machete]
+39 -29
View File
@@ -12,10 +12,10 @@ use hex::FromHex;
use secp256k1::{ecdsa::RecoverableSignature, Message, Secp256k1, SecretKey};
use secrecy::ExposeSecret;
const SEED_LENGTH: usize = 32;
const SECRET_KEY_LENGTH: usize = 32;
/// Seed bytes used to generate a key pair.
pub type Seed = [u8; SEED_LENGTH];
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
/// A signature generated by [`Keypair::sign()`]. These bytes are equivalent
/// to a Substrate `MultiSignature::Ecdsa(bytes)`.
@@ -39,7 +39,7 @@ impl AsRef<[u8]> for PublicKey {
}
/// An ecdsa keypair implementation.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Keypair(pub secp256k1::Keypair);
impl Keypair {
@@ -67,8 +67,8 @@ impl Keypair {
// Else, parse the phrase string taking the password into account. This is
// the same approach taken in sp_core::crypto::Pair::from_string_with_seed.
let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") {
let seed = Seed::from_hex(hex_str)?;
Self::from_seed(seed)?
let seed = SecretKeyBytes::from_hex(hex_str)?;
Self::from_secret_key(seed)?
} else {
let phrase = bip39::Mnemonic::from_str(phrase.expose_secret().as_str())?;
let pass_str = password.as_ref().map(|p| p.expose_secret().as_str());
@@ -97,11 +97,11 @@ impl Keypair {
let big_seed =
seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?;
let seed: Seed = big_seed[..SEED_LENGTH]
let secret_key_bytes: SecretKeyBytes = big_seed[..SECRET_KEY_LENGTH]
.try_into()
.expect("should be valid Seed");
Self::from_seed(seed)
Self::from_secret_key(secret_key_bytes)
}
/// Turn a 32 byte seed into a keypair.
@@ -109,8 +109,8 @@ impl Keypair {
/// # Warning
///
/// This will only be secure if the seed is secure!
pub fn from_seed(seed: Seed) -> Result<Self, Error> {
let secret = SecretKey::from_slice(&seed).map_err(|_| Error::InvalidSeed)?;
pub fn from_secret_key(secret_key: SecretKeyBytes) -> Result<Self, Error> {
let secret = SecretKey::from_slice(&secret_key).map_err(|_| Error::InvalidSeed)?;
Ok(Self(secp256k1::Keypair::from_secret_key(
&Secp256k1::signing_only(),
&secret,
@@ -148,7 +148,7 @@ impl Keypair {
}
}
}
Self::from_seed(acc)
Self::from_secret_key(acc)
}
/// Obtain the [`PublicKey`] part of this key pair, which can be used in calls to [`verify()`].
@@ -160,18 +160,9 @@ impl Keypair {
/// Sign some message. These bytes can be used directly in a Substrate `MultiSignature::Ecdsa(..)`.
pub fn sign(&self, message: &[u8]) -> Signature {
// From sp_core::ecdsa::sign:
let message_hash = sp_crypto_hashing::blake2_256(message);
// From sp_core::ecdsa::sign_prehashed:
let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed");
let recsig: RecoverableSignature =
Secp256k1::signing_only().sign_ecdsa_recoverable(&wrapped, &self.0.secret_key());
// From sp_core::ecdsa's `impl From<RecoverableSignature> for Signature`:
let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact();
let mut signature_bytes: [u8; 65] = [0; 65];
signature_bytes[..64].copy_from_slice(&sig);
signature_bytes[64] = (recid.to_i32() & 0xFF) as u8;
Signature(signature_bytes)
Signature(internal::sign(&self.0.secret_key(), &wrapped))
}
}
@@ -188,18 +179,37 @@ impl Keypair {
/// assert!(ecdsa::verify(&signature, message, &public_key));
/// ```
pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool {
let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.0[..64]) else {
return false;
};
let Ok(public) = secp256k1::PublicKey::from_slice(&pubkey.0) else {
return false;
};
let message_hash = sp_crypto_hashing::blake2_256(message.as_ref());
let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed");
Secp256k1::verification_only()
.verify_ecdsa(&wrapped, &signature, &public)
.is_ok()
internal::verify(&sig.0, &wrapped, pubkey)
}
pub(crate) mod internal {
use super::*;
pub fn sign(secret_key: &secp256k1::SecretKey, message: &Message) -> [u8; 65] {
let recsig: RecoverableSignature =
Secp256k1::signing_only().sign_ecdsa_recoverable(message, secret_key);
let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact();
let mut signature_bytes: [u8; 65] = [0; 65];
signature_bytes[..64].copy_from_slice(&sig);
signature_bytes[64] = (recid.to_i32() & 0xFF) as u8;
signature_bytes
}
pub fn verify(sig: &[u8; 65], message: &Message, pubkey: &PublicKey) -> bool {
let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig[..64]) else {
return false;
};
let Ok(public) = secp256k1::PublicKey::from_slice(&pubkey.0) else {
return false;
};
Secp256k1::verification_only()
.verify_ecdsa(message, &signature, &public)
.is_ok()
}
}
/// An error handed back if creating a keypair fails.
+613
View File
@@ -0,0 +1,613 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! An ethereum keypair implementation.
use crate::ecdsa;
use alloc::format;
use alloc::string::String;
use core::fmt::{Display, Formatter};
use core::str::FromStr;
use derive_more::Display;
use keccak_hash::keccak;
use secp256k1::Message;
const SECRET_KEY_LENGTH: usize = 32;
/// Bytes representing a private key.
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
/// An ethereum keypair implementation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Keypair(ecdsa::Keypair);
impl From<ecdsa::Keypair> for Keypair {
fn from(kp: ecdsa::Keypair) -> Self {
Self(kp)
}
}
impl Keypair {
/// Create a keypair from a BIP-39 mnemonic phrase, optional password, account index, and
/// derivation type.
///
/// **Note:** if the `std` feature is not enabled, we won't attempt to normalize the provided password
/// to NFKD first, and so this is your responsibility. This is not a concern if only ASCII
/// characters are used in the password.
///
/// # Example
///
/// ```rust
/// use subxt_signer::{ bip39::Mnemonic, eth::{ Keypair, DerivationPath } };
///
/// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
/// let mnemonic = Mnemonic::parse(phrase).unwrap();
/// let keypair = Keypair::from_phrase(&mnemonic, None, DerivationPath::eth(0,0)).unwrap();
///
/// keypair.sign(b"Hello world!");
/// ```
pub fn from_phrase(
mnemonic: &bip39::Mnemonic,
password: Option<&str>,
derivation_path: DerivationPath,
) -> Result<Self, Error> {
// `to_seed` isn't available unless std is enabled in bip39.
#[cfg(feature = "std")]
let seed = mnemonic.to_seed(password.unwrap_or(""));
#[cfg(not(feature = "std"))]
let seed = mnemonic.to_seed_normalized(password.unwrap_or(""));
// TODO: Currently, we use bip32 to derive private keys which under the hood uses
// the Rust k256 crate. We _also_ use the secp256k1 crate (which is very similar).
// It'd be great if we could 100% use just one of the two crypto libs. bip32 has
// a feature flag to use secp256k1, but it's unfortunately a different version (older)
// than ours.
let private = bip32::XPrv::derive_from_path(seed, &derivation_path.inner)
.map_err(|_| Error::DeriveFromPath)?;
Keypair::from_secret_key(private.to_bytes())
}
/// Turn a 16, 32 or 64 byte seed into a keypair.
///
/// # Warning
///
/// This will only be secure if the seed is secure!
pub fn from_seed(seed: &[u8]) -> Result<Self, Error> {
let private = bip32::XPrv::new(seed).map_err(|_| Error::InvalidSeed)?;
Keypair::from_secret_key(private.to_bytes())
}
/// Turn a 32 byte secret key into a keypair.
///
/// # Warning
///
/// This will only be secure if the secret key is secure!
pub fn from_secret_key(secret_key: SecretKeyBytes) -> Result<Self, Error> {
ecdsa::Keypair::from_secret_key(secret_key)
.map(Self)
.map_err(|_| Error::InvalidSeed)
}
/// Obtain the [`ecdsa::PublicKey`] of this keypair.
pub fn public_key(&self) -> ecdsa::PublicKey {
self.0.public_key()
}
/// Obtains the public address of the account by taking the last 20 bytes
/// of the Keccak-256 hash of the public key.
pub fn account_id(&self) -> AccountId20 {
let uncompressed = self.0 .0.public_key().serialize_uncompressed();
let hash = keccak(&uncompressed[1..]).0;
let hash20 = hash[12..].try_into().expect("should be 20 bytes");
AccountId20(hash20)
}
/// Signs an arbitrary message payload.
pub fn sign(&self, signer_payload: &[u8]) -> Signature {
let message_hash = keccak(signer_payload);
let wrapped =
Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed");
Signature(ecdsa::internal::sign(&self.0 .0.secret_key(), &wrapped))
}
}
/// A derivation path. This can be parsed from a valid derivation path string like
/// `"m/44'/60'/0'/0/0"`, or we can construct one using the helpers [`DerivationPath::empty()`]
/// and [`DerivationPath::eth()`].
#[derive(Clone, Debug)]
pub struct DerivationPath {
inner: bip32::DerivationPath,
}
impl DerivationPath {
/// An empty derivation path (in other words, just use the master-key as is).
pub fn empty() -> Self {
let inner = bip32::DerivationPath::from_str("m").unwrap();
DerivationPath { inner }
}
/// A BIP44 Ethereum compatible derivation using the path "m/44'/60'/account'/0/address_index".
///
/// # Panics
///
/// Panics if the `account` or `address_index` provided are >= 2^31.
pub fn eth(account: u32, address_index: u32) -> Self {
assert!(
account < bip32::ChildNumber::HARDENED_FLAG,
"account must be less than 2^31"
);
assert!(
address_index < bip32::ChildNumber::HARDENED_FLAG,
"address_index must be less than 2^31"
);
let derivation_string = format!("m/44'/60'/{account}'/0/{address_index}");
let inner = bip32::DerivationPath::from_str(&derivation_string).unwrap();
DerivationPath { inner }
}
}
impl FromStr for DerivationPath {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let inner = bip32::DerivationPath::from_str(s).map_err(|_| Error::DeriveFromPath)?;
Ok(DerivationPath { inner })
}
}
/// A signature generated by [`Keypair::sign()`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode)]
pub struct Signature(pub [u8; 65]);
impl AsRef<[u8; 65]> for Signature {
fn as_ref(&self) -> &[u8; 65] {
&self.0
}
}
/// A 20-byte cryptographic identifier.
#[derive(Debug, Copy, Clone, PartialEq, Eq, codec::Encode)]
pub struct AccountId20(pub [u8; 20]);
impl AccountId20 {
fn checksum(&self) -> String {
let hex_address = hex::encode(self.0);
let hash = keccak(hex_address.as_bytes());
let mut checksum_address = String::with_capacity(42);
checksum_address.push_str("0x");
for (i, ch) in hex_address.chars().enumerate() {
// Get the corresponding nibble from the hash
let nibble = hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 }) & 0xf;
if nibble >= 8 {
checksum_address.push(ch.to_ascii_uppercase());
} else {
checksum_address.push(ch);
}
}
checksum_address
}
}
impl AsRef<[u8]> for AccountId20 {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Display for AccountId20 {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.checksum())
}
}
pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &ecdsa::PublicKey) -> bool {
let message_hash = keccak(message.as_ref());
let wrapped =
Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed");
ecdsa::internal::verify(&sig.0, &wrapped, pubkey)
}
/// An error handed back if creating a keypair fails.
#[derive(Debug, PartialEq, Display)]
pub enum Error {
/// Invalid seed.
#[display(fmt = "Invalid seed (was it the wrong length?)")]
InvalidSeed,
/// Invalid derivation path.
#[display(fmt = "Could not derive from path; some valeus in the path may have been >= 2^31?")]
DeriveFromPath,
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
/// Dev accounts, helpful for testing but not to be used in production,
/// since the secret keys are known.
pub mod dev {
use core::str::FromStr;
use crate::DEV_PHRASE;
use super::*;
once_static_cloned! {
pub fn alith() -> Keypair {
Keypair::from_phrase(
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 0)).unwrap()
}
pub fn baltathar() -> Keypair {
Keypair::from_phrase(
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 1)).unwrap()
}
pub fn charleth() -> Keypair {
Keypair::from_phrase(
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 2)).unwrap()
}
pub fn dorothy() -> Keypair {
Keypair::from_phrase(
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 3)).unwrap()
}
pub fn ethan() -> Keypair {
Keypair::from_phrase(
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 4)).unwrap()
}
pub fn faith() -> Keypair {
Keypair::from_phrase(
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 5)).unwrap()
}
}
}
#[cfg(feature = "subxt")]
mod subxt_compat {
use subxt_core::config::Config;
use subxt_core::tx::Signer as SignerT;
use super::*;
impl<T: Config> SignerT<T> for Keypair
where
T::AccountId: From<AccountId20>,
T::Address: From<AccountId20>,
T::Signature: From<Signature>,
{
fn account_id(&self) -> T::AccountId {
self.account_id().into()
}
fn address(&self) -> T::Address {
self.account_id().into()
}
fn sign(&self, signer_payload: &[u8]) -> T::Signature {
self.sign(signer_payload).into()
}
}
}
#[cfg(test)]
mod test {
use bip39::Mnemonic;
use proptest::prelude::*;
use secp256k1::Secp256k1;
use subxt_core::{config::*, tx::Signer as SignerT, utils::H256};
use super::*;
enum StubEthRuntimeConfig {}
impl Config for StubEthRuntimeConfig {
type Hash = H256;
type AccountId = AccountId20;
type Address = AccountId20;
type Signature = Signature;
type Hasher = substrate::BlakeTwo256;
type Header = substrate::SubstrateHeader<u32, substrate::BlakeTwo256>;
type ExtrinsicParams = SubstrateExtrinsicParams<Self>;
type AssetId = u32;
}
type SubxtSigner = dyn SignerT<StubEthRuntimeConfig>;
prop_compose! {
fn keypair()(seed in any::<[u8; 32]>()) -> Keypair {
let secret = secp256k1::SecretKey::from_slice(&seed).expect("valid secret key");
let inner = secp256k1::Keypair::from_secret_key(
&Secp256k1::new(),
&secret,
);
Keypair(ecdsa::Keypair(inner))
}
}
proptest! {
#[test]
fn check_from_phrase(
entropy in any::<[u8; 32]>(),
password in any::<Option<String>>(),
address in 1..(i32::MAX as u32),
account_idx in 1..(i32::MAX as u32),
) {
let mnemonic = bip39::Mnemonic::from_entropy(&entropy).expect("valid mnemonic");
let derivation_path = format!("m/44'/60'/{address}'/0/{account_idx}").parse().expect("valid derivation path");
let private = bip32::XPrv::derive_from_path(
mnemonic.to_seed(password.clone().unwrap_or("".to_string())),
&derivation_path,
).expect("valid private");
// Creating our own keypairs should be equivalent to using bip32 crate to do it:
assert_eq!(
Keypair::from_phrase(&mnemonic, password.as_deref(), DerivationPath::eth(address, account_idx)).expect("valid keypair"),
Keypair(ecdsa::Keypair::from_secret_key(private.to_bytes()).expect("valid ecdsa keypair"))
);
}
#[test]
fn check_from_phrase_bad_index(
address in (i32::MAX as u32)..=u32::MAX,
account_idx in (i32::MAX as u32)..=u32::MAX,
) {
let derivation_path_err = format!("m/44'/60'/{address}'/0/{account_idx}").parse::<DerivationPath>().expect_err("bad path expected");
// Creating invalid derivation paths (ie values too large) will result in an error.
assert_eq!(
derivation_path_err,
Error::DeriveFromPath
);
}
#[test]
fn check_subxt_signer_implementation_matches(keypair in keypair(), msg in ".*") {
let msg_as_bytes = msg.as_bytes();
assert_eq!(SubxtSigner::account_id(&keypair), keypair.account_id());
assert_eq!(SubxtSigner::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes));
}
#[test]
fn check_account_id(keypair in keypair()) {
// https://github.com/ethereumbook/ethereumbook/blob/develop/04keys-addresses.asciidoc#ethereum-addresses
let account_id = {
let uncompressed = keypair.0.0.public_key().serialize_uncompressed();
let hash = keccak(&uncompressed[1..]).0;
let hash20 = hash[12..].try_into().expect("should be 20 bytes");
AccountId20(hash20)
};
assert_eq!(keypair.account_id(), account_id);
}
#[test]
fn check_account_id_eq_address(keypair in keypair()) {
assert_eq!(SubxtSigner::account_id(&keypair), SubxtSigner::address(&keypair));
}
#[test]
fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") {
let sig = SubxtSigner::sign(&keypair, msg.as_bytes());
assert!(verify(
&sig,
msg,
&keypair.public_key())
);
}
}
/// Test that the dev accounts match those listed in the moonbeam README.
/// https://github.com/moonbeam-foundation/moonbeam/blob/96cf8898874509d529b03c4da0e07b2787bacb18/README.md
#[test]
fn check_dev_accounts_match() {
let cases = [
(
dev::alith(),
"0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac",
"0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133",
),
(
dev::baltathar(),
"0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0",
"0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b",
),
(
dev::charleth(),
"0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc",
"0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b",
),
(
dev::dorothy(),
"0x773539d4Ac0e786233D90A233654ccEE26a613D9",
"0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68",
),
(
dev::ethan(),
"0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB",
"0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4",
),
(
dev::faith(),
"0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d",
"0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df",
),
];
for (case_idx, (keypair, exp_account_id, exp_priv_key)) in cases.into_iter().enumerate() {
let act_account_id = keypair.account_id().to_string();
let act_priv_key = format!("0x{}", &keypair.0 .0.display_secret());
assert_eq!(
exp_account_id, act_account_id,
"account ID mismatch in {case_idx}"
);
assert_eq!(
exp_priv_key, act_priv_key,
"private key mismatch in {case_idx}"
);
}
}
// This is a part of the test set linked in BIP39 and copied from https://github.com/trezor/python-mnemonic/blob/f5a975ab10c035596d65d854d21164266ffed284/vectors.json.
// The passphrase is always TREZOR. We check that keys generated with the mnemonic (and no derivation path) line up with the seeds given.
#[test]
fn check_basic_bip39_compliance() {
let mnemonics_and_seeds = [
(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
),
(
"legal winner thank year wave sausage worth useful legal winner thank yellow",
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607",
),
(
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8",
),
(
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069",
),
(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa",
),
(
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd",
),
(
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65",
),
(
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528",
),
(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8",
),
(
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87",
),
(
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f",
),
(
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad",
),
(
"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
"274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028",
),
(
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
"628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac",
),
(
"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
"64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440",
),
(
"scheme spot photo card baby mountain device kick cradle pact join borrow",
"ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612",
),
(
"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
"fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d",
),
(
"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
"72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d",
),
(
"cat swing flag economy stadium alone churn speed unique patch report train",
"deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5",
),
(
"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
"4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02",
),
(
"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
"26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d",
),
(
"vessel ladder alter error federal sibling chat ability sun glass valve picture",
"2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f",
),
(
"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
"7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88",
),
(
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
"01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998",
)
];
for (idx, (m, s)) in mnemonics_and_seeds.into_iter().enumerate() {
let m = Mnemonic::parse(m).expect("mnemonic should be valid");
let pair1 = Keypair::from_phrase(&m, Some("TREZOR"), DerivationPath::empty()).unwrap();
let s = hex::decode(s).expect("seed hex should be valid");
let pair2 = Keypair::from_seed(&s).unwrap();
assert_eq!(pair1, pair2, "pair1 and pair2 at index {idx} don't match");
}
}
/// Test the same accounts from moonbeam so we know for sure that this implementation is working
/// https://github.com/moonbeam-foundation/moonbeam/blob/e70ee0d427dfee8987d5a5671a66416ee6ec38aa/primitives/account/src/lib.rs#L217
mod moonbeam_sanity_tests {
use hex_literal::hex;
use super::*;
const KEY_1: [u8; 32] =
hex!("502f97299c472b88754accd412b7c9a6062ef3186fba0c0388365e1edec24875");
const KEY_2: [u8; 32] =
hex!("0f02ba4d7f83e59eaa32eae9c3c4d99b68ce76decade21cdab7ecce8f4aef81a");
const KEY_3: [u8; 32] =
hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470");
#[test]
fn test_account_derivation_1() {
let kp = Keypair::from_secret_key(KEY_1).expect("valid keypair");
assert_eq!(
kp.account_id().to_string(),
"0x976f8456E4e2034179B284A23C0e0c8f6d3da50c"
);
}
#[test]
fn test_account_derivation_2() {
let kp = Keypair::from_secret_key(KEY_2).expect("valid keypair");
assert_eq!(
kp.account_id().to_string(),
"0x420e9F260B40aF7E49440ceAd3069f8e82A5230f"
);
}
#[test]
fn test_account_derivation_3() {
let kp = Keypair::from_secret_key(KEY_3).expect("valid keypair");
assert_eq!(
kp.account_id().to_string(),
"0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6"
);
}
}
}
+5
View File
@@ -32,6 +32,11 @@ pub mod sr25519;
#[cfg_attr(docsrs, doc(cfg(feature = "ecdsa")))]
pub mod ecdsa;
// An ethereum signer implementation.
#[cfg(feature = "unstable-eth")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-eth")))]
pub mod eth;
// Re-export useful bits and pieces for generating a Pair from a phrase,
// namely the Mnemonic struct.
pub use bip39;
+9 -9
View File
@@ -16,11 +16,11 @@ use schnorrkel::{
};
use secrecy::ExposeSecret;
const SEED_LENGTH: usize = schnorrkel::keys::MINI_SECRET_KEY_LENGTH;
const SECRET_KEY_LENGTH: usize = schnorrkel::keys::MINI_SECRET_KEY_LENGTH;
const SIGNING_CTX: &[u8] = b"substrate";
/// Seed bytes used to generate a key pair.
pub type Seed = [u8; SEED_LENGTH];
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
/// A signature generated by [`Keypair::sign()`]. These bytes are equivalent
/// to a Substrate `MultiSignature::sr25519(bytes)`.
@@ -73,8 +73,8 @@ impl Keypair {
// Else, parse the phrase string taking the password into account. This is
// the same approach taken in sp_core::crypto::Pair::from_string_with_seed.
let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") {
let seed = Seed::from_hex(hex_str)?;
Self::from_seed(seed)?
let seed = SecretKeyBytes::from_hex(hex_str)?;
Self::from_secret_key(seed)?
} else {
let phrase = bip39::Mnemonic::from_str(phrase.expose_secret().as_str())?;
let pass_str = password.as_ref().map(|p| p.expose_secret().as_str());
@@ -103,20 +103,20 @@ impl Keypair {
let big_seed =
seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?;
let seed: Seed = big_seed[..SEED_LENGTH]
let seed: SecretKeyBytes = big_seed[..SECRET_KEY_LENGTH]
.try_into()
.expect("should be valid Seed");
Self::from_seed(seed)
Self::from_secret_key(seed)
}
/// Turn a 32 byte seed into a keypair.
/// Turn a 32 byte secret key into a keypair.
///
/// # Warning
///
/// This will only be secure if the seed is secure!
pub fn from_seed(seed: Seed) -> Result<Self, Error> {
let keypair = MiniSecretKey::from_bytes(&seed)
pub fn from_secret_key(secret_key_bytes: SecretKeyBytes) -> Result<Self, Error> {
let keypair = MiniSecretKey::from_bytes(&secret_key_bytes)
.map_err(|_| Error::InvalidSeed)?
.expand_to_keypair(ExpansionMode::Ed25519);
+7 -1
View File
@@ -13,7 +13,13 @@ console_error_panic_hook = "0.1.7"
# enable the "web" feature here but don't want it enabled as part
# of workspace builds. Also disable the "subxt" feature here because
# we want to ensure it works in isolation of that.
subxt-signer = { path = "..", default-features = false, features = ["web", "sr25519", "ecdsa", "std"] }
subxt-signer = { path = "..", default-features = false, features = [
"web",
"sr25519",
"ecdsa",
"unstable-eth",
"std",
] }
# this shouldn't be needed, it's in workspace.exclude, but still
# I get the complaint unless I add it...
+22 -4
View File
@@ -1,6 +1,6 @@
#![cfg(target_arch = "wasm32")]
use subxt_signer::{ ecdsa, sr25519 };
use subxt_signer::{ecdsa, eth, sr25519};
use wasm_bindgen_test::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@@ -22,7 +22,11 @@ async fn wasm_sr25519_signing_works() {
// There's some non-determinism in the signing, so this ensures that
// the rand stuff is configured properly to run ok in wasm.
let signature = alice.sign(b"Hello there");
assert!(sr25519::verify(&signature, b"Hello there", &alice.public_key()));
assert!(sr25519::verify(
&signature,
b"Hello there",
&alice.public_key()
));
}
#[wasm_bindgen_test]
@@ -32,5 +36,19 @@ async fn wasm_ecdsa_signing_works() {
// There's some non-determinism in the signing, so this ensures that
// the rand stuff is configured properly to run ok in wasm.
let signature = alice.sign(b"Hello there");
assert!(ecdsa::verify(&signature, b"Hello there", &alice.public_key()));
}
assert!(ecdsa::verify(
&signature,
b"Hello there",
&alice.public_key()
));
}
#[wasm_bindgen_test]
async fn wasm_eth_signing_works() {
let alice = eth::dev::alith();
// There's some non-determinism in the signing, so this ensures that
// the rand stuff is configured properly to run ok in wasm.
let signature = alice.sign(b"Hello there");
assert!(eth::verify(&signature, b"Hello there", &alice.public_key()));
}