feat: convert signing to k256 (#72)

* feat: convert signing to k256

* fix: pass pre-hashed message to sig verification

* feat: wrap the hash to a Digest implementation

* refactor: cleanup and move digest impl to separate file

* chore: adjust abigen tests due to rust update

* test: add byte equality test between ethers-rs / web3.js signatures

* fix(keys): use 512 blocks for sha256

Co-authored-by: Rohit Narurkar <rohit.narurkar@protonmail.com>
Co-authored-by: Alex Vlasov <alex.m.vlasov@gmail.com>
This commit is contained in:
Georgios Konstantopoulos 2020-10-01 11:02:21 +03:00 committed by GitHub
parent 2d51c523ba
commit c65497543e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 839 additions and 379 deletions

862
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -115,7 +115,7 @@ mod tests {
// no inputs
let params = vec![];
let token_stream = expand_inputs_call_arg(&params);
assert_eq!(token_stream.to_string(), "( )");
assert_eq!(token_stream.to_string(), "()");
// single input
let params = vec![Param {
@ -137,7 +137,7 @@ mod tests {
},
];
let token_stream = expand_inputs_call_arg(&params);
assert_eq!(token_stream.to_string(), "( arg_a , arg_b , )");
assert_eq!(token_stream.to_string(), "(arg_a , arg_b ,)");
// three inputs
let params = vec![
@ -155,7 +155,7 @@ mod tests {
},
];
let token_stream = expand_inputs_call_arg(&params);
assert_eq!(token_stream.to_string(), "( arg_a , arg_b , arg_c , )");
assert_eq!(token_stream.to_string(), "(arg_a , arg_b , arg_c ,)");
}
#[test]

View File

@ -17,10 +17,13 @@ ethabi = { package = "ethabi-next", version = "12.0.0", default-features = false
arrayvec = { version = "0.5.1", default-features = false }
# crypto
secp256k1 = { package = "libsecp256k1", version = "0.3.5" }
ecdsa = { version = "0.8.0", features = ["std"] }
elliptic-curve = { version = "0.6.1", features = ["arithmetic"] }
generic-array = "0.14.4"
k256 = { git = "https://github.com/RustCrypto/elliptic-curves", version = "0.5.2", features = ["keccak256", "ecdsa"] }
rand = "0.7.2"
tiny-keccak = { version = "2.0.2", default-features = false }
sha2 = { version = "0.9.1" }
# misc
serde = { version = "1.0.110", default-features = false, features = ["derive"] }

View File

@ -49,5 +49,5 @@ pub mod utils;
// re-export rand to avoid potential confusion when there's rand version mismatches
pub use rand;
// re-export libsecp
pub use secp256k1;
// re-export k256
pub use k256;

View File

@ -0,0 +1,87 @@
//! This is a helper module used to pass the pre-hashed message for signing to the
//! `sign_digest` methods of K256.
use crate::types::H256;
use elliptic_curve::consts::U64;
use k256::ecdsa::signature::digest::{
generic_array::GenericArray, BlockInput, Digest, FixedOutput, Output, Reset, Update,
};
pub type Sha256Proxy = ProxyDigest<sha2::Sha256>;
#[derive(Clone)]
pub enum ProxyDigest<D: Digest> {
Proxy(Output<D>),
Digest(D),
}
impl<D: Digest + Clone> From<H256> for ProxyDigest<D>
where
GenericArray<u8, <D as Digest>::OutputSize>: Copy,
{
fn from(src: H256) -> Self {
ProxyDigest::Proxy(*GenericArray::from_slice(src.as_bytes()))
}
}
impl<D: Digest> Default for ProxyDigest<D> {
fn default() -> Self {
ProxyDigest::Digest(D::new())
}
}
impl<D: Digest> Update for ProxyDigest<D> {
// we update only if we are digest
fn update(&mut self, data: impl AsRef<[u8]>) {
match self {
ProxyDigest::Digest(ref mut d) => {
d.update(data);
}
ProxyDigest::Proxy(..) => {
unreachable!("can not update if we are proxy");
}
}
}
// we chain only if we are digest
fn chain(self, data: impl AsRef<[u8]>) -> Self {
match self {
ProxyDigest::Digest(d) => ProxyDigest::Digest(d.chain(data)),
ProxyDigest::Proxy(..) => {
unreachable!("can not update if we are proxy");
}
}
}
}
impl<D: Digest> Reset for ProxyDigest<D> {
// make new one
fn reset(&mut self) {
*self = Self::default();
}
}
// Use Sha256 with 512 bit blocks
impl<D: Digest> BlockInput for ProxyDigest<D> {
type BlockSize = U64;
}
impl<D: Digest> FixedOutput for ProxyDigest<D> {
// we default to the output of the original digest
type OutputSize = D::OutputSize;
fn finalize_into(self, out: &mut GenericArray<u8, Self::OutputSize>) {
match self {
ProxyDigest::Digest(d) => {
*out = d.finalize();
}
ProxyDigest::Proxy(p) => {
*out = p;
}
}
}
fn finalize_into_reset(&mut self, out: &mut GenericArray<u8, Self::OutputSize>) {
let s = std::mem::take(self);
s.finalize_into(out);
}
}

View File

@ -1,15 +1,11 @@
use super::hash::Sha256Proxy;
use crate::{
types::{Address, Signature, TransactionRequest, H256},
utils::{hash_message, keccak256},
};
use rand::Rng;
use rand::{CryptoRng, Rng};
use rustc_hex::FromHex;
use secp256k1::{
self as Secp256k1,
util::{COMPRESSED_PUBLIC_KEY_SIZE, SECRET_KEY_SIZE},
Error as SecpError, Message, PublicKey as PubKey, RecoveryId, SecretKey,
};
use serde::{
de::Error as DeserializeError,
de::{SeqAccess, Visitor},
@ -18,9 +14,28 @@ use serde::{
};
use std::{fmt, ops::Deref, str::FromStr};
use k256::{
ecdsa::{
recoverable::{Id as RecoveryId, Signature as RecoverableSignature},
signature::DigestSigner,
SigningKey,
},
elliptic_curve::{error::Error as EllipticCurveError, FieldBytes},
EncodedPoint as K256PublicKey, Secp256k1, SecretKey as K256SecretKey,
};
const SECRET_KEY_SIZE: usize = 32;
const COMPRESSED_PUBLIC_KEY_SIZE: usize = 33;
/// A private key on Secp256k1
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrivateKey(pub(super) SecretKey);
#[derive(Clone, Debug)]
pub struct PrivateKey(pub(super) K256SecretKey);
impl PartialEq for PrivateKey {
fn eq(&self, other: &Self) -> bool {
self.0.to_bytes().eq(&other.0.to_bytes())
}
}
impl Serialize for PrivateKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@ -28,8 +43,8 @@ impl Serialize for PrivateKey {
S: Serializer,
{
let mut seq = serializer.serialize_tuple(SECRET_KEY_SIZE)?;
for e in &self.0.serialize() {
seq.serialize_element(e)?;
for e in self.0.to_bytes() {
seq.serialize_element(&e)?;
}
seq.end()
}
@ -42,26 +57,26 @@ impl<'de> Deserialize<'de> for PrivateKey {
{
let bytes = <[u8; SECRET_KEY_SIZE]>::deserialize(deserializer)?;
Ok(PrivateKey(
SecretKey::parse(&bytes).map_err(DeserializeError::custom)?,
K256SecretKey::from_bytes(&bytes).map_err(DeserializeError::custom)?,
))
}
}
impl FromStr for PrivateKey {
type Err = SecpError;
type Err = EllipticCurveError;
fn from_str(src: &str) -> Result<PrivateKey, Self::Err> {
let src = src
.from_hex::<Vec<u8>>()
.expect("invalid hex when reading PrivateKey");
let sk = SecretKey::parse_slice(&src)?;
let sk = K256SecretKey::from_bytes(&src)?;
Ok(PrivateKey(sk))
}
}
impl PrivateKey {
pub fn new<R: Rng>(rng: &mut R) -> Self {
PrivateKey(SecretKey::random(rng))
pub fn new<R: Rng + CryptoRng>(rng: &mut R) -> Self {
PrivateKey(K256SecretKey::random(rng))
}
/// Sign arbitrary string data.
@ -78,9 +93,7 @@ impl PrivateKey {
let message = message.as_ref();
let message_hash = hash_message(message);
let sig_message =
Message::parse_slice(message_hash.as_bytes()).expect("hash is non-zero 32-bytes; qed");
self.sign_with_eip155(&sig_message, None)
self.sign_hash_with_eip155(message_hash, None)
}
/// RLP encodes and then signs the stransaction.
@ -95,39 +108,41 @@ impl PrivateKey {
/// If `tx.to` is an ENS name. The caller MUST take care of name resolution before
/// calling this function.
pub fn sign_transaction(&self, tx: &TransactionRequest, chain_id: Option<u64>) -> Signature {
// Get the transaction's sighash
let sighash = tx.sighash(chain_id);
let message =
Message::parse_slice(sighash.as_bytes()).expect("hash is non-zero 32-bytes; qed");
// Sign it (with replay protection if applicable)
self.sign_with_eip155(&message, chain_id)
self.sign_hash_with_eip155(sighash, chain_id)
}
fn sign_with_eip155(&self, message: &Message, chain_id: Option<u64>) -> Signature {
let (signature, recovery_id) = Secp256k1::sign(message, &self.0);
fn sign_hash_with_eip155(&self, hash: H256, chain_id: Option<u64>) -> Signature {
let signing_key = SigningKey::new(&self.0.to_bytes()).expect("invalid secret key");
let v = to_eip155_v(recovery_id, chain_id);
let r = H256::from_slice(&signature.r.b32());
let s = H256::from_slice(&signature.s.b32());
let recoverable_sig: RecoverableSignature =
signing_key.sign_digest(Sha256Proxy::from(hash));
Signature { v, r, s }
let v = to_eip155_v(recoverable_sig.recovery_id(), chain_id);
let r_bytes: FieldBytes<Secp256k1> = recoverable_sig.r().into();
let s_bytes: FieldBytes<Secp256k1> = recoverable_sig.s().into();
let r = H256::from_slice(&r_bytes.as_slice());
let s = H256::from_slice(&s_bytes.as_slice());
Signature { r, s, v }
}
}
/// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md)
fn to_eip155_v(recovery_id: RecoveryId, chain_id: Option<u64>) -> u64 {
let standard_v = recovery_id.serialize() as u64;
let standard_v: u8 = recovery_id.into();
if let Some(chain_id) = chain_id {
// When signing with a chain ID, add chain replay protection.
standard_v + 35 + chain_id * 2
(standard_v as u64) + 35 + chain_id * 2
} else {
// Otherwise, convert to 'Electrum' notation.
standard_v + 27
(standard_v as u64) + 27
}
}
impl Deref for PrivateKey {
type Target = SecretKey;
type Target = K256SecretKey;
fn deref(&self) -> &Self::Target {
&self.0
@ -136,11 +151,11 @@ impl Deref for PrivateKey {
/// A secp256k1 Public Key
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PublicKey(pub(super) PubKey);
pub struct PublicKey(pub(super) K256PublicKey);
impl From<PubKey> for PublicKey {
impl From<K256PublicKey> for PublicKey {
/// Gets the public address of a private key.
fn from(src: PubKey) -> PublicKey {
fn from(src: K256PublicKey) -> PublicKey {
PublicKey(src)
}
}
@ -148,7 +163,7 @@ impl From<PubKey> for PublicKey {
impl From<&PrivateKey> for PublicKey {
/// Gets the public address of a private key.
fn from(src: &PrivateKey) -> PublicKey {
let public_key = PubKey::from_secret_key(src);
let public_key = K256PublicKey::from_secret_key(src, false);
PublicKey(public_key)
}
}
@ -162,7 +177,7 @@ impl From<&PrivateKey> for PublicKey {
/// computing the hash.
impl From<&PublicKey> for Address {
fn from(src: &PublicKey) -> Address {
let public_key = src.0.serialize();
let public_key = src.0.as_bytes();
debug_assert_eq!(public_key[0], 0x04);
let hash = keccak256(&public_key[1..]);
@ -196,7 +211,8 @@ impl Serialize for PublicKey {
S: Serializer,
{
let mut seq = serializer.serialize_tuple(COMPRESSED_PUBLIC_KEY_SIZE)?;
for e in self.0.serialize_compressed().iter() {
for e in self.0.compress().as_bytes().iter() {
seq.serialize_element(e)?;
}
seq.end()
@ -228,9 +244,15 @@ impl<'de> Deserialize<'de> for PublicKey {
.ok_or_else(|| DeserializeError::custom("could not read bytes"))?;
}
Ok(PublicKey(
PubKey::parse_compressed(&bytes).map_err(DeserializeError::custom)?,
))
let pub_key = K256PublicKey::from_bytes(&bytes[..])
.map_err(|_| DeserializeError::custom("parse pub key"))?;
let uncompressed_pub_key = pub_key.decompress();
if uncompressed_pub_key.is_some().into() {
Ok(PublicKey(uncompressed_pub_key.unwrap()))
} else {
Err(DeserializeError::custom("parse pub key"))
}
}
}
@ -241,40 +263,86 @@ impl<'de> Deserialize<'de> for PublicKey {
#[cfg(test)]
mod tests {
use super::*;
use rustc_hex::FromHex;
#[test]
fn serde() {
for _ in 0..10 {
let key = PrivateKey::new(&mut rand::thread_rng());
let serialized = bincode::serialize(&key).unwrap();
assert_eq!(serialized, &key.0.serialize());
assert_eq!(serialized.as_slice(), key.0.to_bytes().as_slice());
let de: PrivateKey = bincode::deserialize(&serialized).unwrap();
assert_eq!(key, de);
let public = PublicKey::from(&key);
println!("public = {:?}", public);
let serialized = bincode::serialize(&public).unwrap();
assert_eq!(&serialized[..], public.0.serialize_compressed().as_ref());
let de: PublicKey = bincode::deserialize(&serialized).unwrap();
assert_eq!(public, de);
}
}
#[test]
fn signs_data() {
// test vector taken from:
// https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign
#[cfg(not(feature = "celo"))]
fn signs_tx() {
use crate::types::Address;
// retrieved test vector from:
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
let tx = TransactionRequest {
from: None,
to: Some(
"F0109fC8DF283027b6285cc889F5aA624EaC1F55"
.parse::<Address>()
.unwrap()
.into(),
),
value: Some(1_000_000_000.into()),
gas: Some(2_000_000.into()),
nonce: Some(0.into()),
gas_price: Some(21_000_000_000u128.into()),
data: None,
};
let chain_id = 1;
let key: PrivateKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
.parse()
.unwrap();
let sign = key.sign("Some data");
let sig = key.sign_transaction(&tx, Some(chain_id));
let sighash = tx.sighash(Some(chain_id));
assert!(sig.verify(sighash, Address::from(key)).is_ok());
}
#[test]
fn key_to_address() {
let priv_key: PrivateKey =
"0000000000000000000000000000000000000000000000000000000000000001"
.parse()
.unwrap();
let addr: Address = priv_key.into();
assert_eq!(
sign.to_vec(),
"b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c"
.from_hex::<Vec<u8>>()
.unwrap()
addr,
Address::from_str("7E5F4552091A69125d5DfCb7b8C2659029395Bdf").expect("Decoding failed")
);
let priv_key: PrivateKey =
"0000000000000000000000000000000000000000000000000000000000000002"
.parse()
.unwrap();
let addr: Address = priv_key.into();
assert_eq!(
addr,
Address::from_str("2B5AD5c4795c026514f8317c7a215E218DcCD6cF").expect("Decoding failed")
);
let priv_key: PrivateKey =
"0000000000000000000000000000000000000000000000000000000000000003"
.parse()
.unwrap();
let addr: Address = priv_key.into();
assert_eq!(
addr,
Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed")
);
}
}

View File

@ -2,4 +2,6 @@ mod keys;
pub use keys::{PrivateKey, PublicKey};
mod signature;
pub use signature::{Signature, SignatureError};
pub use signature::Signature;
mod hash;

View File

@ -5,21 +5,22 @@ use crate::{
};
use rustc_hex::{FromHex, ToHex};
use secp256k1::{
self as Secp256k1, Error as Secp256k1Error, Message, RecoveryId,
Signature as RecoverableSignature,
};
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fmt, str::FromStr};
use thiserror::Error;
use elliptic_curve::consts::U32;
use generic_array::GenericArray;
use k256::ecdsa::{
recoverable::{Id as RecoveryId, Signature as RecoverableSignature},
Error as K256SignatureError, Signature as K256Signature,
};
use k256::EncodedPoint as K256PublicKey;
/// An error involving a signature.
#[derive(Clone, Debug, Error)]
#[derive(Debug, Error)]
pub enum SignatureError {
/// Internal error inside the recovery
#[error(transparent)]
Secp256k1Error(#[from] Secp256k1Error),
/// Invalid length, secp256k1 signatures are 65 bytes
#[error("invalid signature length, got {0}, expected 65")]
InvalidLength(usize),
@ -30,6 +31,12 @@ pub enum SignatureError {
/// produced the signature did not match the expected address)
#[error("Signature verification failed. Expected {0}, got {0}")]
VerificationError(Address, Address),
/// Internal error during signature recovery
#[error(transparent)]
K256Error(#[from] K256SignatureError),
/// Error in recovering public key from signature
#[error("Public key recovery error")]
RecoveryError,
}
/// Recovery message data.
@ -92,22 +99,27 @@ impl Signature {
RecoveryMessage::Data(ref message) => hash_message(message),
RecoveryMessage::Hash(hash) => hash,
};
let message = Message::parse_slice(message_hash.as_bytes())?;
let (signature, recovery_id) = self.as_signature()?;
let public_key = Secp256k1::recover(&message, &signature, &recovery_id)?;
let (recoverable_sig, _recovery_id) = self.as_signature()?;
let verify_key =
recoverable_sig.recover_verify_key_from_digest_bytes(message_hash.as_ref().into())?;
Ok(PublicKey::from(public_key).into())
let uncompressed_pub_key = K256PublicKey::from(&verify_key).decompress();
if uncompressed_pub_key.is_some().into() {
Ok(PublicKey::from(uncompressed_pub_key.unwrap()).into())
} else {
Err(SignatureError::RecoveryError)
}
}
/// Retrieves the recovery signature.
fn as_signature(&self) -> Result<(RecoverableSignature, RecoveryId), SignatureError> {
let recovery_id = self.recovery_id()?;
let signature = {
let mut sig = [0u8; 64];
sig[..32].copy_from_slice(self.r.as_bytes());
sig[32..].copy_from_slice(self.s.as_bytes());
RecoverableSignature::parse(&sig)
let gar: &GenericArray<u8, U32> = GenericArray::from_slice(self.r.as_bytes());
let gas: &GenericArray<u8, U32> = GenericArray::from_slice(self.s.as_bytes());
let sig = K256Signature::from_scalars(*gar, *gas)?;
RecoverableSignature::new(&sig, recovery_id)?
};
Ok((signature, recovery_id))
@ -116,7 +128,7 @@ impl Signature {
/// Retrieve the recovery ID.
fn recovery_id(&self) -> Result<RecoveryId, SignatureError> {
let standard_v = normalize_recovery_id(self.v);
Ok(RecoveryId::parse(standard_v)?)
Ok(RecoveryId::new(standard_v)?)
}
/// Copies and serializes `self` into a new `Vec` with the recovery id included
@ -258,6 +270,23 @@ mod tests {
assert_eq!(recovered2, address);
}
#[test]
fn recover_web3_signature() {
// test vector taken from:
// https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign
let key: PrivateKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
.parse()
.unwrap();
let address = Address::from(&key);
let our_signature = key.sign("Some data");
let signature = Signature::from_str(
"b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c"
).expect("could not parse signature");
assert_eq!(our_signature.recover("Some data").unwrap(), address,);
assert_eq!(signature.recover("Some data").unwrap(), address);
assert_eq!(our_signature, signature);
}
#[test]
fn to_vec() {
let message = "Some data";

View File

@ -33,9 +33,6 @@ pub enum LedgerError {
#[error("Error when decoding UTF8 Response: {0}")]
Utf8Error(#[from] std::str::Utf8Error),
#[error(transparent)]
SignatureError(#[from] ethers_core::types::SignatureError),
}
pub const P1_FIRST: u8 = 0x00;

View File

@ -1,8 +1,8 @@
use crate::Signer;
use ethers_core::{
rand::Rng,
secp256k1,
k256::elliptic_curve::error::Error as K256Error,
rand::{CryptoRng, Rng},
types::{Address, PrivateKey, PublicKey, Signature, TransactionRequest},
};
@ -77,7 +77,7 @@ impl Wallet {
// TODO: Add support for mnemonic and encrypted JSON
/// Creates a new random keypair seeded with the provided RNG
pub fn new<R: Rng>(rng: &mut R) -> Self {
pub fn new<R: Rng + CryptoRng>(rng: &mut R) -> Self {
let private_key = PrivateKey::new(rng);
let public_key = PublicKey::from(&private_key);
let address = Address::from(&private_key);
@ -133,7 +133,7 @@ impl From<PrivateKey> for Wallet {
}
impl FromStr for Wallet {
type Err = secp256k1::Error;
type Err = K256Error;
fn from_str(src: &str) -> Result<Self, Self::Err> {
Ok(PrivateKey::from_str(src)?.into())