feat: trezor support (#663)

* added trezor signer

* linting

* TrezorHDPath instead of HDPath

* update trezor_client rev. added compatible hidapi backend

* remove unused variables

* keep track of the client session_id

* add to Other derivation paths to trezor

* remove commented macro

* remove unnecessary drops

* no ens

* added TrezorTransaction that loads from TypedTransaction

* enforce minimum firmware version

* add big data test to trezor app

* clippy

* replace trezor-client git with published crate

* change one char string to char

* bump trezor-client, with ethereum feature only
This commit is contained in:
joshieDo 2021-12-13 17:23:01 +02:00 committed by GitHub
parent 4c677933ce
commit 6bf325dcab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 585 additions and 0 deletions

22
Cargo.lock generated
View File

@ -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"

View File

@ -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"]

View File

@ -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"]

View File

@ -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;

View File

@ -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<u8>,
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<Self, TrezorError> {
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<u8>) -> Result<Trezor, TrezorError> {
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<Address, TrezorError> {
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<Address, TrezorError> {
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<Signature, TrezorError> {
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<S: AsRef<[u8]>>(&self, message: S) -> Result<Signature, TrezorError> {
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<T>(&self, payload: &T) -> Result<Signature, TrezorError>
where
T: Eip712,
{
unimplemented!()
}
// helper which converts a derivation path to [u32]
fn convert_path(derivation: &DerivationType) -> Vec<u32> {
let derivation = derivation.to_string();
let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
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::<u32>().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<u8>,
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::<Address>().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::<Address>().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::<Address>().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();
}
}

View File

@ -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<S: Send + Sync + AsRef<[u8]>>(
&self,
message: S,
) -> Result<Signature, Self::Error> {
self.sign_message(message).await
}
/// Signs the transaction
async fn sign_transaction(&self, message: &TypedTransaction) -> Result<Signature, Self::Error> {
self.sign_tx(message).await
}
/// Signs a EIP712 derived struct
async fn sign_typed_data<T: Eip712 + Send + Sync>(
&self,
payload: &T,
) -> Result<Signature, Self::Error> {
self.sign_typed_struct(payload).await
}
/// Returns the signer's Ethereum Address
fn address(&self) -> Address {
self.address
}
fn with_chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
self.chain_id = chain_id.into();
self
}
fn chain_id(&self) -> u64 {
self.chain_id
}
}

View File

@ -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<u8>,
pub gas: Vec<u8>,
pub gas_price: Vec<u8>,
pub value: Vec<u8>,
pub to: String,
pub data: Vec<u8>,
pub max_fee_per_gas: Vec<u8>,
pub max_priority_fee_per_gas: Vec<u8>,
pub access_list: Vec<Trezor_AccessListItem>,
}
impl TrezorTransaction {
fn to_trimmed_big_endian(_value: &U256) -> Vec<u8> {
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<Self, TrezorError> {
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<Trezor_AccessListItem> = 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<u8>> = 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,
})
}
}
}
}

29
examples/trezor.rs Normal file
View File

@ -0,0 +1,29 @@
#[tokio::main]
#[cfg(feature = "trezor")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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() {}