ECDSA Support in signer (#1064)

* Add ecdsa support to signer

* Remove nix. Cleanup.

* remove ecdsa example and small tidy

* fmt

* fix wasm test

* feature flag ecdsa/sr25519 support, use global signing context

* clippy fix

* ensure signers all impl Signer trait in doc test

* fix CI

* fix digner test

* remove dead code warnings when no features enabled

* move dead code attr to right place

* fix random clippy error that popped up

---------

Co-authored-by: Lech Głowiak <lech.glowiak@gmail.com>
This commit is contained in:
James Wilson
2023-07-19 10:17:27 +01:00
committed by GitHub
parent 4bda673847
commit fa16080a03
11 changed files with 488 additions and 12 deletions
+4 -1
View File
@@ -70,7 +70,10 @@ jobs:
# Subxt-signer has the "subxt" features enabled in the "check all targets" test. Run it on its own to
# check it without. We can't enable subxt or web features here, so no cargo hack.
- name: Cargo check subxt-signer
run: cargo check -p subxt-signer
run: |
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
# We can't enable web features here, so no cargo hack.
- name: Cargo check subxt-lightclient
Generated
+22 -3
View File
@@ -3314,7 +3314,16 @@ version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62"
dependencies = [
"secp256k1-sys",
"secp256k1-sys 0.6.1",
]
[[package]]
name = "secp256k1"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f"
dependencies = [
"secp256k1-sys 0.8.1",
]
[[package]]
@@ -3326,6 +3335,15 @@ dependencies = [
"cc",
]
[[package]]
name = "secp256k1-sys"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e"
dependencies = [
"cc",
]
[[package]]
name = "secrecy"
version = "0.8.0"
@@ -3727,7 +3745,7 @@ dependencies = [
"regex",
"scale-info",
"schnorrkel 0.9.1",
"secp256k1",
"secp256k1 0.24.3",
"secrecy",
"serde",
"sp-core-hashing",
@@ -3807,7 +3825,7 @@ dependencies = [
"log",
"parity-scale-codec",
"rustversion",
"secp256k1",
"secp256k1 0.24.3",
"sp-core",
"sp-externalities",
"sp-keystore",
@@ -4290,6 +4308,7 @@ dependencies = [
"pbkdf2 0.12.2",
"regex",
"schnorrkel 0.10.2",
"secp256k1 0.27.0",
"secrecy",
"sha2 0.10.7",
"sp-core",
+2 -1
View File
@@ -116,6 +116,7 @@ bip39 = "2.0.0"
hmac = "0.12.1"
pbkdf2 = { version = "0.12.2", default-features = false }
schnorrkel = "0.10.2"
secp256k1 = "0.27.0"
secrecy = "0.8.0"
sha2 = "0.10.6"
zeroize = { version = "1", default-features = false }
zeroize = { version = "1", default-features = false }
+10 -2
View File
@@ -15,7 +15,14 @@ description = "Sign extrinsics to be submitted by Subxt"
keywords = ["parity", "subxt", "extrinsic", "signer"]
[features]
default = []
default = ["sr25519", "ecdsa"]
# Pick the signer implementation(s) you need by enabling the
# corresponding features. Note: I had more difficulties getting
# ecdsa compiling to WASM on my mac; following this comment helped:
# https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699
sr25519 = ["schnorrkel"]
ecdsa = ["secp256k1"]
# 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 +44,8 @@ sha2 = { workspace = true }
hmac = { workspace = true }
zeroize = { workspace = true }
bip39 = { workspace = true }
schnorrkel = { workspace = true }
schnorrkel = { workspace = true, optional = true }
secp256k1 = { workspace = true, features = ["recovery", "global-context"], optional = true }
secrecy = { workspace = true }
# We only pull this in to enable the JS flag for schnorrkel to use.
+1
View File
@@ -10,6 +10,7 @@ use zeroize::Zeroize;
/// This is taken from `substrate-bip39` so that we can keep dependencies in line, and
/// is the same logic that sp-core uses to go from mnemonic entropy to seed. Returns
/// `None` if invalid length.
#[allow(dead_code)]
pub fn seed_from_entropy(entropy: &[u8], password: &str) -> Option<[u8; 64]> {
if entropy.len() < 16 || entropy.len() > 32 || entropy.len() % 4 != 0 {
return None;
+420
View File
@@ -0,0 +1,420 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! An ecdsa keypair implementation.
use codec::Encode;
use crate::crypto::{seed_from_entropy, DeriveJunction, SecretUri};
use hex::FromHex;
use secp256k1::{ecdsa::RecoverableSignature, Message, SecretKey, SECP256K1};
use secrecy::ExposeSecret;
const SEED_LENGTH: usize = 32;
/// Seed bytes used to generate a key pair.
pub type Seed = [u8; SEED_LENGTH];
/// A signature generated by [`Keypair::sign()`]. These bytes are equivalent
/// to a Substrate `MultiSignature::Ecdsa(bytes)`.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Signature(pub [u8; 65]);
impl AsRef<[u8]> for Signature {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
/// The (compressed) public key for an [`Keypair`] key pair.
#[derive(Debug, Clone)]
pub struct PublicKey(pub [u8; 33]);
impl AsRef<[u8]> for PublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
/// An ecdsa keypair implementation.
#[derive(Debug, Clone)]
pub struct Keypair(pub secp256k1::KeyPair);
impl Keypair {
/// Create an ecdsa keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more.
///
/// # Example
///
/// ```rust
/// use subxt_signer::{ SecretUri, ecdsa::Keypair };
/// use std::str::FromStr;
///
/// let uri = SecretUri::from_str("//Alice").unwrap();
/// let keypair = Keypair::from_uri(&uri).unwrap();
///
/// keypair.sign(b"Hello world!");
/// ```
pub fn from_uri(uri: &SecretUri) -> Result<Self, Error> {
let SecretUri {
junctions,
phrase,
password,
} = uri;
// If the phrase is hex, convert bytes directly into a seed, ignoring password.
// 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)?
} else {
let phrase = bip39::Mnemonic::parse(phrase.expose_secret().as_str())?;
let pass_str = password.as_ref().map(|p| p.expose_secret().as_str());
Self::from_phrase(&phrase, pass_str)?
};
// Now, use any "junctions" to derive a new key from this root key.
key.derive(junctions.iter().copied())
}
/// Create an ecdsa keypair from a BIP-39 mnemonic phrase and optional password.
///
/// # Example
///
/// ```rust
/// use subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair };
///
/// 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).unwrap();
///
/// keypair.sign(b"Hello world!");
/// ```
pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result<Self, Error> {
let big_seed = seed_from_entropy(&mnemonic.to_entropy(), password.unwrap_or(""))
.ok_or(Error::InvalidSeed)?;
let seed: Seed = big_seed[..SEED_LENGTH]
.try_into()
.expect("should be valid Seed");
Self::from_seed(seed)
}
/// 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)?;
Ok(Self(secp256k1::KeyPair::from_secret_key(
SECP256K1, &secret,
)))
}
/// Derive a child key from this one given a series of junctions.
///
/// # Example
///
/// ```rust
/// use subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair, DeriveJunction };
///
/// 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).unwrap();
///
/// // Equivalent to the URI path '//Alice//stash':
/// let new_keypair = keypair.derive([
/// DeriveJunction::hard("Alice"),
/// DeriveJunction::hard("stash")
/// ]);
/// ```
pub fn derive<Js: IntoIterator<Item = DeriveJunction>>(
&self,
junctions: Js,
) -> Result<Self, Error> {
let mut acc = self.0.secret_key().clone().secret_bytes();
for junction in junctions {
match junction {
DeriveJunction::Soft(_) => return Err(Error::SoftJunction),
DeriveJunction::Hard(junction_bytes) => {
acc = ("Secp256k1HDKD", acc, junction_bytes)
.using_encoded(sp_core_hashing::blake2_256)
}
}
}
Self::from_seed(acc)
}
/// Obtain the [`PublicKey`] part of this key pair, which can be used in calls to [`verify()`].
/// or otherwise converted into an address. In case of ECDSA, the public key bytes are not
/// equivalent to a Substrate `AccountId32`. They have to be hashed to obtain `AccountId32`.
pub fn public_key(&self) -> PublicKey {
PublicKey(self.0.public_key().serialize())
}
/// 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_core_hashing::blake2_256(message);
// From sp_core::ecdsa::sign_prehashed:
let wrapped = Message::from_slice(&message_hash).expect("Message is 32 bytes; qed");
let recsig: RecoverableSignature =
SECP256K1.sign_ecdsa_recoverable(&wrapped, &self.0.secret_key());
// From sp_core::ecdsa's `impl From<RecoverableSignature> for Signature`:
let (recid, sig) = 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)
}
}
/// Verify that some signature for a message was created by the owner of the [`PublicKey`].
///
/// ```rust
/// use subxt_signer::{ bip39::Mnemonic, ecdsa };
///
/// let keypair = ecdsa::dev::alice();
/// let message = b"Hello!";
///
/// let signature = keypair.sign(message);
/// let public_key = keypair.public_key();
/// 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_core_hashing::blake2_256(message.as_ref());
let wrapped = Message::from_slice(&message_hash).expect("Message is 32 bytes; qed");
signature.verify(&wrapped, &public).is_ok()
}
/// An error handed back if creating a keypair fails.
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum Error {
/// Invalid seed.
#[error("Invalid seed (was it the wrong length?)")]
InvalidSeed,
/// Invalid seed.
#[error("Invalid seed for ECDSA, contained soft junction")]
SoftJunction,
/// Invalid phrase.
#[error("Cannot parse phrase: {0}")]
Phrase(#[from] bip39::Error),
/// Invalid hex.
#[error("Cannot parse hex string: {0}")]
Hex(#[from] hex::FromHexError),
}
/// Dev accounts, helpful for testing but not to be used in production,
/// since the secret keys are known.
pub mod dev {
use super::*;
use std::str::FromStr;
once_static_cloned! {
/// Equivalent to `{DEV_PHRASE}//Alice`.
pub fn alice() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//Alice").unwrap()).unwrap()
}
/// Equivalent to `{DEV_PHRASE}//Bob`.
pub fn bob() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//Bob").unwrap()).unwrap()
}
/// Equivalent to `{DEV_PHRASE}//Charlie`.
pub fn charlie() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//Charlie").unwrap()).unwrap()
}
/// Equivalent to `{DEV_PHRASE}//Dave`.
pub fn dave() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//Dave").unwrap()).unwrap()
}
/// Equivalent to `{DEV_PHRASE}//Eve`.
pub fn eve() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//Eve").unwrap()).unwrap()
}
/// Equivalent to `{DEV_PHRASE}//Ferdie`.
pub fn ferdie() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//Ferdie").unwrap()).unwrap()
}
/// Equivalent to `{DEV_PHRASE}//One`.
pub fn one() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//One").unwrap()).unwrap()
}
/// Equivalent to `{DEV_PHRASE}//Two`.
pub fn two() -> Keypair {
Keypair::from_uri(&SecretUri::from_str("//Two").unwrap()).unwrap()
}
}
}
// Make `Keypair` usable to sign transactions in Subxt. This is optional so that
// `subxt-signer` can be used entirely independently of Subxt.
#[cfg(feature = "subxt")]
mod subxt_compat {
use super::*;
use subxt::config::Config;
use subxt::tx::Signer as SignerT;
use subxt::utils::{AccountId32, MultiAddress, MultiSignature};
impl From<Signature> for MultiSignature {
fn from(value: Signature) -> Self {
MultiSignature::Ecdsa(value.0)
}
}
impl From<PublicKey> for AccountId32 {
fn from(value: PublicKey) -> Self {
value.to_account_id()
}
}
impl<T> From<PublicKey> for MultiAddress<AccountId32, T> {
fn from(value: PublicKey) -> Self {
value.to_address()
}
}
impl PublicKey {
/// A shortcut to obtain an [`AccountId32`] from a [`PublicKey`].
/// We often want this type, and using this method avoids any
/// ambiguous type resolution issues.
pub fn to_account_id(self) -> AccountId32 {
AccountId32(sp_core_hashing::blake2_256(&self.0))
}
/// A shortcut to obtain a [`MultiAddress`] from a [`PublicKey`].
/// We often want this type, and using this method avoids any
/// ambiguous type resolution issues.
pub fn to_address<T>(self) -> MultiAddress<AccountId32, T> {
MultiAddress::Id(self.to_account_id())
}
}
impl<T: Config> SignerT<T> for Keypair
where
T::AccountId: From<PublicKey>,
T::Address: From<PublicKey>,
T::Signature: From<Signature>,
{
fn account_id(&self) -> T::AccountId {
self.public_key().into()
}
fn address(&self) -> T::Address {
self.public_key().into()
}
fn sign(&self, signer_payload: &[u8]) -> T::Signature {
self.sign(signer_payload).into()
}
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use super::*;
use sp_core::crypto::Pair as _;
use sp_core::ecdsa::Pair as SpPair;
#[test]
fn check_from_phrase_matches() {
for _ in 0..20 {
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None);
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
let pair = Keypair::from_phrase(&phrase, None).expect("should be valid");
assert_eq!(sp_pair.public().0, pair.public_key().0);
}
}
#[test]
fn check_from_phrase_with_password_matches() {
for _ in 0..20 {
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
assert_eq!(sp_pair.public().0, pair.public_key().0);
}
}
#[test]
fn check_from_secret_uri_matches() {
// Some derive junctions to check that the logic there aligns:
let uri_paths = ["//bar", "//0001", "//1", "//0001", "//foo//bar//wibble"];
for i in 0..2 {
for path in &uri_paths {
// Build an sp_core::Pair that includes a phrase, path and password:
let password = format!("Testing{i}");
let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some(&password));
let uri = format!("{phrase}{path}///{password}");
let sp_pair = SpPair::from_string(&uri, None).expect("should be valid");
// Now build a local Keypair using the equivalent API:
let uri = SecretUri::from_str(&uri).expect("should be valid secret URI");
let pair = Keypair::from_uri(&uri).expect("should be valid");
// They should match:
assert_eq!(sp_pair.public().0, pair.public_key().0);
}
}
}
#[test]
fn check_derive_errs_with_soft_junction() {
let uri_paths = ["/bar", "/1", "//foo//bar/wibble"];
for path in &uri_paths {
let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None);
let uri = format!("{phrase}{path}");
let uri = SecretUri::from_str(&uri).expect("should be valid secret URI");
let result = Keypair::from_uri(&uri);
assert_eq!(result.err(), Some(Error::SoftJunction));
}
}
#[test]
fn check_signing_and_verifying_matches() {
use sp_core::ecdsa::Signature as SpSignature;
for _ in 0..20 {
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
let message = b"Hello world";
let sp_sig = sp_pair.sign(message).0;
let sig = pair.sign(message).0;
assert!(SpPair::verify(
&SpSignature(sig),
message,
&sp_pair.public(),
));
assert!(verify(&Signature(sp_sig), message, &pair.public_key()));
}
}
#[test]
fn check_hex_uris() {
// Hex URIs seem to ignore the password on sp_core and here. Check that this is consistent.
let uri_str =
"0x1122334455667788112233445566778811223344556677881122334455667788///SomePassword";
let uri = SecretUri::from_str(uri_str).expect("should be valid");
let pair = Keypair::from_uri(&uri).expect("should be valid");
let sp_pair = SpPair::from_string(uri_str, None).expect("should be valid");
assert_eq!(pair.public_key().0, sp_pair.public().0);
}
}
+5
View File
@@ -20,8 +20,13 @@ mod utils;
mod crypto;
// An sr25519 key pair implementation.
#[cfg(feature = "sr25519")]
pub mod sr25519;
// An ecdsa key pair implementation.
#[cfg(feature = "ecdsa")]
pub mod ecdsa;
// Re-export useful bits and pieces for generating a Pair from a phrase,
// namely the Mnemonic struct.
pub use bip39;
+2
View File
@@ -2,6 +2,8 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
#![allow(unused_macros)]
/// Use like:
///
/// ```rust,ignore
+1 -1
View File
@@ -14,7 +14,7 @@ serde_json = "1"
# 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"] }
subxt-signer = { path = "..", default-features = false, features = ["web", "sr25519", "ecdsa"] }
# this shouldn't be needed, it's in workspace.exclude, but still
# I get the complaint unless I add it...
+12 -2
View File
@@ -1,6 +1,6 @@
#![cfg(target_arch = "wasm32")]
use subxt_signer::sr25519;
use subxt_signer::{ ecdsa, sr25519 };
use wasm_bindgen_test::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
@@ -16,7 +16,7 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
// (subxt seems to, for instance).
#[wasm_bindgen_test]
async fn wasm_signing_works() {
async fn wasm_sr25519_signing_works() {
let alice = sr25519::dev::alice();
// There's some non-determinism in the signing, so this ensures that
@@ -24,3 +24,13 @@ async fn wasm_signing_works() {
let signature = alice.sign(b"Hello there");
assert!(sr25519::verify(&signature, b"Hello there", &alice.public_key()));
}
#[wasm_bindgen_test]
async fn wasm_ecdsa_signing_works() {
let alice = ecdsa::dev::alice();
// 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()));
}
+9 -2
View File
@@ -65,7 +65,7 @@
//!
//! There are two main ways to create a compatible signer instance:
//! 1. The `subxt_signer` crate provides a WASM compatible implementation of [`crate::tx::Signer`]
//! for chains which require sr25519 signatures (requires the `subxt` feature to be enabled).
//! for chains which require sr25519 or ecdsa signatures (requires the `subxt` feature to be enabled).
//! 2. Alternately, Subxt can use instances of Substrate's [`sp_core::Pair`] to sign things by wrapping
//! them in a [`crate::tx::PairSigner`] (requires the `substrate-compat` feature to be enabled).
//!
@@ -104,9 +104,16 @@
//! let keypair = sp_core::sr25519::Pair::from_string("vessel ladder alter error federal sibling chat ability sun glass valve picture/0/1///Password", None)
//! .expect("valid URI");
//! let keypair = PairSigner::<PolkadotConfig,_>::new(keypair);
//! #
//! # // Test that these all impl Signer trait while we're here:
//! #
//! # fn is_subxt_signer(_signer: impl subxt::tx::Signer<PolkadotConfig>) {}
//! # is_subxt_signer(subxt_signer::sr25519::dev::alice());
//! # is_subxt_signer(subxt_signer::ecdsa::dev::alice());
//! # is_subxt_signer(PairSigner::<PolkadotConfig,_>::new(sp_keyring::AccountKeyring::Alice.pair()));
//! ```
//!
//! See the `subxt_signer::sr25519::Keypair` or the [`sp_core::Pair`] docs for more ways to construct
//! See the `subxt_signer` crate or the [`sp_core::Pair`] docs for more ways to construct
//! and work with key pairs.
//!
//! If this isn't suitable/available, you can either implement [`crate::tx::Signer`] yourself to use