Skip to content

Add pubkey recovery API #790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Feb 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4d9fb98
Build and test crypto in local dev build
webmaster128 Feb 17, 2021
8bbb605
Add secp256k1_recover_pubkey
webmaster128 Feb 17, 2021
0392f1f
Add test with recovery_param = 1
webmaster128 Feb 17, 2021
fe7bd21
Test invalid recovery param
webmaster128 Feb 17, 2021
b845b87
Groups inputs together
webmaster128 Feb 18, 2021
aeb4c8f
Rename to GAS_COST_SECP256K1_VERIFY_SIGNATURE
webmaster128 Feb 18, 2021
16b22ce
Add do_secp256k1_recover_pubkey
webmaster128 Feb 18, 2021
263202f
Use hex_literal for test
webmaster128 Feb 18, 2021
f2ad955
Add secp256k1_recover_pubkey to API
webmaster128 Feb 18, 2021
a3f0294
Remove unnecessary error cases
webmaster128 Feb 18, 2021
ed44388
Add RecoverPubkeyError to StdError converter
webmaster128 Feb 18, 2021
9809349
Ensure we don't throw away important data
webmaster128 Feb 18, 2021
44cb087
Test secp256k1_recover_pubkey_fails_for_wrong_hash
webmaster128 Feb 18, 2021
841f4be
Add env.secp256k1_recover_pubkey to supported imports
webmaster128 Feb 18, 2021
dfc819d
Add VerifyEthereumSignature
webmaster128 Feb 18, 2021
7c855b5
Fix error propagation
webmaster128 Feb 18, 2021
c7d276d
Enhance broken signature tests
webmaster128 Feb 18, 2021
0c1e1d2
Fix signature format description
webmaster128 Feb 18, 2021
8f215c8
Let secp256k1_recover_pubkey return uncompressed and fix tests
webmaster128 Feb 18, 2021
722c4e7
Check signer address instead of pubkey
webmaster128 Feb 18, 2021
b13a350
Format code
webmaster128 Feb 18, 2021
77860be
Make high/low more explicit; Remove unneeded out variable.
webmaster128 Feb 22, 2021
f1a572c
Pull out get_recovery_param
webmaster128 Feb 22, 2021
7eb303d
Improve docs of QueryMsg::VerifyEthereumText
webmaster128 Feb 22, 2021
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
9 changes: 9 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ nightly toolchain installed as well.

```sh
cargo fmt \
&& (cd packages/crypto && cargo build && cargo test && cargo clippy -- -D warnings) \
&& (cd packages/std && cargo wasm-debug --features iterator && cargo test --features iterator && cargo clippy --features iterator -- -D warnings && cargo schema) \
&& (cd packages/storage && cargo build && cargo test --features iterator && cargo clippy --features iterator -- -D warnings) \
&& (cd packages/schema && cargo build && cargo test && cargo clippy -- -D warnings) \
Expand Down
26 changes: 26 additions & 0 deletions contracts/crypto-verify/Cargo.lock

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

3 changes: 2 additions & 1 deletion contracts/crypto-verify/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ backtraces = ["cosmwasm-std/backtraces", "cosmwasm-vm/backtraces"]
[dependencies]
cosmwasm-std = { path = "../../packages/std", features = ["iterator"] }
cosmwasm-storage = { path = "../../packages/storage", features = ["iterator"] }
hex = "0.4"
schemars = "0.7"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
sha2 = "0.9"
sha3 = "0.9"

[dev-dependencies]
cosmwasm-vm = { path = "../../packages/vm", default-features = false, features = ["iterator"] }
cosmwasm-schema = { path = "../../packages/schema" }
hex = "0.4"
35 changes: 35 additions & 0 deletions contracts/crypto-verify/schema/query_msg.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,41 @@
}
}
},
{
"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",
"type": "object",
"required": [
"verify_ethereum_text"
],
"properties": {
"verify_ethereum_text": {
"type": "object",
"required": [
"message",
"signature",
"signer_address"
],
"properties": {
"message": {
"description": "Message to verify. This will be wrapped in the standard container `\"\\x19Ethereum Signed Message:\\n\" + len(message) + message` before verification.",
"type": "string"
},
"signature": {
"description": "Serialized signature. Fixed length format (64 bytes `r` and `s` plus the one byte `v`).",
"allOf": [
{
"$ref": "#/definitions/Binary"
}
]
},
"signer_address": {
"description": "Signer address. This is matched case insensitive, so you can provide checksummed and non-checksummed addresses. Checksums are not validated.",
"type": "string"
}
}
}
}
},
{
"description": "Tendermint format (ed25519 verification scheme).",
"type": "object",
Expand Down
166 changes: 161 additions & 5 deletions contracts/crypto-verify/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use sha2::{Digest, Sha256};

use cosmwasm_std::{
entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdResult,
entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError,
StdResult,
};
use sha2::{Digest, Sha256};
use sha3::Keccak256;

use crate::msg::{
list_verifications, HandleMsg, InitMsg, ListVerificationsResponse, QueryMsg, VerifyResponse,
Expand Down Expand Up @@ -38,6 +39,16 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<QueryResponse> {
&signature.0,
&public_key.0,
)?),
QueryMsg::VerifyEthereumText {
message,
signature,
signer_address,
} => to_binary(&query_verify_ethereum_text(
deps,
&message,
&signature,
&signer_address,
)?),
QueryMsg::VerifyTendermintSignature {
message,
signature,
Expand All @@ -62,7 +73,41 @@ pub fn query_verify_cosmos(
let hash = Sha256::digest(message);

// Verification
let result = deps.api.secp256k1_verify(&*hash, signature, public_key);
let result = deps
.api
.secp256k1_verify(hash.as_ref(), signature, public_key);
match result {
Ok(verifies) => Ok(VerifyResponse { verifies }),
Err(err) => Err(err.into()),
}
}

pub fn query_verify_ethereum_text(
deps: Deps,
message: &str,
signature: &[u8],
signer_address: &str,
) -> StdResult<VerifyResponse> {
// Hashing
let mut hasher = Keccak256::new();
hasher.update(format!("\x19Ethereum Signed Message:\n{}", message.len()));
hasher.update(message);
let hash = hasher.finalize();

// Decompose signature
let (v, rs) = match signature.split_last() {
Some(pair) => pair,
None => return Err(StdError::generic_err("Signature must not be empty")),
};
let recovery = get_recovery_param(*v)?;

// Verification
let calculated_pubkey = deps.api.secp256k1_recover_pubkey(&hash, rs, recovery)?;
let calculated_address = ethereum_address(&calculated_pubkey)?;
if signer_address.to_ascii_lowercase() != calculated_address {
return Ok(VerifyResponse { verifies: false });
}
let result = deps.api.secp256k1_verify(&hash, rs, &calculated_pubkey);
match result {
Ok(verifies) => Ok(VerifyResponse { verifies }),
Err(err) => Err(err.into()),
Expand All @@ -87,13 +132,44 @@ pub fn query_list_verifications(deps: Deps) -> StdResult<ListVerificationsRespon
})
}

fn ethereum_address(pubkey: &[u8]) -> StdResult<String> {
let (tag, data) = match pubkey.split_first() {
Some(pair) => pair,
None => return Err(StdError::generic_err("Public key must not be empty")),
};
if *tag != 0x04 {
return Err(StdError::generic_err("Public key start with 0x04"));
}
if data.len() != 64 {
return Err(StdError::generic_err("Public key must be 65 bytes long"));
}

let hash = Keccak256::digest(data);
let mut out = String::with_capacity(42);
out.push_str("0x");
out.push_str(&hex::encode(&hash[hash.len() - 20..]));
Ok(out)
}

fn get_recovery_param(v: u8) -> StdResult<u8> {
// See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
// for how `v` is composed.
match v {
27 => Ok(0),
28 => Ok(1),
_ => Err(StdError::generic_err("Values of v other than 27 and 28 not supported. Replay protection (EIP-155) cannot be used here."))
}
}

#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage,
};
use cosmwasm_std::{from_slice, Binary, OwnedDeps, StdError, VerificationError};
use cosmwasm_std::{
from_slice, Binary, OwnedDeps, RecoverPubkeyError, StdError, VerificationError,
};

const CREATOR: &str = "creator";

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

// Signed text "connect all the things" using MyEtherWallet with private key b5b1870957d373ef0eeffecc6e4812c0fd08f554b37b233526acc331bf1544f7
const ETHEREUM_MESSAGE: &str = "connect all the things";
const ETHEREUM_SIGNATURE_HEX: &str = "dada130255a447ecf434a2df9193e6fbba663e4546c35c075cd6eea21d8c7cb1714b9b65a4f7f604ff6aad55fba73f8c36514a512bbbba03709b37069194f8a41b";
const ETHEREUM_SIGNER_ADDRESS: &str = "0x12890D2cce102216644c59daE5baed380d84830c";

fn setup() -> OwnedDeps<MockStorage, MockApi, MockQuerier> {
let mut deps = mock_dependencies(&[]);
let msg = InitMsg {};
Expand Down Expand Up @@ -187,6 +268,81 @@ mod tests {
)
}

#[test]
fn ethereum_signature_verify_works() {
let deps = setup();

let message = ETHEREUM_MESSAGE;
let signature = hex::decode(ETHEREUM_SIGNATURE_HEX).unwrap();
let signer_address = ETHEREUM_SIGNER_ADDRESS;

let verify_msg = QueryMsg::VerifyEthereumText {
message: message.into(),
signature: signature.into(),
signer_address: signer_address.into(),
};
let raw = query(deps.as_ref(), mock_env(), verify_msg).unwrap();
let res: VerifyResponse = from_slice(&raw).unwrap();

assert_eq!(res, VerifyResponse { verifies: true });
}

#[test]
fn ethereum_signature_verify_fails_for_corrupted_message() {
let deps = setup();

let mut message = String::from(ETHEREUM_MESSAGE);
message.push('!');
let signature = hex::decode(ETHEREUM_SIGNATURE_HEX).unwrap();
let signer_address = ETHEREUM_SIGNER_ADDRESS;

let verify_msg = QueryMsg::VerifyEthereumText {
message: message.into(),
signature: signature.into(),
signer_address: signer_address.into(),
};
let raw = query(deps.as_ref(), mock_env(), verify_msg).unwrap();
let res: VerifyResponse = from_slice(&raw).unwrap();

assert_eq!(res, VerifyResponse { verifies: false });
}

#[test]
fn ethereum_signature_verify_fails_for_corrupted_signature() {
let deps = setup();

let message = ETHEREUM_MESSAGE;
let signer_address = ETHEREUM_SIGNER_ADDRESS;

// Wrong signature
let mut signature = hex::decode(ETHEREUM_SIGNATURE_HEX).unwrap();
signature[5] ^= 0x01;
let verify_msg = QueryMsg::VerifyEthereumText {
message: message.into(),
signature: signature.into(),
signer_address: signer_address.into(),
};
let raw = query(deps.as_ref(), mock_env(), verify_msg).unwrap();
let res: VerifyResponse = from_slice(&raw).unwrap();
assert_eq!(res, VerifyResponse { verifies: false });

// Broken signature
let signature = vec![0x1c; 65];
let verify_msg = QueryMsg::VerifyEthereumText {
message: message.into(),
signature: signature.into(),
signer_address: signer_address.into(),
};
let result = query(deps.as_ref(), mock_env(), verify_msg);
match result.unwrap_err() {
StdError::RecoverPubkeyErr {
source: RecoverPubkeyError::UnknownErr { .. },
..
} => {}
err => panic!("Unexpected error: {:?}", err),
}
}

#[test]
fn tendermint_signature_verify_works() {
let deps = setup();
Expand Down
14 changes: 14 additions & 0 deletions contracts/crypto-verify/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ pub enum QueryMsg {
/// Serialized compressed (33 bytes) or uncompressed (65 bytes) public key.
public_key: Binary,
},
/// Ethereum text verification (compatible to the eth_sign RPC/web3 enpoint).
/// This cannot be used to verify transaction.
///
/// See https://web3js.readthedocs.io/en/v1.2.0/web3-eth.html#sign
VerifyEthereumText {
/// Message to verify. This will be wrapped in the standard container
/// `"\x19Ethereum Signed Message:\n" + len(message) + message` before verification.
message: String,
/// Serialized signature. Fixed length format (64 bytes `r` and `s` plus the one byte `v`).
signature: Binary,
/// Signer address.
/// This is matched case insensitive, so you can provide checksummed and non-checksummed addresses. Checksums are not validated.
signer_address: String,
},
/// Tendermint format (ed25519 verification scheme).
VerifyTendermintSignature {
/// Message to verify.
Expand Down
Loading