From a8617e28625659eade06d566a8c8f17e4817d5e3 Mon Sep 17 00:00:00 2001 From: Seun LanLege Date: Wed, 14 Nov 2018 09:02:40 +0100 Subject: [PATCH] EIP 191 (#9701) * added sign_191 rpc method * fixed hash_structured_data return type * added ConfirmationPayload::SignMessage for non-prefixed signatures, added tests for sign191 * renamed WithValidator -> PresignedTransaction * rename applicationData to data in test * adds docs for EIP191Version, renamed SignRequest to EIP191SignRequest --- rpc/src/v1/helpers/dispatch.rs | 21 ++++++- rpc/src/v1/helpers/eip191.rs | 61 +++++++++++++++++++ rpc/src/v1/helpers/mod.rs | 1 + rpc/src/v1/helpers/requests.rs | 4 ++ rpc/src/v1/impls/personal.rs | 28 ++++++++- rpc/src/v1/impls/signer.rs | 8 +++ rpc/src/v1/tests/mocked/personal.rs | 94 ++++++++++++++++++++++++++++- rpc/src/v1/traits/personal.rs | 9 ++- rpc/src/v1/types/confirmations.rs | 53 +++++++++++++--- rpc/src/v1/types/eip191.rs | 57 +++++++++++++++++ rpc/src/v1/types/mod.rs | 5 +- util/EIP-712/src/encode.rs | 4 +- 12 files changed, 324 insertions(+), 21 deletions(-) create mode 100644 rpc/src/v1/helpers/eip191.rs create mode 100644 rpc/src/v1/types/eip191.rs diff --git a/rpc/src/v1/helpers/dispatch.rs b/rpc/src/v1/helpers/dispatch.rs index 3b0d007a2..67f3c9fd4 100644 --- a/rpc/src/v1/helpers/dispatch.rs +++ b/rpc/src/v1/helpers/dispatch.rs @@ -49,7 +49,8 @@ use v1::types::{ RichRawTransaction as RpcRichRawTransaction, ConfirmationPayload as RpcConfirmationPayload, ConfirmationResponse, - SignRequest as RpcSignRequest, + EthSignRequest as RpcEthSignRequest, + EIP191SignRequest as RpcSignRequest, DecryptRequest as RpcDecryptRequest, }; use rlp; @@ -693,6 +694,19 @@ pub fn execute( ); Box::new(future::done(res)) }, + ConfirmationPayload::SignMessage(address, data) => { + if accounts.is_hardware_address(&address) { + return Box::new(future::err(errors::account("Error signing message with hardware_wallet", + "Message signing is unsupported"))); + } + let res = signature(&accounts, address, data, pass) + .map(|result| result + .map(|rsv| H520(rsv.into_electrum())) + .map(RpcH520::from) + .map(ConfirmationResponse::Signature) + ); + Box::new(future::done(res)) + }, ConfirmationPayload::Decrypt(address, data) => { if accounts.is_hardware_address(&address) { return Box::new(future::err(errors::unsupported("Decrypting via hardware wallets is not supported.", None))); @@ -775,8 +789,11 @@ pub fn from_rpc(payload: RpcConfirmationPayload, default_account: Address, di RpcConfirmationPayload::Decrypt(RpcDecryptRequest { address, msg }) => { Box::new(future::ok(ConfirmationPayload::Decrypt(address.into(), msg.into()))) }, - RpcConfirmationPayload::EthSignMessage(RpcSignRequest { address, data }) => { + RpcConfirmationPayload::EthSignMessage(RpcEthSignRequest { address, data }) => { Box::new(future::ok(ConfirmationPayload::EthSignMessage(address.into(), data.into()))) }, + RpcConfirmationPayload::EIP191SignMessage(RpcSignRequest { address, data }) => { + Box::new(future::ok(ConfirmationPayload::SignMessage(address.into(), data.into()))) + }, } } diff --git a/rpc/src/v1/helpers/eip191.rs b/rpc/src/v1/helpers/eip191.rs new file mode 100644 index 000000000..56ceba5f2 --- /dev/null +++ b/rpc/src/v1/helpers/eip191.rs @@ -0,0 +1,61 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! EIP-191 compliant decoding + hashing +use v1::types::{EIP191Version, Bytes, PresignedTransaction}; +use eip712::{hash_structured_data, EIP712}; +use serde_json::{Value, from_value}; +use v1::helpers::errors; +use jsonrpc_core::Error; +use v1::helpers::dispatch::eth_data_hash; +use hash::keccak; +use std::fmt::Display; +use ethereum_types::H256; + +/// deserializes and hashes the message depending on the version specifier +pub fn hash_message(version: EIP191Version, message: Value) -> Result { + let data = match version { + EIP191Version::StructuredData => { + let typed_data = from_value::(message) + .map_err(map_serde_err("StructuredData"))?; + + hash_structured_data(typed_data) + .map_err(|err| errors::invalid_call_data(err.kind()))? + } + + EIP191Version::PresignedTransaction => { + let data = from_value::(message) + .map_err(map_serde_err("WithValidator"))?; + let prefix = b"\x19\x00"; + let data = [&prefix[..], &data.validator.0[..], &data.data.0[..]].concat(); + keccak(data) + } + + EIP191Version::PersonalMessage => { + let bytes = from_value::(message) + .map_err(map_serde_err("Bytes"))?; + eth_data_hash(bytes.0) + } + }; + + Ok(data) +} + +fn map_serde_err(struct_name: &'static str) -> impl Fn(T) -> Error { + move |error: T| { + errors::invalid_call_data(format!("Error deserializing '{}': {}", struct_name, error)) + } +} diff --git a/rpc/src/v1/helpers/mod.rs b/rpc/src/v1/helpers/mod.rs index 5098eca89..0259818af 100644 --- a/rpc/src/v1/helpers/mod.rs +++ b/rpc/src/v1/helpers/mod.rs @@ -25,6 +25,7 @@ pub mod light_fetch; pub mod nonce; pub mod oneshot; pub mod secretstore; +pub mod eip191; mod network_settings; mod poll_filter; diff --git a/rpc/src/v1/helpers/requests.rs b/rpc/src/v1/helpers/requests.rs index 478f6785b..90fb42f50 100644 --- a/rpc/src/v1/helpers/requests.rs +++ b/rpc/src/v1/helpers/requests.rs @@ -18,6 +18,7 @@ use ethereum_types::{U256, Address}; use bytes::Bytes; use v1::types::{Origin, TransactionCondition}; +use ethereum_types::H256; /// Transaction request coming from RPC #[derive(Debug, Clone, Default, Eq, PartialEq, Hash)] @@ -117,6 +118,8 @@ pub enum ConfirmationPayload { SignTransaction(FilledTransactionRequest), /// Sign a message with an Ethereum specific security prefix. EthSignMessage(Address, Bytes), + /// Sign a message + SignMessage(Address, H256), /// Decrypt request Decrypt(Address, Bytes), } @@ -127,6 +130,7 @@ impl ConfirmationPayload { ConfirmationPayload::SendTransaction(ref request) => request.from, ConfirmationPayload::SignTransaction(ref request) => request.from, ConfirmationPayload::EthSignMessage(ref address, _) => *address, + ConfirmationPayload::SignMessage(ref address, _) => *address, ConfirmationPayload::Decrypt(ref address, _) => *address, } } diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index 7c5463071..4da0c1023 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -26,7 +26,7 @@ use ethkey::{public_to_address, recover, Signature}; use jsonrpc_core::{BoxFuture, Result}; use jsonrpc_core::futures::{future, Future}; -use v1::helpers::errors; +use v1::helpers::{errors, eip191}; use v1::helpers::dispatch::{self, eth_data_hash, Dispatcher, SignWith}; use v1::traits::Personal; use v1::types::{ @@ -36,9 +36,11 @@ use v1::types::{ ConfirmationResponse as RpcConfirmationResponse, TransactionRequest, RichRawTransaction as RpcRichRawTransaction, + EIP191Version, }; use v1::metadata::Metadata; use eip712::{EIP712, hash_structured_data}; +use jsonrpc_core::types::Value; /// Account management (personal) rpc implementation. pub struct PersonalClient { @@ -151,15 +153,35 @@ impl Personal for PersonalClient { })) } + fn sign_191(&self, version: EIP191Version, data: Value, account: RpcH160, password: String) -> BoxFuture { + let data = try_bf!(eip191::hash_message(version, data)); + let dispatcher = self.dispatcher.clone(); + let accounts = self.accounts.clone(); + + let payload = RpcConfirmationPayload::EIP191SignMessage((account.clone(), data.into()).into()); + + Box::new(dispatch::from_rpc(payload, account.into(), &dispatcher) + .and_then(|payload| { + dispatch::execute(dispatcher, accounts, payload, dispatch::SignWith::Password(password.into())) + }) + .map(|v| v.into_value()) + .then(|res| match res { + Ok(RpcConfirmationResponse::Signature(signature)) => Ok(signature), + Err(e) => Err(e), + e => Err(errors::internal("Unexpected result", e)), + }) + ) + } + fn sign_typed_data(&self, typed_data: EIP712, account: RpcH160, password: String) -> BoxFuture { let data = match hash_structured_data(typed_data) { Ok(d) => d, - Err(err) => return Box::new(future::done(Err(errors::invalid_call_data(err.kind())))), + Err(err) => return Box::new(future::err(errors::invalid_call_data(err.kind()))), }; let dispatcher = self.dispatcher.clone(); let accounts = self.accounts.clone(); - let payload = RpcConfirmationPayload::EthSignMessage((account.clone(), RpcBytes(data)).into()); + let payload = RpcConfirmationPayload::EIP191SignMessage((account.clone(), data.into()).into()); Box::new(dispatch::from_rpc(payload, account.into(), &dispatcher) .and_then(|payload| { diff --git a/rpc/src/v1/impls/signer.rs b/rpc/src/v1/impls/signer.rs index a6197433f..15cd21d20 100644 --- a/rpc/src/v1/impls/signer.rs +++ b/rpc/src/v1/impls/signer.rs @@ -215,6 +215,14 @@ impl Signer for SignerClient { Err(err) => Err(errors::invalid_params("Invalid signature received.", err)), } }, + ConfirmationPayload::SignMessage(address, hash) => { + let signature = ethkey::Signature::from_electrum(&bytes.0); + match ethkey::verify_address(&address, &signature, &hash) { + Ok(true) => Ok(ConfirmationResponse::Signature(bytes.0.as_slice().into())), + Ok(false) => Err(errors::invalid_params("Sender address does not match the signature.", ())), + Err(err) => Err(errors::invalid_params("Invalid signature received.", err)), + } + }, ConfirmationPayload::Decrypt(_address, _data) => { // TODO [ToDr]: Decrypt can we verify if the answer is correct? Ok(ConfirmationResponse::Decrypt(bytes)) diff --git a/rpc/src/v1/tests/mocked/personal.rs b/rpc/src/v1/tests/mocked/personal.rs index 344998a92..5098d807d 100644 --- a/rpc/src/v1/tests/mocked/personal.rs +++ b/rpc/src/v1/tests/mocked/personal.rs @@ -25,12 +25,16 @@ use jsonrpc_core::IoHandler; use parking_lot::Mutex; use transaction::{Action, Transaction}; use parity_runtime::Runtime; +use hash::keccak; use v1::{PersonalClient, Personal, Metadata}; -use v1::helpers::nonce; +use v1::helpers::{nonce, eip191}; use v1::helpers::dispatch::{eth_data_hash, FullDispatcher}; use v1::tests::helpers::TestMinerService; -use v1::types::H520; +use v1::types::{EIP191Version, PresignedTransaction, H520}; +use rustc_hex::ToHex; +use serde_json::to_value; +use ethkey::Secret; struct PersonalTester { _runtime: Runtime, @@ -328,3 +332,89 @@ fn should_unlock_account_permanently() { assert_eq!(tester.io.handle_request_sync(&request), Some(response.into())); assert!(tester.accounts.sign(address, None, Default::default()).is_ok(), "Should unlock account."); } + +#[test] +fn sign_eip191_with_validator() { + let tester = setup(); + let address = tester.accounts.new_account(&"password123".into()).unwrap(); + let request = r#"{ + "jsonrpc": "2.0", + "method": "personal_sign191", + "params": [ + "0x00", + { + "validator": ""#.to_owned() + &format!("0x{:x}", address) + r#"", + "data": ""# + &format!("0x{:x}", keccak("hello world")) + r#"" + }, + ""# + &format!("0x{:x}", address) + r#"", + "password123" + ], + "id": 1 + }"#; + let with_validator = to_value(PresignedTransaction { + validator: address.into(), + data: keccak("hello world").to_vec().into() + }).unwrap(); + let result = eip191::hash_message(EIP191Version::PresignedTransaction, with_validator).unwrap(); + let result = tester.accounts.sign(address, Some("password123".into()), result).unwrap().into_electrum(); + let expected = r#"{"jsonrpc":"2.0","result":""#.to_owned() + &format!("0x{}", result.to_hex()) + r#"","id":1}"#; + let response = tester.io.handle_request_sync(&request).unwrap(); + assert_eq!(response, expected) +} + +#[test] +fn sign_eip191_structured_data() { + let tester = setup(); + let secret: Secret = keccak("cow").into(); + let address = tester.accounts.insert_account(secret, &"lol".into()).unwrap(); + let request = r#"{ + "jsonrpc": "2.0", + "method": "personal_sign191", + "params": [ + "0x01", + { + "primaryType": "Mail", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": "0x1", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents": "Hello, Bob!" + }, + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ] + } + }, + ""#.to_owned() + &format!("0x{:x}", address) + r#"", + "lol" + ], + "id": 1 + }"#; + let expected = r#"{"jsonrpc":"2.0","result":"0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c","id":1}"#; + let response = tester.io.handle_request_sync(&request).unwrap(); + assert_eq!(response, expected) +} diff --git a/rpc/src/v1/traits/personal.rs b/rpc/src/v1/traits/personal.rs index 4798b0e2d..cda33f4cf 100644 --- a/rpc/src/v1/traits/personal.rs +++ b/rpc/src/v1/traits/personal.rs @@ -15,9 +15,10 @@ // along with Parity. If not, see . //! Personal rpc interface. -use jsonrpc_core::{BoxFuture, Result}; use eip712::EIP712; -use v1::types::{Bytes, U128, H160, H256, H520, TransactionRequest, RichRawTransaction as RpcRichRawTransaction}; +use jsonrpc_core::types::Value; +use jsonrpc_core::{BoxFuture, Result}; +use v1::types::{Bytes, U128, H160, H256, H520, TransactionRequest, RichRawTransaction as RpcRichRawTransaction, EIP191Version}; build_rpc_trait! { /// Personal rpc interface. Safe (read-only) functions. @@ -47,6 +48,10 @@ build_rpc_trait! { #[rpc(name = "personal_signTypedData")] fn sign_typed_data(&self, EIP712, H160, String) -> BoxFuture; + /// Signs an arbitrary message based on the version specified + #[rpc(name = "personal_sign191")] + fn sign_191(&self, EIP191Version, Value, H160, String) -> BoxFuture; + /// Returns the account associated with the private key that was used to calculate the signature in /// `personal_sign`. #[rpc(name = "personal_ecRecover")] diff --git a/rpc/src/v1/types/confirmations.rs b/rpc/src/v1/types/confirmations.rs index ec4d48c8d..e04bda957 100644 --- a/rpc/src/v1/types/confirmations.rs +++ b/rpc/src/v1/types/confirmations.rs @@ -59,31 +59,62 @@ impl fmt::Display for ConfirmationPayload { ConfirmationPayload::SendTransaction(ref transaction) => write!(f, "{}", transaction), ConfirmationPayload::SignTransaction(ref transaction) => write!(f, "(Sign only) {}", transaction), ConfirmationPayload::EthSignMessage(ref sign) => write!(f, "{}", sign), + ConfirmationPayload::EIP191SignMessage(ref sign) => write!(f, "{}", sign), ConfirmationPayload::Decrypt(ref decrypt) => write!(f, "{}", decrypt), } } } -/// Sign request +/// Ethereum-prefixed Sign request #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct SignRequest { +pub struct EthSignRequest { /// Address pub address: H160, /// Hash to sign pub data: Bytes, } -impl From<(H160, Bytes)> for SignRequest { - fn from(tuple: (H160, Bytes)) -> Self { - SignRequest { +/// EIP191 Sign request +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EIP191SignRequest { + /// Address + pub address: H160, + /// Hash to sign + pub data: H256, +} + +impl From<(H160, H256)> for EIP191SignRequest { + fn from(tuple: (H160, H256)) -> Self { + EIP191SignRequest { address: tuple.0, data: tuple.1, } } } -impl fmt::Display for SignRequest { +impl fmt::Display for EIP191SignRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "sign 0x{} with {}", + self.data.0.pretty(), + Colour::White.bold().paint(format!("0x{:?}", self.address)), + ) + } +} + +impl From<(H160, Bytes)> for EthSignRequest { + fn from(tuple: (H160, Bytes)) -> Self { + EthSignRequest { + address: tuple.0, + data: tuple.1, + } + } +} + +impl fmt::Display for EthSignRequest { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -169,7 +200,9 @@ pub enum ConfirmationPayload { SignTransaction(TransactionRequest), /// Signature #[serde(rename = "sign")] - EthSignMessage(SignRequest), + EthSignMessage(EthSignRequest), + /// signature without prefix + EIP191SignMessage(EIP191SignRequest), /// Decryption Decrypt(DecryptRequest), } @@ -179,7 +212,11 @@ impl From for ConfirmationPayload { match c { helpers::ConfirmationPayload::SendTransaction(t) => ConfirmationPayload::SendTransaction(t.into()), helpers::ConfirmationPayload::SignTransaction(t) => ConfirmationPayload::SignTransaction(t.into()), - helpers::ConfirmationPayload::EthSignMessage(address, data) => ConfirmationPayload::EthSignMessage(SignRequest { + helpers::ConfirmationPayload::EthSignMessage(address, data) => ConfirmationPayload::EthSignMessage(EthSignRequest { + address: address.into(), + data: data.into(), + }), + helpers::ConfirmationPayload::SignMessage(address, data) => ConfirmationPayload::EIP191SignMessage(EIP191SignRequest { address: address.into(), data: data.into(), }), diff --git a/rpc/src/v1/types/eip191.rs b/rpc/src/v1/types/eip191.rs new file mode 100644 index 000000000..d5dce3255 --- /dev/null +++ b/rpc/src/v1/types/eip191.rs @@ -0,0 +1,57 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +//! EIP-191 specific types +use serde::{Deserialize, Deserializer}; +use serde::de; +use v1::types::{H160, Bytes}; + +/// EIP-191 version specifier +#[derive(Debug)] +pub enum EIP191Version { + /// byte specifier for structured data (0x01) + StructuredData, + /// byte specifier for personal message (0x45) + PersonalMessage, + /// byte specifier for presignedtransaction (0x00) + PresignedTransaction +} + +/// EIP-191 version 0x0 struct +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PresignedTransaction { + // address of intended validator + pub validator: H160, + // application specific data + pub data: Bytes +} + +impl<'de> Deserialize<'de> for EIP191Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let byte_version = match s.as_str() { + "0x00" => EIP191Version::PresignedTransaction, + "0x01" => EIP191Version::StructuredData, + "0x45" => EIP191Version::PersonalMessage, + other => return Err(de::Error::custom(format!("Invalid byte version '{}'", other))), + }; + Ok(byte_version) + } +} diff --git a/rpc/src/v1/types/mod.rs b/rpc/src/v1/types/mod.rs index eb8c0dc22..b2e7c074b 100644 --- a/rpc/src/v1/types/mod.rs +++ b/rpc/src/v1/types/mod.rs @@ -43,9 +43,10 @@ mod transaction_condition; mod uint; mod work; mod private_receipt; +mod eip191; pub mod pubsub; - +pub use self::eip191::{EIP191Version, PresignedTransaction}; pub use self::account_info::{AccountInfo, ExtAccountInfo, HwAccountInfo}; pub use self::bytes::Bytes; pub use self::block::{RichBlock, Block, BlockTransactions, Header, RichHeader, Rich}; @@ -53,7 +54,7 @@ pub use self::block_number::{BlockNumber, LightBlockNumber, block_number_to_id}; pub use self::call_request::CallRequest; pub use self::confirmations::{ ConfirmationPayload, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken, - TransactionModification, SignRequest, DecryptRequest, Either + TransactionModification, EIP191SignRequest, EthSignRequest, DecryptRequest, Either }; pub use self::consensus_status::*; pub use self::derivation::{DeriveHash, DeriveHierarchical, Derive}; diff --git a/util/EIP-712/src/encode.rs b/util/EIP-712/src/encode.rs index dee4a38aa..2add93db5 100644 --- a/util/EIP-712/src/encode.rs +++ b/util/EIP-712/src/encode.rs @@ -208,7 +208,7 @@ fn encode_data( } /// encodes and hashes the given EIP712 struct -pub fn hash_structured_data(typed_data: EIP712) -> Result> { +pub fn hash_structured_data(typed_data: EIP712) -> Result { // validate input typed_data.validate()?; // EIP-191 compliant @@ -220,7 +220,7 @@ pub fn hash_structured_data(typed_data: EIP712) -> Result> { encode_data(&parser, &Type::Custom(typed_data.primary_type), &typed_data.types, &typed_data.message, None)? ); let concat = [&prefix[..], &domain_hash[..], &data_hash[..]].concat(); - Ok(keccak(concat).to_vec()) + Ok(keccak(concat)) } #[cfg(test)]