Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions Cargo.lock

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

11 changes: 4 additions & 7 deletions crates/eip712-account-claim/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ version = "0.1.0"
edition = "2021"

[dependencies]
sp-core-hashing-proc-macro = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" }
sp-io = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" }
eip712-common = { version = "0.1", path = "../eip712-common", default-features = false }

[dev-dependencies]
eth-eip-712 = { package = "eip-712", version = "=0.1.0" }
eip712-common-test-utils = { version = "0.1", path = "../eip712-common-test-utils" }

hex-literal = "0.3"
secp256k1 = "0.22"
serde_json = "1"
sp-core = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" }

[features]
default = ["std"]
std = ["sp-core/std", "sp-io/std"]
std = ["eip712-common/std"]
158 changes: 20 additions & 138 deletions crates/eip712-account-claim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,13 @@

#![cfg_attr(not(feature = "std"), no_std)]

use sp_core_hashing_proc_macro::keccak_256 as const_keccak_256;
use sp_io::hashing::keccak_256;
use eip712_common::{
const_keccak_256, keccak_256, Domain, EcdsaSignature, EthBytes, EthereumAddress,
};

/// EIP712Domain typehash.
const EIP712_DOMAIN_TYPEHASH: [u8; 32] = const_keccak_256!(
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
/// Account claim typehash.
const ACCOUNT_CLAIM_TYPEHASH: [u8; 32] = const_keccak_256!(b"Claim(bytes substrateAddress)");

/// A type alias representing a `string` solidity type.
type EthString = str;
/// A type alias representing a `uint256` solidity type.
type EthUint265 = [u8; 32];
/// A type alias representing an `address` solidity type.
type EthAddress = [u8; 20];
/// A type alias representing the `bytes` solidity type.
type EthBytes = [u8];

/// A first number of an EIP191 message.
const EIP191_MAGIC_BYTE: u8 = 0x19;
/// The EIP191 version for the EIP-712 structured data.
const EIP191_VERSION_STRUCTURED_DATA: u8 = 0x01;

/// Prepare a hash for the whole EIP-712 message.
fn make_eip712_message_hash(domain_separator: &[u8; 32], payload_hash: &[u8; 32]) -> [u8; 32] {
let mut msg: [u8; 66] = [0; 66];
msg[0] = EIP191_MAGIC_BYTE;
msg[1] = EIP191_VERSION_STRUCTURED_DATA;
msg[2..34].copy_from_slice(domain_separator);
msg[34..66].copy_from_slice(payload_hash);
keccak_256(&msg)
}

/// The EIP712 domain.
pub struct Domain<'a> {
/// The name of the domain.
pub name: &'a EthString,
/// The version of the domain.
/// Bump this value if you need to make the old signed messages obsolete.
pub version: &'a EthString,
/// The Chain ID of the Ethereum chain this code runs at.
pub chain_id: &'a EthUint265,
/// The verifying contract, indeteded for the address of the contract that will be verifying
/// the signature.
pub verifying_contract: &'a EthAddress,
}

/// Prepare a hash for EIP712Domain data type.
fn make_domain_hash(domain: Domain<'_>) -> [u8; 32] {
let mut buf = [0u8; 160];
buf[0..32].copy_from_slice(&EIP712_DOMAIN_TYPEHASH);
buf[32..64].copy_from_slice(&keccak_256(domain.name.as_bytes()));
buf[64..96].copy_from_slice(&keccak_256(domain.version.as_bytes()));
buf[96..128].copy_from_slice(domain.chain_id);
buf[140..160].copy_from_slice(domain.verifying_contract);
keccak_256(&buf)
}

/// Prepare a hash for our account claim data type.
/// To be used at EIP-712 message payload.
fn make_account_claim_hash(account: &EthBytes) -> [u8; 32] {
Expand All @@ -70,63 +18,28 @@ fn make_account_claim_hash(account: &EthBytes) -> [u8; 32] {
keccak_256(&buf)
}

/// A signature (a 512-bit value, plus 8 bits for recovery ID).
pub type Signature = [u8; 65];

/// Verify EIP-712 typed signature based on provided domain_separator and entire message.
pub fn verify_account_claim(
signature: &EcdsaSignature,
domain: Domain<'_>,
account: &[u8],
signature: Signature,
) -> Option<[u8; 20]> {
let domain_hash = make_domain_hash(domain);
let account_claim_hash = make_account_claim_hash(account);
let msg_hash = make_eip712_message_hash(&domain_hash, &account_claim_hash);
recover_signer(signature, &msg_hash)
}

/// Extract the signer address from the signatue and the message.
fn recover_signer(sig: Signature, msg: &[u8; 32]) -> Option<[u8; 20]> {
let pubkey = sp_io::crypto::secp256k1_ecdsa_recover(&sig, msg).ok()?;
Some(ecdsa_public_key_to_evm_address(&pubkey))
}

/// Convert the ECDSA public key to EVM address.
fn ecdsa_public_key_to_evm_address(pubkey: &[u8; 64]) -> [u8; 20] {
let mut address = [0u8; 20];
address.copy_from_slice(&sp_io::hashing::keccak_256(pubkey)[12..]);
address
) -> Option<EthereumAddress> {
let payload_hash = make_account_claim_hash(account);
eip712_common::verify_signature(signature, domain, &payload_hash)
}

#[cfg(test)]
mod tests {
use eth_eip_712::{hash_structured_data, EIP712};
use eip712_common_test_utils::{
ecdsa, ecdsa_pair, ecdsa_sign_typed_data, ethereum_address_from_seed, U256,
};
use hex_literal::hex;
use sp_core::{crypto::Pair, ecdsa, U256};

use super::*;

fn ecdsa_pair(seed: &[u8]) -> ecdsa::Pair {
ecdsa::Pair::from_seed(&keccak_256(seed))
}

fn ecdsa_sign(pair: &ecdsa::Pair, msg: [u8; 32]) -> Signature {
pair.sign_prehashed(&msg).0
}

fn evm_address_from_seed(seed: &[u8]) -> [u8; 20] {
use secp256k1::{PublicKey, Secp256k1, SecretKey};
let secret = SecretKey::from_slice(&keccak_256(seed)).unwrap();
let context = Secp256k1::signing_only();
let public = PublicKey::from_secret_key(&context, &secret);
let mut public_bytes = [0u8; 64];
public_bytes.copy_from_slice(&public.serialize_uncompressed()[1..]);
ecdsa_public_key_to_evm_address(&public_bytes)
}

// A helper function to construct test EIP-712 signature.
fn test_input(pair: &ecdsa::Pair) -> Signature {
let claim_eip_712_json = r#"{
fn test_input(pair: &ecdsa::Pair) -> EcdsaSignature {
let typed_data_json = r#"{
"primaryType": "Claim",
"domain": {
"name": "Humanode EVM Claim",
Expand All @@ -149,10 +62,7 @@ mod tests {
]
}
}"#;
let typed_data: EIP712 = serde_json::from_str(claim_eip_712_json).unwrap();
let msg_bytes: [u8; 32] = hash_structured_data(typed_data).unwrap().into();

ecdsa_sign(pair, msg_bytes)
ecdsa_sign_typed_data(pair, typed_data_json)
}

// A helper function to prepare alice account claim typed data.
Expand All @@ -166,35 +76,6 @@ mod tests {
}
}

#[test]
fn verify_test_chain_id() {
let domain = prepare_sample_domain();
let expected_chain_id: [u8; 32] = U256::from(5234).into();
assert_eq!(domain.chain_id, &expected_chain_id);
}

#[test]
fn verify_domain_separator() {
// See https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol

// From https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol#L101
let sample_separator: [u8; 32] =
U256::from("0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f").into();

// Sample test data
// See https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol#L38-L44
let verifying_contract: [u8; 20] = hex!("CcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC");
let chain_id: [u8; 32] = U256::from("0x1").into();
let domain = Domain {
name: "Ether Mail",
version: "1",
chain_id: &chain_id,
verifying_contract: &verifying_contract,
};

assert_eq!(make_domain_hash(domain), sample_separator);
}

const SAMPLE_ACCOUNT: [u8; 32] =
hex!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d");

Expand All @@ -204,8 +85,8 @@ mod tests {
let signature = test_input(&pair);
let domain = prepare_sample_domain();

let evm_address = verify_account_claim(domain, &SAMPLE_ACCOUNT, signature).unwrap();
assert_eq!(evm_address, evm_address_from_seed(b"Alice"));
let ethereum_address = verify_account_claim(&signature, domain, &SAMPLE_ACCOUNT).unwrap();
assert_eq!(ethereum_address, ethereum_address_from_seed(b"Alice"));
}

#[test]
Expand All @@ -214,8 +95,8 @@ mod tests {
let signature = test_input(&pair);
let domain = prepare_sample_domain();

let evm_address = verify_account_claim(domain, &SAMPLE_ACCOUNT, signature).unwrap();
assert_ne!(evm_address, evm_address_from_seed(b"Bob"));
let ethereum_address = verify_account_claim(&signature, domain, &SAMPLE_ACCOUNT).unwrap();
assert_ne!(ethereum_address, ethereum_address_from_seed(b"Bob"));
}

#[test]
Expand All @@ -231,9 +112,10 @@ mod tests {
hex!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d");
let signature = hex!("151d5f52e6c249db84b8705374c6f51dd08b50ddad5b1175ec20a7e00cbc48f55a23470ab0db16146b3b7d2a8565aaf2b700f548c9e9882a0034e654bd214e821b");

let evm_address = verify_account_claim(domain, &account_to_claim, signature).unwrap();
let ethereum_address =
verify_account_claim(&EcdsaSignature(signature), domain, &account_to_claim).unwrap();
assert_eq!(
evm_address,
ethereum_address.0,
hex!("e9726f3d0a7736034e2a4c63ea28b3ab95622cb9"),
);
}
Expand Down
14 changes: 14 additions & 0 deletions crates/eip712-common-test-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "eip712-common-test-utils"
version = "0.1.0"
edition = "2021"

[dependencies]
eip712-common = { version = "0.1", path = "../eip712-common" }
primitives-ethereum = { version = "0.1", path = "../primitives-ethereum" }

eth-eip-712 = { package = "eip-712", version = "=0.1.0" }
hex-literal = "0.3"
secp256k1 = "0.22"
serde_json = "1"
sp-core = { git = "https://github.com/humanode-network/substrate", branch = "master" }
40 changes: 40 additions & 0 deletions crates/eip712-common-test-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Common test utils for EIP-712 typed data message construction and signature verification.

use eip712_common::*;
use primitives_ethereum::{EcdsaSignature, EthereumAddress};
pub use sp_core::{crypto::Pair, ecdsa, H256, U256};

/// Create a new ECDSA keypair from the seed.
pub fn ecdsa_pair(seed: &[u8]) -> ecdsa::Pair {
ecdsa::Pair::from_seed(&keccak_256(seed))
}

/// Sign a given message with the given ECDSA keypair.
pub fn ecdsa_sign(pair: &ecdsa::Pair, msg: [u8; 32]) -> EcdsaSignature {
EcdsaSignature(pair.sign_prehashed(&msg).0)
}

/// Sign a given EIP-712 typed data JSON with the given ECDSA keypair.
pub fn ecdsa_sign_typed_data(pair: &ecdsa::Pair, type_data_json: &str) -> EcdsaSignature {
let typed_data: eth_eip_712::EIP712 = serde_json::from_str(type_data_json).unwrap();
let msg_bytes: [u8; 32] = eth_eip_712::hash_structured_data(typed_data)
.unwrap()
.into();
ecdsa_sign(pair, msg_bytes)
}

/// Create an Ethereum address from the given seed.
///
/// This algorithm will return the addresses corresponding to the [`ecdsa::Pair`]s generated
/// by [`ecdsa_pair`] with the same `seed`.
pub fn ethereum_address_from_seed(seed: &[u8]) -> EthereumAddress {
use secp256k1::{PublicKey, Secp256k1, SecretKey};
let secret = SecretKey::from_slice(&keccak_256(seed)).unwrap();
let context = Secp256k1::signing_only();
let public = PublicKey::from_secret_key(&context, &secret);
let mut public_bytes = [0u8; 64];
public_bytes.copy_from_slice(&public.serialize_uncompressed()[1..]);
let mut address = [0u8; 20];
address.copy_from_slice(&keccak_256(&public_bytes)[12..]);
EthereumAddress(address)
}
19 changes: 19 additions & 0 deletions crates/eip712-common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "eip712-common"
version = "0.1.0"
edition = "2021"

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

sp-core-hashing-proc-macro = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" }
sp-io = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" }
sp-std = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" }

[dev-dependencies]
hex-literal = "0.3"
sp-core = { git = "https://github.com/humanode-network/substrate", branch = "master" }

[features]
default = ["std"]
std = ["primitives-ethereum/std", "sp-io/std", "sp-std/std"]
Loading