diff --git a/Cargo.lock b/Cargo.lock index 7b8043fd..7ff8ba4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1333,6 +1333,7 @@ dependencies = [ "tracing", "tracing-futures", "tracing-subscriber", + "trezor-client", "yubihsm", ] @@ -2452,6 +2453,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "protobuf" +version = "2.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" + [[package]] name = "quote" version = "1.0.10" @@ -3648,6 +3655,21 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trezor-client" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff94fab279e0d429d762c289f9727f37a0f1b8207ea4795f09c11caad009046f" +dependencies = [ + "byteorder", + "hex", + "hidapi", + "log", + "primitive-types", + "protobuf", + "rusb", +] + [[package]] name = "try-lock" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 12434931..60fa90bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ openssl = ["ethers-providers/openssl"] dev-rpc = ["ethers-providers/dev-rpc"] ## signers ledger = ["ethers-signers/ledger"] +trezor = ["ethers-signers/trezor"] yubi = ["ethers-signers/yubi"] ## contracts abigen = ["ethers-contract/abigen"] diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 84c2b914..46691e67 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -28,6 +28,7 @@ yubihsm = { version = "0.39.0", features = ["secp256k1", "http", "usb"], optiona futures-util = "0.3.18" futures-executor = "0.3.18" semver = "1.0.4" +trezor-client = { version = "0.0.3", optional = true, default-features = false, features = ["f_ethereum"] } # aws rusoto_core = { version = "0.47.0", optional = true } @@ -56,3 +57,4 @@ celo = ["ethers-core/celo"] ledger = ["coins-ledger"] yubi = ["yubihsm"] aws = ["rusoto_core", "rusoto_kms", "tracing", "tracing-futures", "spki"] +trezor = ["trezor-client"] diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 4b94355c..f127fa30 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -20,6 +20,14 @@ pub use ledger::{ types::{DerivationType as HDPath, LedgerError}, }; +#[cfg(feature = "trezor")] +mod trezor; +#[cfg(feature = "trezor")] +pub use trezor::{ + app::TrezorEthereum as Trezor, + types::{DerivationType as TrezorHDPath, TrezorError}, +}; + #[cfg(feature = "yubi")] pub use yubihsm; diff --git a/ethers-signers/src/trezor/app.rs b/ethers-signers/src/trezor/app.rs new file mode 100644 index 00000000..8e299ea6 --- /dev/null +++ b/ethers-signers/src/trezor/app.rs @@ -0,0 +1,339 @@ +#![allow(unused)] +use trezor_client::client::{AccessListItem as Trezor_AccessListItem, Trezor}; + +use futures_executor::block_on; +use futures_util::lock::Mutex; + +use ethers_core::{ + types::{ + transaction::{eip2718::TypedTransaction, eip712::Eip712}, + Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256, + }, + utils::keccak256, +}; +use std::convert::TryFrom; +use thiserror::Error; + +use super::types::*; + +/// A Trezor Ethereum App. +/// +/// This is a simple wrapper around the [Trezor transport](Trezor) +#[derive(Debug)] +pub struct TrezorEthereum { + derivation: DerivationType, + session_id: Vec, + pub(crate) chain_id: u64, + pub(crate) address: Address, +} + +const FIRMWARE_MIN_VERSION: &str = ">=2.4.2"; + +impl TrezorEthereum { + pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { + let mut blank = Self { + derivation: derivation.clone(), + chain_id, + address: Address::from([0_u8; 20]), + session_id: vec![], + }; + + // Check if reachable + blank.initate_session()?; + blank.address = blank.get_address_with_path(&derivation).await?; + Ok(blank) + } + + fn check_version(version: String) -> Result<(), TrezorError> { + let req = semver::VersionReq::parse(FIRMWARE_MIN_VERSION)?; + let version = semver::Version::parse(&version)?; + + // Enforce firmware version is greater than FIRMWARE_MIN_VERSION + if !req.matches(&version) { + return Err(TrezorError::UnsupportedFirmwareVersion(FIRMWARE_MIN_VERSION.to_string())) + } + + Ok(()) + } + + fn initate_session(&mut self) -> Result<(), TrezorError> { + let mut client = trezor_client::unique(false)?; + client.init_device(None)?; + + let features = client.features().ok_or(TrezorError::FeaturesError)?; + + Self::check_version(format!( + "{}.{}.{}", + features.get_major_version(), + features.get_minor_version(), + features.get_patch_version() + ))?; + + self.session_id = features.get_session_id().to_vec(); + + Ok(()) + } + + /// You need to drop(client) once you're done with it + fn get_client(&self, session_id: Vec) -> Result { + let mut client = trezor_client::unique(false)?; + client.init_device(Some(session_id))?; + Ok(client) + } + + /// Get the account which corresponds to our derivation path + pub async fn get_address(&self) -> Result { + Ok(self.get_address_with_path(&self.derivation).await?) + } + + /// Gets the account which corresponds to the provided derivation path + pub async fn get_address_with_path( + &self, + derivation: &DerivationType, + ) -> Result { + let mut client = self.get_client(self.session_id.clone())?; + + let address_str = client.ethereum_get_address(Self::convert_path(derivation))?; + + let mut address = [0; 20]; + address.copy_from_slice(&hex::decode(&address_str[2..])?); + + Ok(Address::from(address)) + } + + /// Signs an Ethereum transaction (requires confirmation on the Trezor) + pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { + let mut client = self.get_client(self.session_id.clone())?; + + let arr_path = Self::convert_path(&self.derivation); + + let transaction = TrezorTransaction::load(tx)?; + + let signature = match tx { + TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx( + arr_path, + transaction.nonce, + transaction.gas_price, + transaction.gas, + transaction.to, + transaction.value, + transaction.data, + self.chain_id, + )?, + TypedTransaction::Eip1559(eip1559_tx) => client.ethereum_sign_eip1559_tx( + arr_path, + transaction.nonce, + transaction.gas, + transaction.to, + transaction.value, + transaction.data, + self.chain_id, + transaction.max_fee_per_gas, + transaction.max_priority_fee_per_gas, + transaction.access_list, + )?, + }; + + Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) + } + + /// Signs an ethereum personal message + pub async fn sign_message>(&self, message: S) -> Result { + let message = message.as_ref(); + let mut client = self.get_client(self.session_id.clone())?; + let apath = Self::convert_path(&self.derivation); + + let signature = client.ethereum_sign_message(message.into(), apath)?; + + Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) + } + + /// Signs an EIP712 encoded domain separator and message + pub async fn sign_typed_struct(&self, payload: &T) -> Result + where + T: Eip712, + { + unimplemented!() + } + + // helper which converts a derivation path to [u32] + fn convert_path(derivation: &DerivationType) -> Vec { + let derivation = derivation.to_string(); + let elements = derivation.split('/').skip(1).collect::>(); + let depth = elements.len(); + + let mut path = vec![]; + for derivation_index in elements { + let hardened = derivation_index.contains('\''); + let mut index = derivation_index.replace('\'', "").parse::().unwrap(); + if hardened { + index |= 0x80000000; + } + path.push(index); + } + + path + } +} + +#[cfg(all(test, feature = "trezor"))] +mod tests { + use super::*; + use crate::Signer; + use ethers_contract::EthAbiType; + use ethers_core::types::{ + transaction::{ + eip2930::{AccessList, AccessListItem}, + eip712::Eip712, + }, + Address, Eip1559TransactionRequest, TransactionRequest, I256, U256, + }; + use ethers_derive_eip712::*; + use std::str::FromStr; + + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "Eip712Test", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000001", + salt = "eip712-test-75F0CCte" + )] + struct FooBar { + foo: I256, + bar: U256, + fizz: Vec, + buzz: [u8; 32], + far: String, + out: Address, + } + + #[tokio::test] + #[ignore] + // Replace this with your ETH addresses. + async fn test_get_address() { + // Instantiate it with the default trezor derivation path + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(1), 1).await.unwrap(); + assert_eq!( + trezor.get_address().await.unwrap(), + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + ); + assert_eq!( + trezor.get_address_with_path(&DerivationType::TrezorLive(0)).await.unwrap(), + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + ); + } + + #[tokio::test] + #[ignore] + async fn test_sign_tx() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap(); + + // approve uni v2 router 0xff + let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); + + let tx_req = TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .gas(1000000) + .gas_price(400e9 as u64) + .nonce(5) + .data(data) + .value(ethers_core::utils::parse_ether(100).unwrap()) + .into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_sign_big_data_tx() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap(); + + // invalid data + let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + &"aa".to_string()).unwrap(); + let tx_req = TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .gas(1000000) + .gas_price(400e9 as u64) + .nonce(5) + .data(big_data) + .value(ethers_core::utils::parse_ether(100).unwrap()) + .into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_sign_eip1559_tx() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap(); + + // approve uni v2 router 0xff + let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); + + let lst = AccessList(vec![ + AccessListItem { + address: "0x8ba1f109551bd432803012645ac136ddd64dba72".parse().unwrap(), + storage_keys: vec![ + "0x0000000000000000000000000000000000000000000000000000000000000000" + .parse() + .unwrap(), + "0x0000000000000000000000000000000000000000000000000000000000000042" + .parse() + .unwrap(), + ], + }, + AccessListItem { + address: "0x2ed7afa17473e17ac59908f088b4371d28585476".parse().unwrap(), + storage_keys: vec![ + "0x0000000000000000000000000000000000000000000000000000000000000000" + .parse() + .unwrap(), + "0x0000000000000000000000000000000000000000000000000000000000000042" + .parse() + .unwrap(), + ], + }, + ]); + + let tx_req = Eip1559TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .gas(1000000) + .max_fee_per_gas(400e9 as u64) + .max_priority_fee_per_gas(400e9 as u64) + .nonce(5) + .data(data) + .access_list(lst) + .value(ethers_core::utils::parse_ether(100).unwrap()) + .into(); + + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_sign_message() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap(); + let message = "hello world"; + let sig = trezor.sign_message(message).await.unwrap(); + let addr = trezor.get_address().await.unwrap(); + sig.verify(message, addr).unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_sign_eip712_struct() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1u64).await.unwrap(); + + let foo_bar = FooBar { + foo: I256::from(10), + bar: U256::from(20), + fizz: b"fizz".to_vec(), + buzz: keccak256("buzz"), + far: String::from("space"), + out: Address::from([0; 20]), + }; + + let sig = trezor.sign_typed_struct(&foo_bar).await.expect("failed to sign typed data"); + let foo_bar_hash = foo_bar.encode_eip712().unwrap(); + sig.verify(foo_bar_hash, trezor.address).unwrap(); + } +} diff --git a/ethers-signers/src/trezor/mod.rs b/ethers-signers/src/trezor/mod.rs new file mode 100644 index 00000000..060959b6 --- /dev/null +++ b/ethers-signers/src/trezor/mod.rs @@ -0,0 +1,52 @@ +pub mod app; +pub mod types; + +use crate::Signer; +use app::TrezorEthereum; +use async_trait::async_trait; +use ethers_core::types::{ + transaction::{eip2718::TypedTransaction, eip712::Eip712}, + Address, Signature, +}; +use types::TrezorError; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Signer for TrezorEthereum { + type Error = TrezorError; + + /// Signs the hash of the provided message after prefixing it + async fn sign_message>( + &self, + message: S, + ) -> Result { + self.sign_message(message).await + } + + /// Signs the transaction + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + self.sign_tx(message).await + } + + /// Signs a EIP712 derived struct + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result { + self.sign_typed_struct(payload).await + } + + /// Returns the signer's Ethereum Address + fn address(&self) -> Address { + self.address + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } + + fn chain_id(&self) -> u64 { + self.chain_id + } +} diff --git a/ethers-signers/src/trezor/types.rs b/ethers-signers/src/trezor/types.rs new file mode 100644 index 00000000..2942d496 --- /dev/null +++ b/ethers-signers/src/trezor/types.rs @@ -0,0 +1,132 @@ +#![allow(clippy::upper_case_acronyms)] +//! Helpers for interacting with the Ethereum Trezor App +//! [Official Docs](https://github.com/TrezorHQ/app-ethereum/blob/master/doc/ethapp.asc) +use std::fmt; +use thiserror::Error; + +use ethers_core::types::{transaction::eip2718::TypedTransaction, NameOrAddress, U256}; +use trezor_client::client::AccessListItem as Trezor_AccessListItem; + +#[derive(Clone, Debug)] +/// Trezor wallet type +pub enum DerivationType { + /// Trezor Live-generated HD path + TrezorLive(usize), + /// Any other path. Attention! Trezor by default forbids custom derivation paths + /// Run trezorctl set safety-checks prompt, to allow it + Other(String), +} + +impl fmt::Display for DerivationType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{}", + match self { + DerivationType::TrezorLive(index) => format!("m/44'/60'/{}'/0/0", index), + DerivationType::Other(inner) => inner.to_owned(), + } + ) + } +} + +#[derive(Error, Debug)] +/// Error when using the Trezor transport +pub enum TrezorError { + /// Underlying Trezor transport error + #[error(transparent)] + TrezorError(#[from] trezor_client::error::Error), + #[error("Trezor was not able to retrieve device features")] + FeaturesError, + #[error("Not able to unpack value for TrezorTransaction.")] + DataError, + #[error(transparent)] + /// Error when converting from a hex string + HexError(#[from] hex::FromHexError), + #[error(transparent)] + /// Error when converting a semver requirement + SemVerError(#[from] semver::Error), + /// Error when signing EIP712 struct with not compatible Trezor ETH app + #[error("Trezor ethereum app requires at least version: {0:?}")] + UnsupportedFirmwareVersion(String), +} + +/// Trezor Transaction Struct +pub struct TrezorTransaction { + pub nonce: Vec, + pub gas: Vec, + pub gas_price: Vec, + pub value: Vec, + pub to: String, + pub data: Vec, + pub max_fee_per_gas: Vec, + pub max_priority_fee_per_gas: Vec, + pub access_list: Vec, +} + +impl TrezorTransaction { + fn to_trimmed_big_endian(_value: &U256) -> Vec { + let mut trimmed_value = [0_u8; 32]; + _value.to_big_endian(&mut trimmed_value); + trimmed_value[_value.leading_zeros() as usize / 8..].to_vec() + } + + pub fn load(tx: &TypedTransaction) -> Result { + let to: String = match tx.to().ok_or(TrezorError::DataError)? { + NameOrAddress::Name(_) => unimplemented!(), + NameOrAddress::Address(value) => format!("0x{}", hex::encode(value)), + }; + + let nonce = Self::to_trimmed_big_endian(tx.nonce().ok_or(TrezorError::DataError)?); + let gas = Self::to_trimmed_big_endian(tx.gas().ok_or(TrezorError::DataError)?); + let gas_price = Self::to_trimmed_big_endian(&tx.gas_price().ok_or(TrezorError::DataError)?); + let value = Self::to_trimmed_big_endian(tx.value().ok_or(TrezorError::DataError)?); + let data = tx.data().ok_or(TrezorError::DataError)?.to_vec(); + + match tx { + TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => Ok(Self { + nonce, + gas, + gas_price, + value, + to, + data, + max_fee_per_gas: vec![], + max_priority_fee_per_gas: vec![], + access_list: vec![], + }), + TypedTransaction::Eip1559(eip1559_tx) => { + let max_fee_per_gas = Self::to_trimmed_big_endian( + &eip1559_tx.max_fee_per_gas.ok_or(TrezorError::DataError)?, + ); + let max_priority_fee_per_gas = Self::to_trimmed_big_endian( + &eip1559_tx.max_priority_fee_per_gas.ok_or(TrezorError::DataError)?, + ); + + let mut access_list: Vec = Vec::new(); + for item in &eip1559_tx.access_list.0 { + let address: String = format!("0x{}", hex::encode(item.address)); + let mut storage_keys: Vec> = Vec::new(); + + for key in &item.storage_keys { + storage_keys.push(key.as_bytes().to_vec()) + } + + access_list.push(Trezor_AccessListItem { address, storage_keys }) + } + + Ok(Self { + nonce, + gas, + gas_price, + value, + to, + data, + max_fee_per_gas, + max_priority_fee_per_gas, + access_list, + }) + } + } + } +} diff --git a/examples/trezor.rs b/examples/trezor.rs new file mode 100644 index 00000000..75c6b454 --- /dev/null +++ b/examples/trezor.rs @@ -0,0 +1,29 @@ +#[tokio::main] +#[cfg(feature = "trezor")] +async fn main() -> Result<(), Box> { + use ethers::{prelude::*, utils::parse_ether}; + + // Connect over websockets + let provider = Provider::new(Ws::connect("ws://localhost:8545").await?); + // Instantiate the connection to trezor with Trezor Live derivation path and + // the wallet's index. You may also provide the chain_id. + // (here: mainnet) for EIP155 support. + // EIP1559 support + // No EIP712 support yet. + let trezor = Trezor::new(TrezorHDPath::TrezorLive(0), 1).await?; + let client = SignerMiddleware::new(provider, trezor); + + // Create and broadcast a transaction (ENS disabled!) + // (this will require confirming the tx on the device) + let tx = TransactionRequest::new() + .to("0x99E2B13A8Ea8b00C68FA017ee250E98e870D8241") + .value(parse_ether(10)?); + let pending_tx = client.send_transaction(tx, None).await?; + + // Get the receipt + let _receipt = pending_tx.confirmations(3).await?; + Ok(()) +} + +#[cfg(not(feature = "trezor"))] +fn main() {}