Support external eth_sign (#5481)

* Display a QR for eth_sign requests.

* Support raw confirmation of eth_sign

* Fix ethkey issue on nightly.

* Fixing test.

* Fixing test.
This commit is contained in:
Tomasz Drwięga
2017-04-27 18:23:22 +02:00
committed by Gav Wood
parent 43175f17e4
commit 28dcbc6426
17 changed files with 313 additions and 93 deletions

View File

@@ -66,6 +66,10 @@ extern crate ethjson;
#[cfg(test)]
extern crate ethcore_devtools as devtools;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
pub extern crate jsonrpc_ws_server as ws;
mod metadata;

View File

@@ -206,6 +206,17 @@ pub fn fetch_gas_price_corpus(
}
}
/// Returns a eth_sign-compatible hash of data to sign.
/// The data is prepended with special message to prevent
/// chosen-plaintext attacks.
pub fn eth_data_hash(mut data: Bytes) -> H256 {
let mut message_data =
format!("\x19Ethereum Signed Message:\n{}", data.len())
.into_bytes();
message_data.append(&mut data);
message_data.sha3()
}
/// Dispatcher for light clients -- fetches default gas price, next nonce, etc. from network.
#[derive(Clone)]
pub struct LightDispatcher {
@@ -474,21 +485,11 @@ pub fn execute<D: Dispatcher + 'static>(
.map(ConfirmationResponse::SignTransaction)
).boxed()
},
ConfirmationPayload::EthSignMessage(address, mut data) => {
let mut message_data =
format!("\x19Ethereum Signed Message:\n{}", data.len())
.into_bytes();
message_data.append(&mut data);
let res = signature(&accounts, address, message_data.sha3(), pass)
ConfirmationPayload::EthSignMessage(address, data) => {
let hash = eth_data_hash(data);
let res = signature(&accounts, address, hash, pass)
.map(|result| result
.map(|rsv| {
let mut vrs = [0u8; 65];
let rsv = rsv.as_ref();
vrs[0] = rsv[64] + 27;
vrs[1..33].copy_from_slice(&rsv[0..32]);
vrs[33..65].copy_from_slice(&rsv[32..64]);
H520(vrs)
})
.map(|rsv| H520(rsv.into_vrs()))
.map(RpcH520::from)
.map(ConfirmationResponse::Signature)
);

View File

@@ -21,12 +21,13 @@ use std::sync::{Arc, Weak};
use rlp::UntrustedRlp;
use ethcore::account_provider::AccountProvider;
use ethcore::transaction::{SignedTransaction, PendingTransaction};
use ethkey;
use futures::{future, BoxFuture, Future, IntoFuture};
use jsonrpc_core::Error;
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload};
use v1::helpers::dispatch::{self, Dispatcher, WithToken};
use v1::helpers::accounts::unwrap_provider;
use v1::helpers::dispatch::{self, Dispatcher, WithToken, eth_data_hash};
use v1::helpers::{errors, SignerService, SigningQueue, ConfirmationPayload, FilledTransactionRequest};
use v1::traits::Signer;
use v1::types::{TransactionModification, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken, U256, Bytes};
@@ -104,6 +105,37 @@ impl<D: Dispatcher + 'static> SignerClient<D> {
})
.unwrap_or_else(|| future::err(errors::invalid_params("Unknown RequestID", id)).boxed())
}
fn verify_transaction<F>(bytes: Bytes, request: FilledTransactionRequest, process: F) -> Result<ConfirmationResponse, Error> where
F: FnOnce(PendingTransaction) -> Result<ConfirmationResponse, Error>,
{
let signed_transaction = UntrustedRlp::new(&bytes.0).as_val().map_err(errors::from_rlp_error)?;
let signed_transaction = SignedTransaction::new(signed_transaction).map_err(|e| errors::invalid_params("Invalid signature.", e))?;
let sender = signed_transaction.sender();
// Verification
let sender_matches = sender == request.from;
let data_matches = signed_transaction.data == request.data;
let value_matches = signed_transaction.value == request.value;
let nonce_matches = match request.nonce {
Some(nonce) => signed_transaction.nonce == nonce,
None => true,
};
// Dispatch if everything is ok
if sender_matches && data_matches && value_matches && nonce_matches {
let pending_transaction = PendingTransaction::new(signed_transaction, request.condition.map(Into::into));
process(pending_transaction)
} else {
let mut error = Vec::new();
if !sender_matches { error.push("from") }
if !data_matches { error.push("data") }
if !value_matches { error.push("value") }
if !nonce_matches { error.push("nonce") }
Err(errors::invalid_params("Sent transaction does not match the request.", error))
}
}
}
impl<D: Dispatcher + 'static> Signer for SignerClient<D> {
@@ -149,38 +181,27 @@ impl<D: Dispatcher + 'static> Signer for SignerClient<D> {
signer.peek(&id).map(|confirmation| {
let result = match confirmation.payload {
ConfirmationPayload::SendTransaction(request) => {
let signed_transaction = UntrustedRlp::new(&bytes.0).as_val().map_err(errors::from_rlp_error)?;
let signed_transaction = SignedTransaction::new(signed_transaction).map_err(|e| errors::invalid_params("Invalid signature.", e))?;
let sender = signed_transaction.sender();
// Verification
let sender_matches = sender == request.from;
let data_matches = signed_transaction.data == request.data;
let value_matches = signed_transaction.value == request.value;
let nonce_matches = match request.nonce {
Some(nonce) => signed_transaction.nonce == nonce,
None => true,
};
// Dispatch if everything is ok
if sender_matches && data_matches && value_matches && nonce_matches {
let pending_transaction = PendingTransaction::new(signed_transaction, request.condition.map(Into::into));
Self::verify_transaction(bytes, request, |pending_transaction| {
self.dispatcher.dispatch_transaction(pending_transaction)
.map(Into::into)
.map(ConfirmationResponse::SendTransaction)
} else {
let mut error = Vec::new();
if !sender_matches { error.push("from") }
if !data_matches { error.push("data") }
if !value_matches { error.push("value") }
if !nonce_matches { error.push("nonce") }
Err(errors::invalid_params("Sent transaction does not match the request.", error))
})
},
ConfirmationPayload::SignTransaction(request) => {
Self::verify_transaction(bytes, request, |pending_transaction| {
Ok(ConfirmationResponse::SignTransaction(pending_transaction.transaction.into()))
})
},
ConfirmationPayload::EthSignMessage(address, data) => {
let expected_hash = eth_data_hash(data);
let signature = ethkey::Signature::from_vrs(&bytes.0);
match ethkey::verify_address(&address, &signature, &expected_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)),
}
},
// TODO [ToDr]:
// 1. Sign - verify signature
// 2. Decrypt - pass through?
// TODO [ToDr]: Decrypt - pass through?
_ => Err(errors::unimplemented(Some("Non-transaction requests does not support RAW signing yet.".into()))),
};
if let Ok(ref response) = result {

View File

@@ -20,7 +20,7 @@ use util::{U256, Uint, Address, ToPretty};
use ethcore::account_provider::AccountProvider;
use ethcore::client::TestBlockChainClient;
use ethcore::transaction::{Transaction, Action};
use ethcore::transaction::{Transaction, Action, SignedTransaction};
use rlp::encode;
use serde_json;
@@ -28,8 +28,9 @@ use jsonrpc_core::IoHandler;
use v1::{SignerClient, Signer, Origin};
use v1::metadata::Metadata;
use v1::tests::helpers::TestMinerService;
use v1::types::H520;
use v1::helpers::{SigningQueue, SignerService, FilledTransactionRequest, ConfirmationPayload};
use v1::helpers::dispatch::FullDispatcher;
use v1::helpers::dispatch::{FullDispatcher, eth_data_hash};
struct SignerTester {
signer: Arc<SignerService>,
@@ -359,7 +360,6 @@ fn should_confirm_transaction_with_rlp() {
"params":["0x1", "0x"#.to_owned() + &rlp.to_hex() + r#""],
"id":1
}"#;
println!("{:?}", request);
let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#;
// then
@@ -415,6 +415,101 @@ fn should_return_error_when_sender_does_not_match() {
assert_eq!(tester.signer.requests().len(), 1);
}
#[test]
fn should_confirm_sign_transaction_with_rlp() {
// given
let tester = signer_tester();
let address = tester.accounts.new_account("test").unwrap();
let recipient = Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap();
tester.signer.add_request(ConfirmationPayload::SignTransaction(FilledTransactionRequest {
from: address,
used_default_from: false,
to: Some(recipient),
gas_price: U256::from(10_000),
gas: U256::from(10_000_000),
value: U256::from(1),
data: vec![],
nonce: None,
condition: None,
}), Origin::Unknown).unwrap();
assert_eq!(tester.signer.requests().len(), 1);
let t = Transaction {
nonce: U256::zero(),
gas_price: U256::from(0x1000),
gas: U256::from(10_000_000),
action: Action::Call(recipient),
value: U256::from(0x1),
data: vec![]
};
let signature = tester.accounts.sign(address, Some("test".into()), t.hash(None)).unwrap();
let t = SignedTransaction::new(t.with_signature(signature.clone(), None)).unwrap();
let rlp = encode(&t);
// when
let request = r#"{
"jsonrpc":"2.0",
"method":"signer_confirmRequestRaw",
"params":["0x1", "0x"#.to_owned() + &rlp.to_hex() + r#""],
"id":1
}"#;
let response = r#"{"jsonrpc":"2.0","result":{"#.to_owned() +
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
r#""tx":{"# +
r#""blockHash":null,"blockNumber":null,"condition":null,"creates":null,"# +
&format!("\"from\":\"0x{:?}\",", &address) +
r#""gas":"0x989680","gasPrice":"0x1000","# +
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
r#""input":"0x","# +
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
r#""nonce":"0x0","# +
&format!("\"publicKey\":\"0x{:?}\",", t.public_key().unwrap()) +
&format!("\"r\":\"0x{}\",", U256::from(signature.r()).to_hex()) +
&format!("\"raw\":\"0x{}\",", rlp.to_hex()) +
&format!("\"s\":\"0x{}\",", U256::from(signature.s()).to_hex()) +
&format!("\"standardV\":\"0x{}\",", U256::from(t.standard_v()).to_hex()) +
r#""to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","transactionIndex":null,"# +
&format!("\"v\":\"0x{}\",", U256::from(t.original_v()).to_hex()) +
r#""value":"0x1""# +
r#"}},"id":1}"#;
// then
assert_eq!(tester.io.handle_request_sync(&request), Some(response.to_owned()));
assert_eq!(tester.signer.requests().len(), 0);
assert_eq!(tester.miner.imported_transactions.lock().len(), 0);
}
#[test]
fn should_confirm_data_sign_with_signature() {
// given
let tester = signer_tester();
let address = tester.accounts.new_account("test").unwrap();
tester.signer.add_request(ConfirmationPayload::EthSignMessage(
address,
vec![1, 2, 3, 4].into(),
), Origin::Unknown).unwrap();
assert_eq!(tester.signer.requests().len(), 1);
let data_hash = eth_data_hash(vec![1, 2, 3, 4].into());
let signature = H520(tester.accounts.sign(address, Some("test".into()), data_hash).unwrap().into_vrs());
let signature = format!("0x{:?}", signature);
// when
let request = r#"{
"jsonrpc":"2.0",
"method":"signer_confirmRequestRaw",
"params":["0x1", ""#.to_owned() + &signature + r#""],
"id":1
}"#;
let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + &signature + r#"","id":1}"#;
// then
assert_eq!(tester.io.handle_request_sync(&request), Some(response.to_owned()));
assert_eq!(tester.signer.requests().len(), 0);
assert_eq!(tester.miner.imported_transactions.lock().len(), 0);
}
#[test]
fn should_generate_new_token() {
// given

View File

@@ -129,7 +129,7 @@ pub enum ConfirmationResponse {
SendTransaction(H256),
/// Transaction RLP
SignTransaction(RichRawTransaction),
/// Signature
/// Signature (encoded as VRS)
Signature(H520),
/// Decrypted data
Decrypt(Bytes),