port subxt-signer to no-std (such a pain)

This commit is contained in:
Tadeo hepperle
2024-02-02 14:39:03 +01:00
parent 930f2c4300
commit 14b21ab0df
13 changed files with 378 additions and 62 deletions
+15 -11
View File
@@ -15,7 +15,8 @@ description = "Sign extrinsics to be submitted by Subxt"
keywords = ["parity", "subxt", "extrinsic", "signer"]
[features]
default = ["sr25519", "ecdsa", "subxt"]
default = ["sr25519", "ecdsa", "subxt", "std"]
std = []
# Pick the signer implementation(s) you need by enabling the
# corresponding features. Note: I had more difficulties getting
@@ -34,25 +35,28 @@ web = ["getrandom/js"]
[dependencies]
subxt-core = { workspace = true, optional = true, default-features = false }
regex = { workspace = true }
regex = { workspace = true, default-features = false }
hex = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
sp-core-hashing = { workspace = true }
thiserror = { workspace = true }
pbkdf2 = { workspace = true }
sha2 = { workspace = true }
hmac = { workspace = true }
sp-core-hashing = { workspace = true, default-features = false }
derive_more = { workspace = true }
pbkdf2 = { workspace = true, default-features = false }
sha2 = { workspace = true, default-features = false }
hmac = { workspace = true, default-features = false }
zeroize = { workspace = true }
bip39 = { workspace = true }
schnorrkel = { workspace = true, optional = true }
secp256k1 = { workspace = true, features = ["recovery", "global-context"], optional = true }
bip39 = { workspace = true, default-features = false, features = ["unicode-normalization"] }
schnorrkel = { workspace = true, optional = true, default-features = false }
secp256k1 = { workspace = true, optional = true, default-features = false, features = ["alloc", "recovery"] } # features = ["recovery", "global-context"],
secrecy = { workspace = true }
# We only pull this in because `std::sync::OnceLock` is not available in no-std contexts.
once_cell = { workspace = true, default-features = false, features = ["alloc", "critical-section"] }
# We only pull this in to enable the JS flag for schnorrkel to use.
getrandom = { workspace = true, optional = true }
[dev-dependencies]
sp-core = { workspace = true, features = ["std"] }
sp-core = { workspace = true, default-features = false }
sp-keyring = { workspace = true }
[package.metadata.cargo-machete]
+5 -3
View File
@@ -3,6 +3,8 @@
// see LICENSE for license details.
use super::DeriveJunction;
use alloc::vec::Vec;
use derive_more::Display;
use regex::Regex;
use secrecy::SecretString;
@@ -88,7 +90,7 @@ pub struct SecretUri {
pub junctions: Vec<DeriveJunction>,
}
impl std::str::FromStr for SecretUri {
impl core::str::FromStr for SecretUri {
type Err = SecretUriError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -115,10 +117,10 @@ impl std::str::FromStr for SecretUri {
}
/// This is returned if `FromStr` cannot parse a string into a `SecretUri`.
#[derive(Debug, Copy, Clone, PartialEq, thiserror::Error)]
#[derive(Debug, Copy, Clone, PartialEq, Display)]
pub enum SecretUriError {
/// Parsing the secret URI from a string failed; wrong format.
#[error("Invalid secret phrase format")]
#[display(fmt = "Invalid secret phrase format")]
InvalidFormat,
}
+1
View File
@@ -2,6 +2,7 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use alloc::string::String;
use hmac::Hmac;
use pbkdf2::pbkdf2;
use sha2::Sha512;
+37 -19
View File
@@ -6,10 +6,22 @@
use codec::Encode;
use crate::crypto::{seed_from_entropy, DeriveJunction, SecretUri};
use core::str::FromStr;
use derive_more::{Display, From};
use hex::FromHex;
use secp256k1::{ecdsa::RecoverableSignature, Message, SecretKey, SECP256K1};
use secp256k1::{ecdsa::RecoverableSignature, All, Message, Secp256k1, SecretKey};
use secrecy::ExposeSecret;
struct GlobalSecp256K1Context;
impl core::ops::Deref for GlobalSecp256K1Context {
type Target = Secp256k1<All>;
fn deref(&self) -> &Self::Target {
static ONCE: once_cell::sync::OnceCell<Secp256k1<All>> = once_cell::sync::OnceCell::new();
ONCE.get_or_init(|| Secp256k1::new())
}
}
const SEED_LENGTH: usize = 32;
/// Seed bytes used to generate a key pair.
@@ -68,7 +80,7 @@ impl Keypair {
let seed = Seed::from_hex(hex_str)?;
Self::from_seed(seed)?
} else {
let phrase = bip39::Mnemonic::parse(phrase.expose_secret().as_str())?;
let phrase = bip39::Mnemonic::from_str(phrase.expose_secret().as_str())?;
let pass_str = password.as_ref().map(|p| p.expose_secret().as_str());
Self::from_phrase(&phrase, pass_str)?
};
@@ -91,8 +103,9 @@ impl Keypair {
/// 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 (arr, len) = mnemonic.to_entropy_array();
let big_seed =
seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?;
let seed: Seed = big_seed[..SEED_LENGTH]
.try_into()
@@ -109,7 +122,8 @@ impl Keypair {
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,
&GlobalSecp256K1Context,
&secret,
)))
}
@@ -161,9 +175,9 @@ impl Keypair {
// From sp_core::ecdsa::sign_prehashed:
let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed");
let recsig: RecoverableSignature =
SECP256K1.sign_ecdsa_recoverable(&wrapped, &self.0.secret_key());
GlobalSecp256K1Context.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 (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;
@@ -192,31 +206,35 @@ pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -
};
let message_hash = sp_core_hashing::blake2_256(message.as_ref());
let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed");
signature.verify(&wrapped, &public).is_ok()
GlobalSecp256K1Context
.verify_ecdsa(&wrapped, &signature, &public)
.is_ok()
}
/// An error handed back if creating a keypair fails.
#[derive(Debug, PartialEq, thiserror::Error)]
#[derive(Debug, PartialEq, Display, From)]
pub enum Error {
/// Invalid seed.
#[error("Invalid seed (was it the wrong length?)")]
#[display(fmt = "Invalid seed (was it the wrong length?)")]
#[from(ignore)]
InvalidSeed,
/// Invalid seed.
#[error("Invalid seed for ECDSA, contained soft junction")]
#[display(fmt = "Invalid seed for ECDSA, contained soft junction")]
#[from(ignore)]
SoftJunction,
/// Invalid phrase.
#[error("Cannot parse phrase: {0}")]
Phrase(#[from] bip39::Error),
#[display(fmt = "Cannot parse phrase: {_0}")]
Phrase(bip39::Error),
/// Invalid hex.
#[error("Cannot parse hex string: {0}")]
Hex(#[from] hex::FromHexError),
#[display(fmt = "Cannot parse hex string: {_0}")]
Hex(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;
use core::str::FromStr;
once_static_cloned! {
/// Equivalent to `{DEV_PHRASE}//Alice`.
@@ -260,9 +278,9 @@ pub mod dev {
mod subxt_compat {
use super::*;
use subxt::config::Config;
use subxt::tx::Signer as SignerT;
use subxt::utils::{AccountId32, MultiAddress, MultiSignature};
use subxt_core::config::Config;
use subxt_core::utils::{AccountId32, MultiAddress, MultiSignature};
use subxt_core::Signer as SignerT;
impl From<Signature> for MultiSignature {
fn from(value: Signature) -> Self {
+3 -6
View File
@@ -14,6 +14,9 @@
//! subxt transactions for chains supporting sr25519 signatures.
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
#[macro_use]
mod utils;
@@ -40,9 +43,3 @@ pub use secrecy::{ExposeSecret, SecretString};
// SecretUri's can be parsed from strings and used to generate key pairs.
// DeriveJunctions are the "path" part of these SecretUris.
pub use crypto::{DeriveJunction, SecretUri, SecretUriError, DEV_PHRASE};
#[cfg(any(
all(feature = "web", feature = "native"),
not(any(feature = "web", feature = "native"))
))]
compile_error!("subxt-signer: exactly one of the 'web' and 'native' features should be used.");
+17 -13
View File
@@ -4,7 +4,11 @@
//! An sr25519 keypair implementation.
use core::str::FromStr;
use crate::crypto::{seed_from_entropy, DeriveJunction, SecretUri};
use derive_more::{Display, From};
use hex::FromHex;
use schnorrkel::{
derive::{ChainCode, Derivation},
@@ -72,7 +76,7 @@ impl Keypair {
let seed = Seed::from_hex(hex_str)?;
Self::from_seed(seed)?
} else {
let phrase = bip39::Mnemonic::parse(phrase.expose_secret().as_str())?;
let phrase = bip39::Mnemonic::from_str(phrase.expose_secret().as_str())?;
let pass_str = password.as_ref().map(|p| p.expose_secret().as_str());
Self::from_phrase(&phrase, pass_str)?
};
@@ -95,8 +99,9 @@ impl Keypair {
/// 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 (arr, len) = mnemonic.to_entropy_array();
let big_seed =
seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?;
let seed: Seed = big_seed[..SEED_LENGTH]
.try_into()
@@ -187,24 +192,25 @@ pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -
}
/// An error handed back if creating a keypair fails.
#[derive(Debug, thiserror::Error)]
#[derive(Debug, Display, From)]
pub enum Error {
/// Invalid seed.
#[error("Invalid seed (was it the wrong length?)")]
#[display(fmt = "Invalid seed (was it the wrong length?)")]
#[from(ignore)]
InvalidSeed,
/// Invalid phrase.
#[error("Cannot parse phrase: {0}")]
Phrase(#[from] bip39::Error),
#[display(fmt = "Cannot parse phrase: {_0}")]
Phrase(bip39::Error),
/// Invalid hex.
#[error("Cannot parse hex string: {0}")]
Hex(#[from] hex::FromHexError),
#[display(fmt = "Cannot parse hex string: {_0}")]
Hex(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;
use core::str::FromStr;
once_static_cloned! {
/// Equivalent to `{DEV_PHRASE}//Alice`.
@@ -249,9 +255,7 @@ pub mod dev {
mod subxt_compat {
use super::*;
use subxt::config::Config;
use subxt::tx::Signer as SignerT;
use subxt::utils::{AccountId32, MultiAddress, MultiSignature};
use subxt_core::{AccountId32, Config, MultiAddress, MultiSignature, Signer as SignerT};
impl From<Signature> for MultiSignature {
fn from(value: Signature) -> Self {
+2 -2
View File
@@ -19,7 +19,7 @@ macro_rules! once_static {
$(
$(#[$attr])*
$vis fn $name() -> &'static $ty {
static VAR: std::sync::OnceLock<$ty> = std::sync::OnceLock::new();
static VAR: once_cell::sync::OnceCell<$ty> = once_cell::sync::OnceCell::new();
VAR.get_or_init(|| { $expr })
}
)+
@@ -33,7 +33,7 @@ macro_rules! once_static_cloned {
$(
$(#[$attr])*
$vis fn $name() -> $ty {
static VAR: std::sync::OnceLock<$ty> = std::sync::OnceLock::new();
static VAR: once_cell::sync::OnceCell<$ty> = once_cell::sync::OnceCell::new();
VAR.get_or_init(|| { $expr }).clone()
}
)+