Skip to content

Commit

Permalink
from_seed => from_secret_key, and check bip39 compliance eg no deriva…
Browse files Browse the repository at this point in the history
…tion path mnemonics vs seeds
  • Loading branch information
jsdw committed Apr 12, 2024
1 parent eddb979 commit 5e22a9f
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 23 deletions.
2 changes: 1 addition & 1 deletion signer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ description = "Sign extrinsics to be submitted by Subxt"
keywords = ["parity", "subxt", "extrinsic", "signer"]

[features]
default = ["sr25519", "ecdsa", "subxt", "std"]
default = ["sr25519", "ecdsa", "unstable-eth", "subxt", "std"]
std = [
"regex/std",
"sp-crypto-hashing/std",
Expand Down
18 changes: 9 additions & 9 deletions signer/src/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -97,20 +97,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 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.
///
/// # 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,
Expand Down Expand Up @@ -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()`].
Expand Down
149 changes: 136 additions & 13 deletions signer/src/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use derive_more::Display;
use keccak_hash::keccak;
use secp256k1::Message;

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];
/// Bytes representing a private key.
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];

/// An ethereum keypair implementation.
#[derive(Debug, Clone, PartialEq, Eq)]
Expand All @@ -39,7 +39,7 @@ impl Keypair {
/// # Example
///
/// ```rust
/// use subxt_signer::{ bip39::Mnemonic, eth::Keypair };
/// 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();
Expand All @@ -66,16 +66,26 @@ impl Keypair {
let private = bip32::XPrv::derive_from_path(seed, &derivation_path.inner)
.map_err(|_| Error::DeriveFromPath)?;

Keypair::from_seed(private.to_bytes())
Keypair::from_secret_key(private.to_bytes())
}

/// Turn a 32 byte seed into a keypair.
/// 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: Seed) -> Result<Self, Error> {
ecdsa::Keypair::from_seed(seed)
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)
}
Expand Down Expand Up @@ -284,6 +294,7 @@ mod subxt_compat {

#[cfg(test)]
mod test {
use bip39::Mnemonic;
use proptest::prelude::*;
use secp256k1::Secp256k1;

Expand Down Expand Up @@ -336,7 +347,7 @@ mod test {
// 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_seed(private.to_bytes()).expect("valid ecdsa keypair"))
Keypair(ecdsa::Keypair::from_secret_key(private.to_bytes()).expect("valid ecdsa keypair"))
);
}

Expand Down Expand Up @@ -445,9 +456,121 @@ mod test {
}
}

// 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
#[cfg(test)]
mod moonbeam_sanity_tests {
use hex_literal::hex;

Expand All @@ -462,7 +585,7 @@ mod test {

#[test]
fn test_account_derivation_1() {
let kp = Keypair::from_seed(KEY_1).expect("valid keypair");
let kp = Keypair::from_secret_key(KEY_1).expect("valid keypair");
assert_eq!(
kp.account_id().to_string(),
"0x976f8456E4e2034179B284A23C0e0c8f6d3da50c"
Expand All @@ -471,7 +594,7 @@ mod test {

#[test]
fn test_account_derivation_2() {
let kp = Keypair::from_seed(KEY_2).expect("valid keypair");
let kp = Keypair::from_secret_key(KEY_2).expect("valid keypair");
assert_eq!(
kp.account_id().to_string(),
"0x420e9F260B40aF7E49440ceAd3069f8e82A5230f"
Expand All @@ -480,7 +603,7 @@ mod test {

#[test]
fn test_account_derivation_3() {
let kp = Keypair::from_seed(KEY_3).expect("valid keypair");
let kp = Keypair::from_secret_key(KEY_3).expect("valid keypair");
assert_eq!(
kp.account_id().to_string(),
"0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6"
Expand Down

0 comments on commit 5e22a9f

Please sign in to comment.