fix: eip712 signing with ledger hw (#518)
* fix: eip712 signing with ledger hw This commit fixes the way a EIP712 derived struct is sent to the Ledger HW to obtain the signature. It also checks if the Ledger ETH app is at least 1.6.0 since this is a requirement. * fix: apply cargo fmt * fix: revert ledger eip712 test * refactor: remove unused code * test: add ledger eip712 test * fix: cargo fmt * fix: move the logic to app.rs * feat: make sign_typed_data take a reference * chore: ensure abigen is enabled in ledger tests we need this to compile ethers-contract as a standalone package * test: ensure that the ledger sig verifies * test: pass foo_bar by ref Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
parent
1d2ed0d964
commit
9db36e59fb
|
@ -1025,13 +1025,17 @@ dependencies = [
|
||||||
"coins-ledger",
|
"coins-ledger",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"eth-keystore",
|
"eth-keystore",
|
||||||
|
"ethers-contract",
|
||||||
"ethers-core",
|
"ethers-core",
|
||||||
|
"ethers-derive-eip712",
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"rand 0.8.4",
|
"rand 0.8.4",
|
||||||
"rusoto_core",
|
"rusoto_core",
|
||||||
"rusoto_kms",
|
"rusoto_kms",
|
||||||
|
"semver 1.0.4",
|
||||||
|
"serde_json",
|
||||||
"sha2 0.9.8",
|
"sha2 0.9.8",
|
||||||
"spki",
|
"spki",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
|
@ -660,7 +660,7 @@ mod eth_tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
let sig = wallet
|
let sig = wallet
|
||||||
.sign_typed_data(foo_bar.clone())
|
.sign_typed_data(&foo_bar)
|
||||||
.await
|
.await
|
||||||
.expect("failed to sign typed data");
|
.expect("failed to sign typed data");
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ rand = { version = "0.8.4", default-features = false }
|
||||||
yubihsm = { version = "0.39.0", features = ["secp256k1", "http", "usb"], optional = true }
|
yubihsm = { version = "0.39.0", features = ["secp256k1", "http", "usb"], optional = true }
|
||||||
futures-util = "0.3.17"
|
futures-util = "0.3.17"
|
||||||
futures-executor = "0.3.17"
|
futures-executor = "0.3.17"
|
||||||
|
semver = "1.0.4"
|
||||||
|
|
||||||
# aws
|
# aws
|
||||||
rusoto_core = { version = "0.47.0", optional = true }
|
rusoto_core = { version = "0.47.0", optional = true }
|
||||||
|
@ -39,6 +40,9 @@ spki = { version = "0.4.1", optional = true }
|
||||||
eth-keystore = { version = "0.3.0" }
|
eth-keystore = { version = "0.3.0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
ethers-contract = { version = "^0.5.0", path = "../ethers-contract", features = ["eip712", "abigen"]}
|
||||||
|
ethers-derive-eip712 = { version = "0.1.0", path = "../ethers-core/ethers-derive-eip712" }
|
||||||
|
serde_json = { version = "1.0.64" }
|
||||||
tracing-subscriber = "0.2.25"
|
tracing-subscriber = "0.2.25"
|
||||||
yubihsm = { version = "0.39.0", features = ["secp256k1", "usb", "mockhsm"] }
|
yubihsm = { version = "0.39.0", features = ["secp256k1", "usb", "mockhsm"] }
|
||||||
|
|
||||||
|
|
|
@ -256,7 +256,7 @@ impl<'a> super::Signer for AwsSigner<'a> {
|
||||||
|
|
||||||
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
payload: T,
|
payload: &T,
|
||||||
) -> Result<EthSig, Self::Error> {
|
) -> Result<EthSig, Self::Error> {
|
||||||
let hash = payload
|
let hash = payload
|
||||||
.encode_eip712()
|
.encode_eip712()
|
||||||
|
|
|
@ -8,8 +8,8 @@ use futures_util::lock::Mutex;
|
||||||
|
|
||||||
use ethers_core::{
|
use ethers_core::{
|
||||||
types::{
|
types::{
|
||||||
transaction::eip2718::TypedTransaction, Address, NameOrAddress, Signature, Transaction,
|
transaction::{eip2718::TypedTransaction, eip712::Eip712},
|
||||||
TransactionRequest, TxHash, H256, U256,
|
Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256,
|
||||||
},
|
},
|
||||||
utils::keccak256,
|
utils::keccak256,
|
||||||
};
|
};
|
||||||
|
@ -29,6 +29,8 @@ pub struct LedgerEthereum {
|
||||||
pub(crate) address: Address,
|
pub(crate) address: Address,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EIP712_MIN_VERSION: &str = ">=1.6.0";
|
||||||
|
|
||||||
impl LedgerEthereum {
|
impl LedgerEthereum {
|
||||||
/// Instantiate the application by acquiring a lock on the ledger device.
|
/// Instantiate the application by acquiring a lock on the ledger device.
|
||||||
///
|
///
|
||||||
|
@ -136,7 +138,38 @@ impl LedgerEthereum {
|
||||||
self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, payload).await
|
self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, payload).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for signing either transaction data or personal messages
|
/// Signs an EIP712 encoded domain separator and message
|
||||||
|
pub async fn sign_typed_struct<T>(&self, payload: &T) -> Result<Signature, LedgerError>
|
||||||
|
where
|
||||||
|
T: Eip712,
|
||||||
|
{
|
||||||
|
// See comment for v1.6.0 requirement
|
||||||
|
// https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999
|
||||||
|
let req = semver::VersionReq::parse(EIP712_MIN_VERSION)?;
|
||||||
|
let version = semver::Version::parse(&self.version().await?)?;
|
||||||
|
|
||||||
|
// Enforce app version is greater than EIP712_MIN_VERSION
|
||||||
|
if !req.matches(&version) {
|
||||||
|
return Err(LedgerError::UnsupportedAppVersion(
|
||||||
|
EIP712_MIN_VERSION.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain_separator = payload
|
||||||
|
.domain_separator()
|
||||||
|
.map_err(|e| LedgerError::Eip712Error(e.to_string()))?;
|
||||||
|
let struct_hash = payload
|
||||||
|
.struct_hash()
|
||||||
|
.map_err(|e| LedgerError::Eip712Error(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut payload = Self::path_to_bytes(&self.derivation);
|
||||||
|
payload.extend_from_slice(&domain_separator);
|
||||||
|
payload.extend_from_slice(&struct_hash);
|
||||||
|
|
||||||
|
self.sign_payload(INS::SIGN_ETH_EIP_712, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for signing either transaction data, personal messages or EIP712 derived structs
|
||||||
pub async fn sign_payload(
|
pub async fn sign_payload(
|
||||||
&self,
|
&self,
|
||||||
command: INS,
|
command: INS,
|
||||||
|
@ -200,9 +233,30 @@ impl LedgerEthereum {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Signer;
|
use crate::Signer;
|
||||||
use ethers_core::types::{Address, TransactionRequest};
|
use ethers_contract::EthAbiType;
|
||||||
|
use ethers_core::types::{
|
||||||
|
transaction::eip712::Eip712, Address, TransactionRequest, I256, U256,
|
||||||
|
};
|
||||||
|
use ethers_derive_eip712::*;
|
||||||
use std::str::FromStr;
|
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]
|
#[tokio::test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
// Replace this with your ETH addresses.
|
// Replace this with your ETH addresses.
|
||||||
|
@ -269,4 +323,28 @@ mod tests {
|
||||||
let addr = ledger.get_address().await.unwrap();
|
let addr = ledger.get_address().await.unwrap();
|
||||||
sig.verify(message, addr).unwrap();
|
sig.verify(message, addr).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_sign_eip712_struct() {
|
||||||
|
let ledger = LedgerEthereum::new(DerivationType::LedgerLive(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 = ledger
|
||||||
|
.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, ledger.address).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,17 +27,12 @@ impl Signer for LedgerEthereum {
|
||||||
self.sign_tx(message).await
|
self.sign_tx(message).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Signs a EIP712 derived struct
|
||||||
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
payload: T,
|
payload: &T,
|
||||||
) -> Result<Signature, Self::Error> {
|
) -> Result<Signature, Self::Error> {
|
||||||
let hash = payload
|
self.sign_typed_struct(payload).await
|
||||||
.encode_eip712()
|
|
||||||
.map_err(|e| Self::Error::Eip712Error(e.to_string()))?;
|
|
||||||
|
|
||||||
let sig = self.sign_message(hash).await?;
|
|
||||||
|
|
||||||
Ok(sig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the signer's Ethereum Address
|
/// Returns the signer's Ethereum Address
|
||||||
|
|
|
@ -42,9 +42,15 @@ pub enum LedgerError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
/// Error when converting from a hex string
|
/// Error when converting from a hex string
|
||||||
HexError(#[from] hex::FromHexError),
|
HexError(#[from] hex::FromHexError),
|
||||||
|
#[error(transparent)]
|
||||||
|
/// Error when converting a semver requirement
|
||||||
|
SemVerError(#[from] semver::Error),
|
||||||
/// Error type from Eip712Error message
|
/// Error type from Eip712Error message
|
||||||
#[error("error encoding eip712 struct: {0:?}")]
|
#[error("error encoding eip712 struct: {0:?}")]
|
||||||
Eip712Error(String),
|
Eip712Error(String),
|
||||||
|
/// Error when signing EIP712 struct with not compatible Ledger ETH app
|
||||||
|
#[error("Ledger ethereum app requires at least version: {0:?}")]
|
||||||
|
UnsupportedAppVersion(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const P1_FIRST: u8 = 0x00;
|
pub const P1_FIRST: u8 = 0x00;
|
||||||
|
@ -57,6 +63,7 @@ pub enum INS {
|
||||||
SIGN = 0x04,
|
SIGN = 0x04,
|
||||||
GET_APP_CONFIGURATION = 0x06,
|
GET_APP_CONFIGURATION = 0x06,
|
||||||
SIGN_PERSONAL_MESSAGE = 0x08,
|
SIGN_PERSONAL_MESSAGE = 0x08,
|
||||||
|
SIGN_ETH_EIP_712 = 0x0C,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
|
|
|
@ -99,7 +99,7 @@ pub trait Signer: std::fmt::Debug + Send + Sync {
|
||||||
/// Payload must implement Eip712 trait.
|
/// Payload must implement Eip712 trait.
|
||||||
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
payload: T,
|
payload: &T,
|
||||||
) -> Result<Signature, Self::Error>;
|
) -> Result<Signature, Self::Error>;
|
||||||
|
|
||||||
/// Returns the signer's Ethereum Address
|
/// Returns the signer's Ethereum Address
|
||||||
|
|
|
@ -88,7 +88,7 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
|
||||||
|
|
||||||
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
payload: T,
|
payload: &T,
|
||||||
) -> Result<Signature, Self::Error> {
|
) -> Result<Signature, Self::Error> {
|
||||||
let encoded = payload
|
let encoded = payload
|
||||||
.encode_eip712()
|
.encode_eip712()
|
||||||
|
|
Loading…
Reference in New Issue