Skip to content

Commit c4a583c

Browse files
authored
Merge pull request #790 from CosmWasm/recover_pubkey
Add pubkey recovery API
2 parents 17d1378 + 7eb303d commit c4a583c

26 files changed

+892
-26
lines changed

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ nightly toolchain installed as well.
316316

317317
```sh
318318
cargo fmt \
319+
&& (cd packages/crypto && cargo build && cargo test && cargo clippy -- -D warnings) \
319320
&& (cd packages/std && cargo wasm-debug --features iterator && cargo test --features iterator && cargo clippy --features iterator -- -D warnings && cargo schema) \
320321
&& (cd packages/storage && cargo build && cargo test --features iterator && cargo clippy --features iterator -- -D warnings) \
321322
&& (cd packages/schema && cargo build && cargo test && cargo clippy -- -D warnings) \

contracts/crypto-verify/Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contracts/crypto-verify/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ backtraces = ["cosmwasm-std/backtraces", "cosmwasm-vm/backtraces"]
3434
[dependencies]
3535
cosmwasm-std = { path = "../../packages/std", features = ["iterator"] }
3636
cosmwasm-storage = { path = "../../packages/storage", features = ["iterator"] }
37+
hex = "0.4"
3738
schemars = "0.7"
3839
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
3940
sha2 = "0.9"
41+
sha3 = "0.9"
4042

4143
[dev-dependencies]
4244
cosmwasm-vm = { path = "../../packages/vm", default-features = false, features = ["iterator"] }
4345
cosmwasm-schema = { path = "../../packages/schema" }
44-
hex = "0.4"

contracts/crypto-verify/schema/query_msg.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,41 @@
4545
}
4646
}
4747
},
48+
{
49+
"description": "Ethereum text verification (compatible to the eth_sign RPC/web3 enpoint). This cannot be used to verify transaction.\n\nSee https://web3js.readthedocs.io/en/v1.2.0/web3-eth.html#sign",
50+
"type": "object",
51+
"required": [
52+
"verify_ethereum_text"
53+
],
54+
"properties": {
55+
"verify_ethereum_text": {
56+
"type": "object",
57+
"required": [
58+
"message",
59+
"signature",
60+
"signer_address"
61+
],
62+
"properties": {
63+
"message": {
64+
"description": "Message to verify. This will be wrapped in the standard container `\"\\x19Ethereum Signed Message:\\n\" + len(message) + message` before verification.",
65+
"type": "string"
66+
},
67+
"signature": {
68+
"description": "Serialized signature. Fixed length format (64 bytes `r` and `s` plus the one byte `v`).",
69+
"allOf": [
70+
{
71+
"$ref": "#/definitions/Binary"
72+
}
73+
]
74+
},
75+
"signer_address": {
76+
"description": "Signer address. This is matched case insensitive, so you can provide checksummed and non-checksummed addresses. Checksums are not validated.",
77+
"type": "string"
78+
}
79+
}
80+
}
81+
}
82+
},
4883
{
4984
"description": "Tendermint format (ed25519 verification scheme).",
5085
"type": "object",

contracts/crypto-verify/src/contract.rs

Lines changed: 161 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use sha2::{Digest, Sha256};
2-
31
use cosmwasm_std::{
4-
entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdResult,
2+
entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError,
3+
StdResult,
54
};
5+
use sha2::{Digest, Sha256};
6+
use sha3::Keccak256;
67

78
use crate::msg::{
89
list_verifications, HandleMsg, InitMsg, ListVerificationsResponse, QueryMsg, VerifyResponse,
@@ -38,6 +39,16 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<QueryResponse> {
3839
&signature.0,
3940
&public_key.0,
4041
)?),
42+
QueryMsg::VerifyEthereumText {
43+
message,
44+
signature,
45+
signer_address,
46+
} => to_binary(&query_verify_ethereum_text(
47+
deps,
48+
&message,
49+
&signature,
50+
&signer_address,
51+
)?),
4152
QueryMsg::VerifyTendermintSignature {
4253
message,
4354
signature,
@@ -62,7 +73,41 @@ pub fn query_verify_cosmos(
6273
let hash = Sha256::digest(message);
6374

6475
// Verification
65-
let result = deps.api.secp256k1_verify(&*hash, signature, public_key);
76+
let result = deps
77+
.api
78+
.secp256k1_verify(hash.as_ref(), signature, public_key);
79+
match result {
80+
Ok(verifies) => Ok(VerifyResponse { verifies }),
81+
Err(err) => Err(err.into()),
82+
}
83+
}
84+
85+
pub fn query_verify_ethereum_text(
86+
deps: Deps,
87+
message: &str,
88+
signature: &[u8],
89+
signer_address: &str,
90+
) -> StdResult<VerifyResponse> {
91+
// Hashing
92+
let mut hasher = Keccak256::new();
93+
hasher.update(format!("\x19Ethereum Signed Message:\n{}", message.len()));
94+
hasher.update(message);
95+
let hash = hasher.finalize();
96+
97+
// Decompose signature
98+
let (v, rs) = match signature.split_last() {
99+
Some(pair) => pair,
100+
None => return Err(StdError::generic_err("Signature must not be empty")),
101+
};
102+
let recovery = get_recovery_param(*v)?;
103+
104+
// Verification
105+
let calculated_pubkey = deps.api.secp256k1_recover_pubkey(&hash, rs, recovery)?;
106+
let calculated_address = ethereum_address(&calculated_pubkey)?;
107+
if signer_address.to_ascii_lowercase() != calculated_address {
108+
return Ok(VerifyResponse { verifies: false });
109+
}
110+
let result = deps.api.secp256k1_verify(&hash, rs, &calculated_pubkey);
66111
match result {
67112
Ok(verifies) => Ok(VerifyResponse { verifies }),
68113
Err(err) => Err(err.into()),
@@ -87,13 +132,44 @@ pub fn query_list_verifications(deps: Deps) -> StdResult<ListVerificationsRespon
87132
})
88133
}
89134

135+
fn ethereum_address(pubkey: &[u8]) -> StdResult<String> {
136+
let (tag, data) = match pubkey.split_first() {
137+
Some(pair) => pair,
138+
None => return Err(StdError::generic_err("Public key must not be empty")),
139+
};
140+
if *tag != 0x04 {
141+
return Err(StdError::generic_err("Public key start with 0x04"));
142+
}
143+
if data.len() != 64 {
144+
return Err(StdError::generic_err("Public key must be 65 bytes long"));
145+
}
146+
147+
let hash = Keccak256::digest(data);
148+
let mut out = String::with_capacity(42);
149+
out.push_str("0x");
150+
out.push_str(&hex::encode(&hash[hash.len() - 20..]));
151+
Ok(out)
152+
}
153+
154+
fn get_recovery_param(v: u8) -> StdResult<u8> {
155+
// See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
156+
// for how `v` is composed.
157+
match v {
158+
27 => Ok(0),
159+
28 => Ok(1),
160+
_ => Err(StdError::generic_err("Values of v other than 27 and 28 not supported. Replay protection (EIP-155) cannot be used here."))
161+
}
162+
}
163+
90164
#[cfg(test)]
91165
mod tests {
92166
use super::*;
93167
use cosmwasm_std::testing::{
94168
mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage,
95169
};
96-
use cosmwasm_std::{from_slice, Binary, OwnedDeps, StdError, VerificationError};
170+
use cosmwasm_std::{
171+
from_slice, Binary, OwnedDeps, RecoverPubkeyError, StdError, VerificationError,
172+
};
97173

98174
const CREATOR: &str = "creator";
99175

@@ -107,6 +183,11 @@ mod tests {
107183
const ED25519_PUBLIC_KEY_HEX: &str =
108184
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025";
109185

186+
// Signed text "connect all the things" using MyEtherWallet with private key b5b1870957d373ef0eeffecc6e4812c0fd08f554b37b233526acc331bf1544f7
187+
const ETHEREUM_MESSAGE: &str = "connect all the things";
188+
const ETHEREUM_SIGNATURE_HEX: &str = "dada130255a447ecf434a2df9193e6fbba663e4546c35c075cd6eea21d8c7cb1714b9b65a4f7f604ff6aad55fba73f8c36514a512bbbba03709b37069194f8a41b";
189+
const ETHEREUM_SIGNER_ADDRESS: &str = "0x12890D2cce102216644c59daE5baed380d84830c";
190+
110191
fn setup() -> OwnedDeps<MockStorage, MockApi, MockQuerier> {
111192
let mut deps = mock_dependencies(&[]);
112193
let msg = InitMsg {};
@@ -187,6 +268,81 @@ mod tests {
187268
)
188269
}
189270

271+
#[test]
272+
fn ethereum_signature_verify_works() {
273+
let deps = setup();
274+
275+
let message = ETHEREUM_MESSAGE;
276+
let signature = hex::decode(ETHEREUM_SIGNATURE_HEX).unwrap();
277+
let signer_address = ETHEREUM_SIGNER_ADDRESS;
278+
279+
let verify_msg = QueryMsg::VerifyEthereumText {
280+
message: message.into(),
281+
signature: signature.into(),
282+
signer_address: signer_address.into(),
283+
};
284+
let raw = query(deps.as_ref(), mock_env(), verify_msg).unwrap();
285+
let res: VerifyResponse = from_slice(&raw).unwrap();
286+
287+
assert_eq!(res, VerifyResponse { verifies: true });
288+
}
289+
290+
#[test]
291+
fn ethereum_signature_verify_fails_for_corrupted_message() {
292+
let deps = setup();
293+
294+
let mut message = String::from(ETHEREUM_MESSAGE);
295+
message.push('!');
296+
let signature = hex::decode(ETHEREUM_SIGNATURE_HEX).unwrap();
297+
let signer_address = ETHEREUM_SIGNER_ADDRESS;
298+
299+
let verify_msg = QueryMsg::VerifyEthereumText {
300+
message: message.into(),
301+
signature: signature.into(),
302+
signer_address: signer_address.into(),
303+
};
304+
let raw = query(deps.as_ref(), mock_env(), verify_msg).unwrap();
305+
let res: VerifyResponse = from_slice(&raw).unwrap();
306+
307+
assert_eq!(res, VerifyResponse { verifies: false });
308+
}
309+
310+
#[test]
311+
fn ethereum_signature_verify_fails_for_corrupted_signature() {
312+
let deps = setup();
313+
314+
let message = ETHEREUM_MESSAGE;
315+
let signer_address = ETHEREUM_SIGNER_ADDRESS;
316+
317+
// Wrong signature
318+
let mut signature = hex::decode(ETHEREUM_SIGNATURE_HEX).unwrap();
319+
signature[5] ^= 0x01;
320+
let verify_msg = QueryMsg::VerifyEthereumText {
321+
message: message.into(),
322+
signature: signature.into(),
323+
signer_address: signer_address.into(),
324+
};
325+
let raw = query(deps.as_ref(), mock_env(), verify_msg).unwrap();
326+
let res: VerifyResponse = from_slice(&raw).unwrap();
327+
assert_eq!(res, VerifyResponse { verifies: false });
328+
329+
// Broken signature
330+
let signature = vec![0x1c; 65];
331+
let verify_msg = QueryMsg::VerifyEthereumText {
332+
message: message.into(),
333+
signature: signature.into(),
334+
signer_address: signer_address.into(),
335+
};
336+
let result = query(deps.as_ref(), mock_env(), verify_msg);
337+
match result.unwrap_err() {
338+
StdError::RecoverPubkeyErr {
339+
source: RecoverPubkeyError::UnknownErr { .. },
340+
..
341+
} => {}
342+
err => panic!("Unexpected error: {:?}", err),
343+
}
344+
}
345+
190346
#[test]
191347
fn tendermint_signature_verify_works() {
192348
let deps = setup();

contracts/crypto-verify/src/msg.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ pub enum QueryMsg {
2323
/// Serialized compressed (33 bytes) or uncompressed (65 bytes) public key.
2424
public_key: Binary,
2525
},
26+
/// Ethereum text verification (compatible to the eth_sign RPC/web3 enpoint).
27+
/// This cannot be used to verify transaction.
28+
///
29+
/// See https://web3js.readthedocs.io/en/v1.2.0/web3-eth.html#sign
30+
VerifyEthereumText {
31+
/// Message to verify. This will be wrapped in the standard container
32+
/// `"\x19Ethereum Signed Message:\n" + len(message) + message` before verification.
33+
message: String,
34+
/// Serialized signature. Fixed length format (64 bytes `r` and `s` plus the one byte `v`).
35+
signature: Binary,
36+
/// Signer address.
37+
/// This is matched case insensitive, so you can provide checksummed and non-checksummed addresses. Checksums are not validated.
38+
signer_address: String,
39+
},
2640
/// Tendermint format (ed25519 verification scheme).
2741
VerifyTendermintSignature {
2842
/// Message to verify.

0 commit comments

Comments
 (0)