From 40551b8ffd33e30d09039a7c5d57ffa0652890c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Wed, 10 Jan 2018 20:44:10 +0000 Subject: [PATCH] Add `personal_sign` and `personal_ecRecover` RPC methods (#7453) * rpc: implement personal_sign * rpc: add test for personal_sign * rpc: implement personal_ecRecover * rpc: add test for personal_ecRecover * rpc: fix order of arguments in personal_sign * rpc: remove auxiliary methods for sign and ec_recover --- rpc/src/v1/impls/personal.rs | 50 +++++++++++++-- rpc/src/v1/tests/mocked/personal.rs | 97 ++++++++++++++++++++++++++++- rpc/src/v1/traits/personal.rs | 12 +++- 3 files changed, 152 insertions(+), 7 deletions(-) diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index f6315581d..b6b56d1ae 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -20,16 +20,23 @@ use std::sync::Arc; use ethcore::account_provider::AccountProvider; use ethcore::transaction::PendingTransaction; -use ethereum_types::{U128, Address}; -use bytes::ToPretty; - +use bytes::{Bytes, ToPretty}; +use ethereum_types::{H520, U128, Address}; +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::dispatch::{Dispatcher, SignWith}; +use v1::helpers::dispatch::{self, eth_data_hash, Dispatcher, SignWith}; use v1::helpers::accounts::unwrap_provider; use v1::traits::Personal; -use v1::types::{H160 as RpcH160, H256 as RpcH256, U128 as RpcU128, TransactionRequest, RichRawTransaction as RpcRichRawTransaction}; +use v1::types::{ + H160 as RpcH160, H256 as RpcH256, H520 as RpcH520, U128 as RpcU128, + Bytes as RpcBytes, + ConfirmationPayload as RpcConfirmationPayload, + ConfirmationResponse as RpcConfirmationResponse, + TransactionRequest, + RichRawTransaction as RpcRichRawTransaction, +}; use v1::metadata::Metadata; /// Account management (personal) rpc implementation. @@ -132,6 +139,39 @@ impl Personal for PersonalClient { } } + fn sign(&self, data: RpcBytes, account: RpcH160, password: String) -> BoxFuture { + let dispatcher = self.dispatcher.clone(); + let accounts = try_bf!(self.account_provider()); + + let payload = RpcConfirmationPayload::EthSignMessage((account.clone(), data).into()); + + Box::new(dispatch::from_rpc(payload, account.into(), &dispatcher) + .and_then(|payload| { + dispatch::execute(dispatcher, accounts, payload, dispatch::SignWith::Password(password)) + }) + .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 ec_recover(&self, data: RpcBytes, signature: RpcH520) -> BoxFuture { + let signature: H520 = signature.into(); + let signature = Signature::from_electrum(&signature); + let data: Bytes = data.into(); + + let hash = eth_data_hash(data); + let account = recover(&signature.into(), &hash) + .map_err(errors::encryption) + .map(|public| { + public_to_address(&public).into() + }); + + Box::new(future::done(account)) + } + fn sign_transaction(&self, meta: Metadata, request: TransactionRequest, password: String) -> BoxFuture { Box::new(self.do_sign_transaction(meta, request, password) .map(|(pending_tx, dispatcher)| dispatcher.enrich(pending_tx.transaction))) diff --git a/rpc/src/v1/tests/mocked/personal.rs b/rpc/src/v1/tests/mocked/personal.rs index 2e1a54297..f81b8188d 100644 --- a/rpc/src/v1/tests/mocked/personal.rs +++ b/rpc/src/v1/tests/mocked/personal.rs @@ -17,6 +17,7 @@ use std::sync::Arc; use std::str::FromStr; +use bytes::ToPretty; use ethereum_types::{U256, Address}; use ethcore::account_provider::AccountProvider; use ethcore::client::TestBlockChainClient; @@ -26,8 +27,9 @@ use parking_lot::Mutex; use v1::{PersonalClient, Personal, Metadata}; use v1::helpers::nonce; -use v1::helpers::dispatch::FullDispatcher; +use v1::helpers::dispatch::{eth_data_hash, FullDispatcher}; use v1::tests::helpers::TestMinerService; +use v1::types::H520; struct PersonalTester { accounts: Arc, @@ -118,6 +120,53 @@ fn invalid_password_test(method: &str) assert_eq!(tester.io.handle_request_sync(request.as_ref()), Some(response.into())); } +#[test] +fn sign() { + let tester = setup(); + let address = tester.accounts.new_account("password123").unwrap(); + let data = vec![5u8]; + + let request = r#"{ + "jsonrpc": "2.0", + "method": "personal_sign", + "params": [ + ""#.to_owned() + format!("0x{}", data.to_hex()).as_ref() + r#"", + ""# + format!("0x{:?}", address).as_ref() + r#"", + "password123" + ], + "id": 1 + }"#; + + let hash = eth_data_hash(data); + let signature = H520(tester.accounts.sign(address, Some("password123".into()), hash).unwrap().into_electrum()); + let signature = format!("0x{:?}", signature); + + let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + &signature + r#"","id":1}"#; + + assert_eq!(tester.io.handle_request_sync(request.as_ref()), Some(response)); +} + +#[test] +fn sign_with_invalid_password() { + let tester = setup(); + let address = tester.accounts.new_account("password123").unwrap(); + + let request = r#"{ + "jsonrpc": "2.0", + "method": "personal_sign", + "params": [ + "0x0000000000000000000000000000000000000000000000000000000000000005", + ""#.to_owned() + format!("0x{:?}", address).as_ref() + r#"", + "" + ], + "id": 1 + }"#; + + let response = r#"{"jsonrpc":"2.0","error":{"code":-32021,"message":"Account password is invalid or account does not exist.","data":"SStore(InvalidPassword)"},"id":1}"#; + + assert_eq!(tester.io.handle_request_sync(request.as_ref()), Some(response.into())); +} + #[test] fn sign_transaction_with_invalid_password() { invalid_password_test("personal_signTransaction"); @@ -190,6 +239,52 @@ fn sign_and_send_test(method: &str) { assert_eq!(tester.io.handle_request_sync(request.as_ref()), Some(response)); } +#[test] +fn ec_recover() { + let tester = setup(); + let address = tester.accounts.new_account("password123").unwrap(); + let data = vec![5u8]; + + let hash = eth_data_hash(data.clone()); + let signature = H520(tester.accounts.sign(address, Some("password123".into()), hash).unwrap().into_electrum()); + let signature = format!("0x{:?}", signature); + + let request = r#"{ + "jsonrpc": "2.0", + "method": "personal_ecRecover", + "params": [ + ""#.to_owned() + format!("0x{}", data.to_hex()).as_ref() + r#"", + ""# + &signature + r#"" + ], + "id": 1 + }"#; + + let address = format!("0x{:?}", address); + let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + &address + r#"","id":1}"#; + + assert_eq!(tester.io.handle_request_sync(request.as_ref()), Some(response.into())); +} + +#[test] +fn ec_recover_invalid_signature() { + let tester = setup(); + let data = vec![5u8]; + + let request = r#"{ + "jsonrpc": "2.0", + "method": "personal_ecRecover", + "params": [ + ""#.to_owned() + format!("0x{}", data.to_hex()).as_ref() + r#"", + "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "id": 1 + }"#; + + let response = r#"{"jsonrpc":"2.0","error":{"code":-32055,"message":"Encryption error.","data":"InvalidSignature"},"id":1}"#; + + assert_eq!(tester.io.handle_request_sync(request.as_ref()), Some(response.into())); +} + #[test] fn should_unlock_not_account_temporarily_if_allow_perm_is_disabled() { let tester = setup(); diff --git a/rpc/src/v1/traits/personal.rs b/rpc/src/v1/traits/personal.rs index 9eda7528b..7c187fcff 100644 --- a/rpc/src/v1/traits/personal.rs +++ b/rpc/src/v1/traits/personal.rs @@ -17,7 +17,7 @@ //! Personal rpc interface. use jsonrpc_core::{BoxFuture, Result}; -use v1::types::{U128, H160, H256, TransactionRequest, RichRawTransaction as RpcRichRawTransaction}; +use v1::types::{Bytes, U128, H160, H256, H520, TransactionRequest, RichRawTransaction as RpcRichRawTransaction}; build_rpc_trait! { /// Personal rpc interface. Safe (read-only) functions. @@ -37,6 +37,16 @@ build_rpc_trait! { #[rpc(name = "personal_unlockAccount")] fn unlock_account(&self, H160, String, Option) -> Result; + /// Signs the hash of data with given account signature using the given password to unlock the account during + /// the request. + #[rpc(name = "personal_sign")] + fn sign(&self, Bytes, 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")] + fn ec_recover(&self, Bytes, H520) -> BoxFuture; + /// Signs transaction. The account is not unlocked in such case. #[rpc(meta, name = "personal_signTransaction")] fn sign_transaction(&self, Self::Metadata, TransactionRequest, String) -> BoxFuture;