Skip to content

Commit

Permalink
Implement manual signing support for token claims (#483)
Browse files Browse the repository at this point in the history
* Implement eth personal sign common logic

* Rename to eip191-common

* Use no_std hash calculation for eip191

* Prepare hardcoded eip191-token-claim message builder

* Add eip191 logic to token claim verifier at runtime

* Update docs

* Update dependencies

* Fix cargo doc

* Proper message preparing for token claim

* Add real world test case

* Fix cargo toml

* Proper keccak_256 usage

* Use primitives-ethereum deps at test

* Rename eip712 to eip at runtime

* Add comment to test case

* Update comment for TokenClaimVerifier

* Rename eip ti eth_sig

* Proper import at benchmarking

* Fix cargo toml

* Rename mod deps at benchmarking
  • Loading branch information
dmitrylavrenov authored Oct 3, 2022
1 parent b01b196 commit eba34f1
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 13 deletions.
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions crates/eip191-crypto/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "eip191-crypto"
version = "0.1.0"
edition = "2021"

[dependencies]
primitives-ethereum = { version = "0.1", path = "../primitives-ethereum", default-features = false }

numtoa = { version = "0.2", default-features = false }
sp-core-hashing-proc-macro = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/humanode-2022-09-21" }
sp-io = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/humanode-2022-09-21" }
sp-std = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/humanode-2022-09-21" }

[dev-dependencies]
hex = "0.4"

[features]
default = ["std"]
std = ["primitives-ethereum/std", "sp-io/std", "sp-std/std"]
97 changes: 97 additions & 0 deletions crates/eip191-crypto/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! Common logic for EIP-191 message construction and signature verification.
#![cfg_attr(not(feature = "std"), no_std)]

use numtoa::NumToA;
use primitives_ethereum::{EcdsaSignature, EthereumAddress};
use sp_io::hashing::keccak_256;
use sp_std::{vec, vec::Vec};

/// Extract the signer address from the signature and the message.
pub fn recover_signer(sig: &EcdsaSignature, message: &[u8]) -> Option<EthereumAddress> {
let msg_hash = make_personal_message_hash(message);
let pubkey = sp_io::crypto::secp256k1_ecdsa_recover(&sig.0, &msg_hash).ok()?;
Some(ecdsa_public_key_to_ethereum_address(&pubkey))
}

/// Prepare the eth personal sign message.
fn make_personal_message_hash(message: &[u8]) -> [u8; 32] {
let mut buf = vec![];
buf.extend_from_slice("\x19Ethereum Signed Message:\n".as_bytes());
buf.extend_from_slice(usize_as_string_bytes(message.len()).as_slice());
buf.extend_from_slice(message);
keccak_256(&buf)
}

/// A helper function to represent message len as string bytes.
///
/// <https://crates.io/crates/numtoa>.
fn usize_as_string_bytes(message_len: usize) -> Vec<u8> {
let mut buffer = [0u8; 20];
message_len.numtoa(10, &mut buffer).to_vec()
}

/// Convert the ECDSA public key to Ethereum address.
fn ecdsa_public_key_to_ethereum_address(pubkey: &[u8; 64]) -> EthereumAddress {
let mut address = [0u8; 20];
address.copy_from_slice(&keccak_256(pubkey)[12..]);
EthereumAddress(address)
}

#[cfg(test)]
mod tests {
use super::*;

/// This test contains the data obtained from Metamask/eth-sig-util.
///
/// https://github.com/MetaMask/eth-sig-util/blob/8a470650074174f5338308d2acbd97caf5542434/src/personal-sign.test.ts#L88
#[test]
fn valid_signature() {
let message = "hello world";
let hex_signature = "0xce909e8ea6851bc36c007a0072d0524b07a3ff8d4e623aca4c71ca8e57250c4d0a3fc38fa8fbaaa81ead4b9f6bd03356b6f8bf18bccad167d78891636e1d69561b";
let expected_address = "0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb";

let address = recover_signer(
&EcdsaSignature(
hex::decode(&hex_signature[2..])
.unwrap()
.try_into()
.unwrap(),
),
message.as_bytes(),
);

assert_eq!(
format!("0x{}", hex::encode(address.unwrap().0)),
expected_address
);
}

/// This test contains the data obtained from MetaMask browser extension via an injected web3
/// interface using personal_sign API.
///
/// https://metamask.github.io/test-dapp/.
///
/// It validates that the real-world external ecosystem works properly with our code.
#[test]
fn real_world_case() {
let message = "Example `personal_sign` message";
let hex_signature = "0xbef8374833e572271b2f17d233a8e03c53c8f35e451cd33494793bbdc036f1d72dd955c0628483bc50bd3f7849d1d730a69cdd9775ab3eed556b87eaa20426511b";
let expected_address = "0xc16fb04cbc2c946399772688c33d9bb6ae6ac71b";

let address = recover_signer(
&EcdsaSignature(
hex::decode(&hex_signature[2..])
.unwrap()
.try_into()
.unwrap(),
),
message.as_bytes(),
);

assert_eq!(
format!("0x{}", hex::encode(address.unwrap().0)),
expected_address
);
}
}
18 changes: 18 additions & 0 deletions crates/eip191-token-claim/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "eip191-token-claim"
version = "0.1.0"
edition = "2021"

[dependencies]
hex = { version = "0.4", default-features = false }
sp-std = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/humanode-2022-09-21" }

[dev-dependencies]
eip191-crypto = { version = "0.1", path = "../eip191-crypto" }
primitives-ethereum = { version = "0.1", path = "../primitives-ethereum" }

hex-literal = "0.3"

[features]
default = ["std"]
std = ["hex/std", "sp-std/std"]
63 changes: 63 additions & 0 deletions crates/eip191-token-claim/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! EIP-191 token claim message builder.
#![cfg_attr(not(feature = "std"), no_std)]

use sp_std::{vec, vec::Vec};

/// Token claim message.
pub struct Message<'a> {
/// Substrate address.
pub substrate_address: &'a [u8; 32],
/// Genesis hash.
pub genesis_hash: &'a [u8; 32],
}

impl<'a> Message<'a> {
/// Prepare EIP-191 token claim message.
pub fn prepare_message(&self) -> Vec<u8> {
let mut buf = vec![];
buf.extend_from_slice("I hereby sign that I claim HMND to 0x".as_bytes());
buf.extend_from_slice(hex::encode(self.substrate_address).as_bytes());
buf.extend_from_slice(" on network with genesis 0x".as_bytes());
buf.extend_from_slice(hex::encode(self.genesis_hash).as_bytes());
buf.extend_from_slice(".".as_bytes());
buf
}
}

#[cfg(test)]
mod tests {

use hex_literal::hex;
use primitives_ethereum::EcdsaSignature;

use super::*;

/// This test contains the data obtained from MetaMask browser extension via an injected web3
/// interface.
/// It validates that the real-world external ecosystem works properly with our code.
#[test]
fn real_world_case() {
let substrate_address =
hex!("1e38cdd099576380ca4df726fa8b740d3ae6b159e71cd5ef7aa621f5bd01d653");
let genesis_hash = hex!("bed15072ffa35432da5d20c33920b3afc2ab850e864a26b684e7f6caed6a1479");

let token_claim_message = Message {
substrate_address: &substrate_address,
genesis_hash: &genesis_hash,
};

let signature = hex!("f76b13746bb661fb6a1242b5591d4442a88a09c1600c5ccb77e7083f37b7d17e6b975bbdd88e186870fcadd464dcff0a4b4f6d32e4a51291d4b1f543ea588ae11c");

let ethereum_address = eip191_crypto::recover_signer(
&EcdsaSignature(signature),
token_claim_message.prepare_message().as_slice(),
)
.unwrap();

assert_eq!(
ethereum_address.0,
hex!("f24ff3a9cf04c71dbc94d0b566f7a27b94566cac"),
);
}
}
6 changes: 6 additions & 0 deletions crates/humanode-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ substrate-wasm-builder = { git = "https://github.com/humanode-network/substrate"
author-ext-api = { version = "0.1", path = "../author-ext-api", default-features = false }
bioauth-flow-api = { version = "0.1", path = "../bioauth-flow-api", default-features = false }
crypto-utils = { version = "0.1", path = "../crypto-utils", default-features = false, optional = true }
eip191-crypto = { version = "0.1", path = "../eip191-crypto", default-features = false }
eip191-token-claim = { version = "0.1", path = "../eip191-token-claim", default-features = false }
eip712-account-claim = { version = "0.1", path = "../eip712-account-claim", default-features = false }
eip712-common = { version = "0.1", path = "../eip712-common", default-features = false }
eip712-common-test-utils = { version = "0.1", path = "../eip712-common-test-utils", default-features = false, optional = true }
Expand All @@ -31,6 +33,7 @@ pallet-vesting = { version = "0.1", path = "../pallet-vesting", default-features
precompile-bioauth = { version = "0.1", path = "../precompile-bioauth", default-features = false }
precompile-evm-accounts-mapping = { version = "0.1", path = "../precompile-evm-accounts-mapping", default-features = false }
primitives-auth-ticket = { version = "0.1", path = "../primitives-auth-ticket", default-features = false }
primitives-ethereum = { version = "0.1", path = "../primitives-ethereum", default-features = false }
robonode-crypto = { version = "0.1", path = "../robonode-crypto", default-features = false }
vesting-schedule-linear = { version = "0.1", path = "../vesting-schedule-linear", default-features = false }
vesting-scheduling-timestamp = { version = "0.1", path = "../vesting-scheduling-timestamp", default-features = false }
Expand Down Expand Up @@ -117,13 +120,16 @@ runtime-benchmarks = [
std = [
"author-ext-api/std",
"bioauth-flow-api/std",
"eip191-crypto/std",
"eip191-token-claim/std",
"eip712-account-claim/std",
"eip712-common/std",
"eip712-token-claim/std",
"keystore-bioauth-account-id/std",
"precompile-bioauth/std",
"precompile-evm-accounts-mapping/std",
"primitives-auth-ticket/std",
"primitives-ethereum/std",
"chrono/std",
"codec/std",
"scale-info/std",
Expand Down
7 changes: 4 additions & 3 deletions crates/humanode-runtime/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
//! The benchmarking utilities.
use eip712_common::{keccak_256, EcdsaSignature, EthereumAddress};
use eip712_common::keccak_256;
use frame_support::{
dispatch::DispatchResult,
traits::{OnFinalize, OnInitialize},
};
use primitives_ethereum::{EcdsaSignature, EthereumAddress};
use sp_runtime::traits::{One, Zero};

use super::*;
Expand Down Expand Up @@ -74,8 +75,8 @@ impl pallet_token_claims::benchmarking::Interface for Runtime {
panic!("bad ethereum address");
}

let chain_id: [u8; 32] = U256::from(crate::eip712::ETHEREUM_MAINNET_CHAIN_ID).into();
let verifying_contract = crate::eip712::genesis_verifying_contract();
let chain_id: [u8; 32] = U256::from(crate::eth_sig::ETHEREUM_MAINNET_CHAIN_ID).into();
let verifying_contract = crate::eth_sig::genesis_verifying_contract();
let domain = eip712_common::Domain {
name: "Humanode Token Claim",
version: "1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Various EIP-712 implementations.
//! Various EIP-712 and EIP-191 related implementations.
use eip712_common::{EcdsaSignature, EthereumAddress};
use primitives_ethereum::{EcdsaSignature, EthereumAddress};

use super::*;

Expand Down Expand Up @@ -30,7 +30,7 @@ impl pallet_evm_accounts_mapping::SignedClaimVerifier for AccountClaimVerifier {
}
}

/// The verifier for the EIP-712 signature of the token claim message.
/// The verifier for the EIP-712 and EIP-191 signatures of the token claim message.
pub enum TokenClaimVerifier {}

pub(crate) const ETHEREUM_MAINNET_CHAIN_ID: u32 = 1;
Expand All @@ -50,6 +50,22 @@ impl pallet_token_claims::traits::EthereumSignatureVerifier for TokenClaimVerifi
chain_id: &chain_id,
verifying_contract: &verifying_contract,
};
eip712_token_claim::recover_signer(signature, domain, message_params.account_id.as_ref())
if let Some(ethereum_address) = eip712_token_claim::recover_signer(
signature,
domain,
message_params.account_id.as_ref(),
) {
if ethereum_address == message_params.ethereum_address {
return Some(ethereum_address);
}
}

let genesis_hash: [u8; 32] = System::block_hash(0).into();
let eip191_message = eip191_token_claim::Message {
substrate_address: message_params.account_id.as_ref(),
genesis_hash: &genesis_hash,
};

eip191_crypto::recover_signer(signature, eip191_message.prepare_message().as_slice())
}
}
Loading

0 comments on commit eba34f1

Please sign in to comment.