diff --git a/rpc/src/v1/helpers/mod.rs b/rpc/src/v1/helpers/mod.rs index 0259818af..c770836fb 100644 --- a/rpc/src/v1/helpers/mod.rs +++ b/rpc/src/v1/helpers/mod.rs @@ -36,8 +36,10 @@ mod signing_queue; mod subscribers; mod subscription_manager; mod work; +mod signature; pub use self::dispatch::{Dispatcher, FullDispatcher, LightDispatcher}; +pub use self::signature::verify_signature; pub use self::network_settings::NetworkSettings; pub use self::poll_manager::PollManager; pub use self::poll_filter::{PollFilter, SyncPollFilter, limit_logs}; diff --git a/rpc/src/v1/helpers/signature.rs b/rpc/src/v1/helpers/signature.rs new file mode 100644 index 000000000..47664fc74 --- /dev/null +++ b/rpc/src/v1/helpers/signature.rs @@ -0,0 +1,182 @@ +// 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 . + +use ethkey::{recover, public_to_address, Signature}; +use jsonrpc_core::Result; +use v1::types::{Bytes, RecoveredAccount, H256, U64}; +use v1::helpers::errors; +use v1::helpers::dispatch::eth_data_hash; +use hash::keccak; + +/// helper method for parity_verifySignature +pub fn verify_signature( + is_prefixed: bool, + message: Bytes, + r: H256, + s: H256, + v: U64, + chain_id: Option +) -> Result { + let hash = if is_prefixed { + eth_data_hash(message.0) + } else { + keccak(message.0) + }; + let v: u64 = v.into(); + let is_valid_for_current_chain = match (chain_id, v) { + (None, v) if v == 0 || v == 1 => true, + (Some(chain_id), v) if v >= 35 => (v - 35) / 2 == chain_id, + _ => false, + }; + + let v = if v >= 35 { (v - 1) % 2 } else { v }; + + let signature = Signature::from_rsv(&r.into(), &s.into(), v as u8); + let public_key = recover(&signature, &hash).map_err(errors::encryption)?; + let address = public_to_address(&public_key); + Ok(RecoveredAccount { address, public_key, is_valid_for_current_chain }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use ethcore::account_provider::AccountProvider; + use v1::types::H160; + + pub fn add_chain_replay_protection(v: u64, chain_id: Option) -> u64 { + v + if let Some(n) = chain_id { 35 + n * 2 } else { 0 } + } + + struct TestCase { + should_prefix: bool, + signing_chain_id: Option, + rpc_chain_id: Option, + is_valid_for_current_chain: bool, + } + + /// mocked signer + fn sign(should_prefix: bool, data: Vec, signing_chain_id: Option) -> (H160, [u8; 32], [u8; 32], U64) { + let hash = if should_prefix { eth_data_hash(data) } else { keccak(data) }; + let accounts = Arc::new(AccountProvider::transient_provider()); + let address = accounts.new_account(&"password123".into()).unwrap(); + let sig = accounts.sign(address, Some("password123".into()), hash).unwrap(); + let (r, s, v) = (sig.r(), sig.s(), sig.v()); + let v = add_chain_replay_protection(v as u64, signing_chain_id); + let (r_buf, s_buf) = { + let (mut r_buf, mut s_buf) = ([0u8; 32], [0u8; 32]); + r_buf.copy_from_slice(r); + s_buf.copy_from_slice(s); + (r_buf, s_buf) + }; + (address.into(), r_buf, s_buf, v.into()) + } + + fn run_test(test_case: TestCase) { + let TestCase { should_prefix, signing_chain_id, rpc_chain_id, is_valid_for_current_chain } = test_case; + let data = vec![5u8]; + + let (address, r, s, v) = sign(should_prefix, data.clone(), signing_chain_id); + let account = verify_signature(should_prefix, data.into(), r.into(), s.into(), v, rpc_chain_id).unwrap(); + + assert_eq!(account.address, address.into()); + assert_eq!(account.is_valid_for_current_chain, is_valid_for_current_chain) + } + + #[test] + fn test_verify_signature_prefixed_mainnet() { + run_test(TestCase { + should_prefix: true, + signing_chain_id: Some(1), + rpc_chain_id: Some(1), + is_valid_for_current_chain: true, + }) + } + + #[test] + fn test_verify_signature_not_prefixed_mainnet() { + run_test(TestCase { + should_prefix: false, + signing_chain_id: Some(1), + rpc_chain_id: Some(1), + is_valid_for_current_chain: true, + }) + } + + #[test] + fn test_verify_signature_incompatible_chain_id() { + run_test(TestCase { + should_prefix: false, + signing_chain_id: Some(65), + rpc_chain_id: Some(1), + is_valid_for_current_chain: false, + }); + run_test(TestCase { + should_prefix: true, + signing_chain_id: Some(65), + rpc_chain_id: Some(1), + is_valid_for_current_chain: false, + }); + } + + #[test] + fn test_verify_signature_no_signing_chain_id() { + run_test(TestCase { + should_prefix: false, + signing_chain_id: None, + rpc_chain_id: Some(1), + is_valid_for_current_chain: false, + }); + run_test(TestCase { + should_prefix: true, + signing_chain_id: None, + rpc_chain_id: Some(1), + is_valid_for_current_chain: false, + }); + } + + #[test] + fn test_verify_signature_no_rpc_chain_id() { + run_test(TestCase { + should_prefix: false, + signing_chain_id: Some(1), + rpc_chain_id: None, + is_valid_for_current_chain: false, + }); + run_test(TestCase { + should_prefix: true, + signing_chain_id: Some(1), + rpc_chain_id: None, + is_valid_for_current_chain: false, + }); + } + + #[test] + fn test_verify_signature_no_chain_replay_protection() { + run_test(TestCase { + should_prefix: false, + signing_chain_id: None, + rpc_chain_id: None, + is_valid_for_current_chain: true, + }); + run_test(TestCase { + should_prefix: true, + signing_chain_id: None, + rpc_chain_id: None, + is_valid_for_current_chain: true, + }); + } +} diff --git a/rpc/src/v1/impls/light/parity.rs b/rpc/src/v1/impls/light/parity.rs index 3d5bf7897..c281760f3 100644 --- a/rpc/src/v1/impls/light/parity.rs +++ b/rpc/src/v1/impls/light/parity.rs @@ -30,18 +30,18 @@ use ethcore_logger::RotatingLogger; use jsonrpc_core::{Result, BoxFuture}; use jsonrpc_core::futures::Future; use jsonrpc_macros::Trailing; -use v1::helpers::{self, errors, ipfs, SigningQueue, SignerService, NetworkSettings}; +use v1::helpers::{self, errors, ipfs, SigningQueue, SignerService, NetworkSettings, verify_signature}; use v1::helpers::dispatch::LightDispatcher; use v1::helpers::light_fetch::{LightFetch, light_all_transactions}; use v1::metadata::Metadata; use v1::traits::Parity; use v1::types::{ - Bytes, U256, H64, H160, H256, H512, CallRequest, + Bytes, U256, U64, H64, H160, H256, H512, CallRequest, Peers, Transaction, RpcSettings, Histogram, TransactionStats, LocalTransactionStatus, - BlockNumber, LightBlockNumber, ConsensusCapability, VersionInfo, - OperationsInfo, ChainStatus, - AccountInfo, HwAccountInfo, Header, RichHeader, Receipt, + LightBlockNumber, ChainStatus, Receipt, + BlockNumber, ConsensusCapability, VersionInfo, + OperationsInfo, AccountInfo, HwAccountInfo, Header, RichHeader, RecoveredAccount }; use Host; @@ -425,4 +425,7 @@ impl Parity for ParityClient { Err(errors::status_error(has_peers)) } } + fn verify_signature(&self, is_prefixed: bool, message: Bytes, r: H256, s: H256, v: U64) -> Result { + verify_signature(is_prefixed, message, r, s, v, self.light_dispatch.client.signing_chain_id()) + } } diff --git a/rpc/src/v1/impls/parity.rs b/rpc/src/v1/impls/parity.rs index 2671a0eab..9d73238a0 100644 --- a/rpc/src/v1/impls/parity.rs +++ b/rpc/src/v1/impls/parity.rs @@ -38,17 +38,17 @@ use jsonrpc_core::{BoxFuture, Result}; use jsonrpc_core::futures::future; use jsonrpc_macros::Trailing; -use v1::helpers::{self, errors, fake_sign, ipfs, SigningQueue, SignerService, NetworkSettings}; use v1::helpers::block_import::is_major_importing; +use v1::helpers::{self, errors, fake_sign, ipfs, SigningQueue, SignerService, NetworkSettings, verify_signature}; use v1::metadata::Metadata; use v1::traits::Parity; use v1::types::{ - Bytes, U256, H64, H160, H256, H512, CallRequest, + Bytes, U256, H64, U64, H160, H256, H512, CallRequest, Peers, Transaction, RpcSettings, Histogram, TransactionStats, LocalTransactionStatus, BlockNumber, ConsensusCapability, VersionInfo, OperationsInfo, ChainStatus, - AccountInfo, HwAccountInfo, RichHeader, Receipt, + AccountInfo, HwAccountInfo, RichHeader, Receipt, RecoveredAccount, block_number_to_id }; use Host; @@ -504,4 +504,8 @@ impl Parity for ParityClient where Err(errors::status_error(has_peers)) } } + + fn verify_signature(&self, is_prefixed: bool, message: Bytes, r: H256, s: H256, v: U64) -> Result { + verify_signature(is_prefixed, message, r, s, v, self.client.signing_chain_id()) + } } diff --git a/rpc/src/v1/tests/mocked/parity.rs b/rpc/src/v1/tests/mocked/parity.rs index 0fa623ed4..d978a4ac7 100644 --- a/rpc/src/v1/tests/mocked/parity.rs +++ b/rpc/src/v1/tests/mocked/parity.rs @@ -604,3 +604,26 @@ fn rpc_status_error_sync() { assert_eq!(io.handle_request_sync(request), Some(response.to_owned())); } + +#[test] +fn rpc_parity_verify_signature() { + let deps = Dependencies::new(); + let io = deps.default_client(); + + let request = r#"{ + "jsonrpc": "2.0", + "method": "parity_verifySignature", + "params": [ + false, + "0xe552acf4caabe9661893fd48c7b5e68af20bf007193442f8ca05ce836699d75e", + "0x2089e84151c3cdc45255c07557b349f5bf2ed3e68f6098723eaa90a0f8b2b3e5", + "0x5f70e8df7bd0c4417afb5f5a39d82e15d03adeff8796725d8b14889ed1d1aa8a", + "0x1" + ], + "id": 0 + }"#; + + let response = r#"{"jsonrpc":"2.0","result":{"address":"0x9a2a08a1170f51208c2f3cede0d29ada94481eed","isValidForCurrentChain":true,"publicKey":"0xbeec94ea24444906fe247c47841a45220f07e5718d06157fe4502fac326dab617e973e221e45746721330c2db3f63202268686378cc28b9800c1daaf0bbafeb1"},"id":0}"#; + + assert_eq!(io.handle_request_sync(request), Some(response.to_owned())); +} diff --git a/rpc/src/v1/traits/parity.rs b/rpc/src/v1/traits/parity.rs index a81e50089..5ac467018 100644 --- a/rpc/src/v1/traits/parity.rs +++ b/rpc/src/v1/traits/parity.rs @@ -20,10 +20,9 @@ use std::collections::BTreeMap; use jsonrpc_core::{BoxFuture, Result}; use jsonrpc_macros::Trailing; - use v1::types::{ - H64, H160, H256, H512, U256, Bytes, CallRequest, - Peers, Transaction, RpcSettings, Histogram, + H160, H256, H512, U256, U64, H64, Bytes, CallRequest, + Peers, Transaction, RpcSettings, Histogram, RecoveredAccount, TransactionStats, LocalTransactionStatus, BlockNumber, ConsensusCapability, VersionInfo, OperationsInfo, ChainStatus, @@ -237,5 +236,10 @@ build_rpc_trait! { /// Otherwise the RPC returns error. #[rpc(name = "parity_nodeStatus")] fn status(&self) -> Result<()>; + + /// Extracts Address and public key from signature using the r, s and v params. Equivalent to Solidity erecover + /// as well as checks the signature for chain replay protection + #[rpc(name = "parity_verifySignature")] + fn verify_signature(&self, bool, Bytes, H256, H256, U64) -> Result; } } diff --git a/rpc/src/v1/types/account_info.rs b/rpc/src/v1/types/account_info.rs index 487507de9..e0e2464d8 100644 --- a/rpc/src/v1/types/account_info.rs +++ b/rpc/src/v1/types/account_info.rs @@ -13,6 +13,10 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . + +//! Return types for RPC calls + +use ethereum_types::{Public, Address}; use v1::types::{H160, H256, U256, Bytes}; /// Account information. @@ -64,3 +68,19 @@ pub struct HwAccountInfo { /// Device manufacturer. pub manufacturer: String, } + +/// account derived from a signature +/// as well as information that tells if it is valid for +/// the current chain +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all="camelCase")] +pub struct RecoveredAccount { + /// address of the recovered account + pub address: Address, + /// public key of the recovered account + pub public_key: Public, + /// If the signature contains chain replay protection, + /// And the chain_id encoded within the signature + /// matches the current chain this would be true, otherwise false. + pub is_valid_for_current_chain: bool +} diff --git a/rpc/src/v1/types/mod.rs b/rpc/src/v1/types/mod.rs index 68401fb0c..cde98e597 100644 --- a/rpc/src/v1/types/mod.rs +++ b/rpc/src/v1/types/mod.rs @@ -48,7 +48,7 @@ mod eip191; pub mod pubsub; pub use self::eip191::{EIP191Version, PresignedTransaction}; -pub use self::account_info::{AccountInfo, ExtAccountInfo, HwAccountInfo, EthAccount, StorageProof}; +pub use self::account_info::{AccountInfo, ExtAccountInfo, HwAccountInfo, EthAccount, StorageProof, RecoveredAccount}; pub use self::bytes::Bytes; pub use self::block::{RichBlock, Block, BlockTransactions, Header, RichHeader, Rich}; pub use self::block_number::{BlockNumber, LightBlockNumber, block_number_to_id};