diff --git a/.travis.yml b/.travis.yml index 19c6b18ef..53e552f6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,7 @@ script: cargo fmt -- --check; fi - cargo check --workspace --tests --benches - - cargo test --all --exclude uint --exclude fixed-hash --exclude parity-crypto + - cargo test --workspace --exclude uint --exclude fixed-hash --exclude parity-crypto - if [ "$TRAVIS_RUST_VERSION" == "nightly" ]; then cd contract-address/ && cargo test --features=external_doc && cd ..; fi diff --git a/keccak-hash/CHANGELOG.md b/keccak-hash/CHANGELOG.md index 13f73c4a9..e8580f5d2 100644 --- a/keccak-hash/CHANGELOG.md +++ b/keccak-hash/CHANGELOG.md @@ -6,9 +6,13 @@ The format is based on [Keep a Changelog]. ## [Unreleased] -## [0.4.2] - 2020-03-16 +## [0.5.1] - 2020-04-10 +- Added `keccak256_range` and `keccak512_range` functions. [#370](https://github.com/paritytech/parity-common/pull/370) + +## [0.5.0] - 2020-03-16 - License changed from GPL3 to dual MIT/Apache2. [#342](https://github.com/paritytech/parity-common/pull/342) - Updated dependencies. [#361](https://github.com/paritytech/parity-common/pull/361) +- Updated tiny-keccak. [#260](https://github.com/paritytech/parity-common/pull/260) ## [0.4.1] - 2019-10-24 ### Dependencies diff --git a/keccak-hash/Cargo.toml b/keccak-hash/Cargo.toml index ea36f7288..c663c66c5 100644 --- a/keccak-hash/Cargo.toml +++ b/keccak-hash/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "keccak-hash" -version = "0.5.0" +version = "0.5.1" description = "`keccak-hash` is a set of utility functions to facilitate working with Keccak hashes (256/512 bits long)." authors = ["Parity Technologies "] repository = "https://github.com/paritytech/parity-common" @@ -19,3 +19,7 @@ criterion = "0.3.0" [features] default = ["std"] std = [] + +[[bench]] +name = "keccak_256" +harness = false diff --git a/keccak-hash/benches/keccak_256.rs b/keccak-hash/benches/keccak_256.rs index 97e9ee13d..3a28f993a 100644 --- a/keccak-hash/benches/keccak_256.rs +++ b/keccak-hash/benches/keccak_256.rs @@ -22,12 +22,26 @@ pub fn keccak_256_with_empty_input(c: &mut Criterion) { } pub fn keccak_256_with_typical_input(c: &mut Criterion) { - let data: Vec = From::from("some medium length string with important information"); - c.bench_function("keccak_256_with_typical_input", |b| { + let mut data: Vec = From::from("some medium length string with important information"); + let len = data.len(); + let mut group = c.benchmark_group("keccak_256_with_typical_input"); + group.bench_function("regular", |b| { b.iter(|| { let _out = keccak(black_box(&data)); }) }); + group.bench_function("inplace", |b| { + b.iter(|| { + keccak_hash::keccak256(black_box(&mut data[..])); + }) + }); + group.bench_function("inplace_range", |b| { + b.iter(|| { + keccak_hash::keccak256_range(black_box(&mut data[..]), 0..len); + }) + }); + + group.finish(); } pub fn keccak_256_with_large_input(c: &mut Criterion) { diff --git a/keccak-hash/src/lib.rs b/keccak-hash/src/lib.rs index e9f410672..dbad92af5 100644 --- a/keccak-hash/src/lib.rs +++ b/keccak-hash/src/lib.rs @@ -45,6 +45,32 @@ pub fn keccak256(data: &mut [u8]) { keccak256.finalize(data); } +/// Computes in-place keccak256 hash of `data[range]`. +/// +/// The `range` argument specifies a subslice of `data` in bytes to be hashed. +/// The resulting hash will be written back to `data`. +/// # Panics +/// +/// If `range` is out of bounds. +/// +/// # Example +/// +/// ``` +/// let mut data = [1u8; 32]; +/// // Hash the first 8 bytes of `data` and write the result, 32 bytes, to `data`. +/// keccak_hash::keccak256_range(&mut data, 0..8); +/// let expected = [ +/// 0x54, 0x84, 0x4f, 0x69, 0xb4, 0xda, 0x4b, 0xb4, 0xa9, 0x9f, 0x24, 0x59, 0xb5, 0x11, 0xd4, 0x42, +/// 0xcc, 0x5b, 0xd2, 0xfd, 0xf4, 0xc3, 0x54, 0xd2, 0x07, 0xbb, 0x13, 0x08, 0x94, 0x43, 0xaf, 0x68, +/// ]; +/// assert_eq!(&data, &expected); +/// ``` +pub fn keccak256_range(data: &mut [u8], range: core::ops::Range) { + let mut keccak256 = Keccak::v256(); + keccak256.update(&data[range]); + keccak256.finalize(data); +} + /// Computes in-place keccak512 hash of `data`. pub fn keccak512(data: &mut [u8]) { let mut keccak512 = Keccak::v512(); @@ -52,6 +78,31 @@ pub fn keccak512(data: &mut [u8]) { keccak512.finalize(data); } +/// Computes in-place keccak512 hash of `data[range]`. +/// +/// The `range` argument specifies a subslice of `data` in bytes to be hashed. +/// The resulting hash will be written back to `data`. +/// # Panics +/// +/// If `range` is out of bounds. +/// +/// # Example +/// +/// ``` +/// let mut data = [1u8; 64]; +/// keccak_hash::keccak512_range(&mut data, 0..8); +/// let expected = [ +/// 0x90, 0x45, 0xc5, 0x9e, 0xd3, 0x0e, 0x1f, 0x42, 0xac, 0x35, 0xcc, 0xc9, 0x55, 0x7c, 0x77, 0x17, +/// 0xc8, 0x89, 0x3a, 0x77, 0x6c, 0xea, 0x2e, 0xf3, 0x88, 0xea, 0xe5, 0xc0, 0xea, 0x40, 0x26, 0x64, +/// ]; +/// assert_eq!(&data[..32], &expected); +/// ``` +pub fn keccak512_range(data: &mut [u8], range: core::ops::Range) { + let mut keccak512 = Keccak::v512(); + keccak512.update(&data[range]); + keccak512.finalize(data); +} + pub fn keccak_256(input: &[u8], output: &mut [u8]) { write_keccak(input, output); } diff --git a/parity-crypto/CHANGELOG.md b/parity-crypto/CHANGELOG.md index 14290d54d..08cd46532 100644 --- a/parity-crypto/CHANGELOG.md +++ b/parity-crypto/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog]. ## [Unreleased] +## [0.6.1] - 2020-04-11 +- Add `recover_allowing_all_zero_message()` and `ZeroesAllowedMessage` to accomodate ethereum's `ecrecover` builtin. [#369](https://github.com/paritytech/parity-common/pull/369) + ## [0.6.0] - 2020-03-16 - License changed from GPL3 to dual MIT/Apache2. [#342](https://github.com/paritytech/parity-common/pull/342) - Updated dependencies. [#361](https://github.com/paritytech/parity-common/pull/361) diff --git a/parity-crypto/Cargo.toml b/parity-crypto/Cargo.toml index 763fc4b74..1b1944ad3 100644 --- a/parity-crypto/Cargo.toml +++ b/parity-crypto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "parity-crypto" -version = "0.6.0" +version = "0.6.1" authors = ["Parity Technologies "] repository = "https://github.com/paritytech/parity-common" description = "Crypto utils used by ethstore and network." diff --git a/parity-crypto/src/publickey/ecdsa_signature.rs b/parity-crypto/src/publickey/ecdsa_signature.rs index 6853b1586..7968967a7 100644 --- a/parity-crypto/src/publickey/ecdsa_signature.rs +++ b/parity-crypto/src/publickey/ecdsa_signature.rs @@ -8,7 +8,7 @@ //! Signature based on ECDSA, algorithm's description: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm -use super::{public_to_address, Address, Error, Message, Public, Secret, SECP256K1}; +use super::{public_to_address, Address, Error, Message, Public, Secret, ZeroesAllowedMessage, SECP256K1}; use ethereum_types::{H256, H520}; use rustc_hex::{FromHex, ToHex}; use secp256k1::key::{PublicKey, SecretKey}; @@ -246,22 +246,56 @@ pub fn verify_address(address: &Address, signature: &Signature, message: &Messag /// Recovers the public key from the signature for the message pub fn recover(signature: &Signature, message: &Message) -> Result { - let context = &SECP256K1; let rsig = RecoverableSignature::from_compact(&signature[0..64], RecoveryId::from_i32(signature[64] as i32)?)?; - let pubkey = context.recover(&SecpMessage::from_slice(&message[..])?, &rsig)?; + let pubkey = &SECP256K1.recover(&SecpMessage::from_slice(&message[..])?, &rsig)?; let serialized = pubkey.serialize_uncompressed(); - let mut public = Public::default(); public.as_bytes_mut().copy_from_slice(&serialized[1..65]); Ok(public) } +/// Recovers the public key from the signature for the given message. +/// This version of `recover()` allows for all-zero messages, which is necessary +/// for ethereum but is otherwise highly discouraged. Use with caution. +pub fn recover_allowing_all_zero_message( + signature: &Signature, + message: ZeroesAllowedMessage, +) -> Result { + let rsig = RecoverableSignature::from_compact(&signature[0..64], RecoveryId::from_i32(signature[64] as i32)?)?; + let pubkey = &SECP256K1.recover(&message.into(), &rsig)?; + let serialized = pubkey.serialize_uncompressed(); + let mut public = Public::zero(); + public.as_bytes_mut().copy_from_slice(&serialized[1..65]); + Ok(public) +} + #[cfg(test)] mod tests { - use super::super::{Generator, Message, Random}; - use super::{recover, sign, verify_address, verify_public, Signature}; + use super::super::{Generator, Message, Random, SECP256K1}; + use super::{ + recover, recover_allowing_all_zero_message, sign, verify_address, verify_public, Secret, Signature, + ZeroesAllowedMessage, + }; + use secp256k1::SecretKey; use std::str::FromStr; + // Copy of `sign()` that allows signing all-zero Messages. + // Note: this is for *tests* only. DO NOT USE UNLESS YOU NEED IT. + fn sign_zero_message(secret: &Secret) -> Signature { + let context = &SECP256K1; + let sec = SecretKey::from_slice(secret.as_ref()).unwrap(); + // force an all-zero message into a secp `Message` bypassing the validity check. + let zero_msg = ZeroesAllowedMessage(Message::zero()); + let s = context.sign_recoverable(&zero_msg.into(), &sec); + let (rec_id, data) = s.serialize_compact(); + let mut data_arr = [0; 65]; + + // no need to check if s is low, it always is + data_arr[0..64].copy_from_slice(&data[0..64]); + data_arr[64] = rec_id.to_i32() as u8; + Signature(data_arr) + } + #[test] fn vrs_conversion() { // given @@ -295,6 +329,22 @@ mod tests { assert_eq!(keypair.public(), &recover(&signature, &message).unwrap()); } + #[test] + fn sign_and_recover_public_fails_with_zeroed_messages() { + let keypair = Random.generate(); + let signature = sign_zero_message(keypair.secret()); + let zero_message = Message::zero(); + assert!(&recover(&signature, &zero_message).is_err()); + } + + #[test] + fn recover_allowing_all_zero_message_can_recover_from_all_zero_messages() { + let keypair = Random.generate(); + let signature = sign_zero_message(keypair.secret()); + let zero_message = ZeroesAllowedMessage(Message::zero()); + assert_eq!(keypair.public(), &recover_allowing_all_zero_message(&signature, zero_message).unwrap()) + } + #[test] fn sign_and_verify_public() { let keypair = Random.generate(); diff --git a/parity-crypto/src/publickey/mod.rs b/parity-crypto/src/publickey/mod.rs index 294d67b38..54d3ffe79 100644 --- a/parity-crypto/src/publickey/mod.rs +++ b/parity-crypto/src/publickey/mod.rs @@ -20,7 +20,9 @@ pub mod ecdh; pub mod ecies; pub mod error; -pub use self::ecdsa_signature::{recover, sign, verify_address, verify_public, Signature}; +pub use self::ecdsa_signature::{ + recover, recover_allowing_all_zero_message, sign, verify_address, verify_public, Signature, +}; pub use self::error::Error; pub use self::extended_keys::{Derivation, DerivationError, ExtendedKeyPair, ExtendedPublic, ExtendedSecret}; pub use self::keypair::{public_to_address, KeyPair}; @@ -33,6 +35,22 @@ use lazy_static::lazy_static; pub use ethereum_types::{Address, Public}; pub type Message = H256; +use secp256k1::ThirtyTwoByteHash; + +/// In ethereum we allow public key recovery from a signature + message pair +/// where the message is all-zeroes. This conflicts with the best practise of +/// not allowing such values and so in order to avoid breaking consensus we need +/// this to work around it. The `ZeroesAllowedType` wraps an `H256` that can be +/// converted to a `[u8; 32]` which in turn can be cast to a +/// `secp256k1::Message` by the `ThirtyTwoByteHash` and satisfy the API for +/// `recover()`. +pub struct ZeroesAllowedMessage(pub H256); +impl ThirtyTwoByteHash for ZeroesAllowedMessage { + fn into_32(self) -> [u8; 32] { + self.0.to_fixed_bytes() + } +} + /// The number -1 encoded as a secret key const MINUS_ONE_KEY: &'static [u8] = &[ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xba, 0xae, 0xdc,