diff --git a/Cargo.lock b/Cargo.lock index 1c26e2c5fa..3c9d7572db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,7 @@ dependencies = [ "ed25519-zebra", "elliptic-curve", "hex", + "hex-literal", "k256", "rand_core", "serde", @@ -239,6 +240,7 @@ dependencies = [ "cosmwasm-derive", "cosmwasm-schema", "hex", + "hex-literal", "schemars", "serde", "serde-json-wasm", @@ -263,6 +265,7 @@ dependencies = [ "cosmwasm-std", "criterion", "hex", + "hex-literal", "parity-wasm", "schemars", "serde", @@ -735,6 +738,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +[[package]] +name = "hex-literal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af1f635ef1bc545d78392b136bfe1c9809e029023c84a3638a864a10b8819c8" + [[package]] name = "hmac" version = "0.10.1" diff --git a/README.md b/README.md index 2494ae9b50..9e419d42ad 100644 --- a/README.md +++ b/README.md @@ -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) \ diff --git a/contracts/crypto-verify/Cargo.lock b/contracts/crypto-verify/Cargo.lock index bdeec3b5c1..272bcf61a6 100644 --- a/contracts/crypto-verify/Cargo.lock +++ b/contracts/crypto-verify/Cargo.lock @@ -74,9 +74,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ + "block-padding", "generic-array", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "byteorder" version = "1.3.4" @@ -333,6 +340,7 @@ dependencies = [ "schemars", "serde", "sha2", + "sha3", ] [[package]] @@ -639,6 +647,12 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "keccak" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1054,6 +1068,18 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer", + "digest", + "keccak", + "opaque-debug", +] + [[package]] name = "signature" version = "1.2.2" diff --git a/contracts/crypto-verify/Cargo.toml b/contracts/crypto-verify/Cargo.toml index ce003709e4..70e7068477 100644 --- a/contracts/crypto-verify/Cargo.toml +++ b/contracts/crypto-verify/Cargo.toml @@ -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" diff --git a/contracts/crypto-verify/schema/query_msg.json b/contracts/crypto-verify/schema/query_msg.json index 98c9818683..ff3fc02573 100644 --- a/contracts/crypto-verify/schema/query_msg.json +++ b/contracts/crypto-verify/schema/query_msg.json @@ -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", diff --git a/contracts/crypto-verify/src/contract.rs b/contracts/crypto-verify/src/contract.rs index 41037f302c..20fea1d20f 100644 --- a/contracts/crypto-verify/src/contract.rs +++ b/contracts/crypto-verify/src/contract.rs @@ -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, @@ -38,6 +39,16 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { &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, @@ -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 { + // 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()), @@ -87,13 +132,44 @@ pub fn query_list_verifications(deps: Deps) -> StdResult StdResult { + 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 { + // 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"; @@ -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 { let mut deps = mock_dependencies(&[]); let msg = InitMsg {}; @@ -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(); diff --git a/contracts/crypto-verify/src/msg.rs b/contracts/crypto-verify/src/msg.rs index 379473d030..be542cdaec 100644 --- a/contracts/crypto-verify/src/msg.rs +++ b/contracts/crypto-verify/src/msg.rs @@ -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. diff --git a/contracts/crypto-verify/tests/integration.rs b/contracts/crypto-verify/tests/integration.rs index 1b50dbbcb8..4871e3b464 100644 --- a/contracts/crypto-verify/tests/integration.rs +++ b/contracts/crypto-verify/tests/integration.rs @@ -42,6 +42,11 @@ const ED25519_SIGNATURE_HEX: &str = "6291d657deec24024827e69c3abe01a30ce548a2847 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() -> Instance { let mut deps = mock_instance(WASM, &[]); let msg = InitMsg {}; @@ -115,6 +120,76 @@ fn cosmos_signature_verify_errors() { assert_eq!(res.unwrap_err(), "Verification error: Public key error") } +#[test] +fn ethereum_signature_verify_works() { + let mut 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(&mut deps, 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 mut 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(&mut deps, 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 mut 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.clone().into(), + }; + let raw = query(&mut deps, 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(&mut deps, mock_env(), verify_msg); + let msg = result.unwrap_err(); + assert_eq!(msg, "Recover pubkey error: Unknown error: 10"); +} + #[test] fn tendermint_signature_verify_works() { let mut deps = setup(); diff --git a/packages/crypto/Cargo.toml b/packages/crypto/Cargo.toml index 277e7f07b7..bf8b833023 100644 --- a/packages/crypto/Cargo.toml +++ b/packages/crypto/Cargo.toml @@ -26,5 +26,6 @@ serde_json = "1.0" sha2 = "0.9" base64 = "0.13.0" hex = "0.4" +hex-literal = "0.3.1" rand_core = { version = "0.5", features = ["getrandom"] } elliptic-curve = "0.8.4" diff --git a/packages/crypto/src/errors.rs b/packages/crypto/src/errors.rs index 1fb1c2aa31..924813becb 100644 --- a/packages/crypto/src/errors.rs +++ b/packages/crypto/src/errors.rs @@ -37,6 +37,11 @@ pub enum CryptoError { #[cfg(feature = "backtraces")] backtrace: Backtrace, }, + #[error("Invalid recovery parameter. Supported values: 0 and 1.")] + InvalidRecoveryParam { + #[cfg(feature = "backtraces")] + backtrace: Backtrace, + }, } impl CryptoError { @@ -80,6 +85,13 @@ impl CryptoError { } } + pub fn invalid_recovery_param() -> Self { + CryptoError::InvalidRecoveryParam { + #[cfg(feature = "backtraces")] + backtrace: Backtrace::capture(), + } + } + /// Numeric error code that can easily be passed over the /// contract VM boundary. pub fn code(&self) -> u32 { @@ -88,6 +100,7 @@ impl CryptoError { CryptoError::HashErr { .. } => 3, CryptoError::SignatureErr { .. } => 4, CryptoError::PublicKeyErr { .. } => 5, + CryptoError::InvalidRecoveryParam { .. } => 6, CryptoError::GenericErr { .. } => 10, } } diff --git a/packages/crypto/src/lib.rs b/packages/crypto/src/lib.rs index 4c6a4e7232..bca2cd8eb2 100644 --- a/packages/crypto/src/lib.rs +++ b/packages/crypto/src/lib.rs @@ -16,6 +16,6 @@ pub use crate::ed25519::{EDDSA_PUBKEY_LEN, EDDSA_SIGNATURE_LEN, MESSAGE_MAX_LEN} #[doc(hidden)] pub use crate::errors::{CryptoError, CryptoResult}; #[doc(hidden)] -pub use crate::secp256k1::secp256k1_verify; +pub use crate::secp256k1::{secp256k1_recover_pubkey, secp256k1_verify}; #[doc(hidden)] pub use crate::secp256k1::{ECDSA_PUBKEY_MAX_LEN, ECDSA_SIGNATURE_LEN, MESSAGE_HASH_MAX_LEN}; diff --git a/packages/crypto/src/secp256k1.rs b/packages/crypto/src/secp256k1.rs index 66cd46052a..dd9e9d1097 100644 --- a/packages/crypto/src/secp256k1.rs +++ b/packages/crypto/src/secp256k1.rs @@ -1,7 +1,9 @@ use digest::Digest; // trait use k256::{ + ecdsa::recoverable, ecdsa::signature::{DigestVerifier, Signature as _}, // traits ecdsa::{Signature, VerifyingKey}, // type aliases + elliptic_curve::sec1::ToEncodedPoint, }; use crate::errors::{CryptoError, CryptoResult}; @@ -87,11 +89,58 @@ pub fn secp256k1_verify( .map_err(|e| CryptoError::generic_err(e.to_string()))?; match public_key.verify_digest(message_digest, &signature) { - Ok(_) => Ok(true), + Ok(()) => Ok(true), Err(_) => Ok(false), } } +/// Recovers a public key from a message hash and a signature. +/// +/// This is required when working with Ethereum where public keys +/// are not stored on chain directly. +/// +/// `recovery_param` must be 0 or 1. The values 2 and 3 are unsupported by this implementation, +/// which is the same restriction as Ethereum has (https://github.com/ethereum/go-ethereum/blob/v1.9.25/internal/ethapi/api.go#L466-L469). +/// All other values are invalid. +/// +/// Returns the recovered pubkey in compressed form, which can be used +/// in secp256k1_verify directly. +pub fn secp256k1_recover_pubkey( + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, +) -> Result, CryptoError> { + if message_hash.len() != MESSAGE_HASH_MAX_LEN { + return Err(CryptoError::hash_err(format!( + "wrong length: {}", + message_hash.len() + ))); + } + if signature.len() != ECDSA_SIGNATURE_LEN { + return Err(CryptoError::sig_err(format!( + "wrong / unsupported length: {}", + signature.len() + ))); + } + + let id = + recoverable::Id::new(recovery_param).map_err(|_| CryptoError::invalid_recovery_param())?; + + // Compose extended signature + let signature = + Signature::from_bytes(signature).map_err(|e| CryptoError::generic_err(e.to_string()))?; + let extended_signature = recoverable::Signature::new(&signature, id) + .map_err(|e| CryptoError::generic_err(e.to_string()))?; + + // Recover + let message_digest = Identity256::new().chain(message_hash); + let pubkey = extended_signature + .recover_verify_key_from_digest(message_digest) + .map_err(|e| CryptoError::generic_err(e.to_string()))?; + let encoded: Vec = pubkey.to_encoded_point(false).as_bytes().into(); + Ok(encoded) +} + #[cfg(test)] mod tests { use super::*; @@ -99,6 +148,7 @@ mod tests { use elliptic_curve::sec1::ToEncodedPoint; use rand_core::OsRng; + use hex_literal::hex; use k256::{ ecdsa::signature::DigestSigner, // trait ecdsa::SigningKey, // type alias @@ -245,4 +295,79 @@ mod tests { ); } } + + #[test] + fn secp256k1_recover_pubkey_works() { + // Test data from https://github.com/ethereumjs/ethereumjs-util/blob/v6.1.0/test/index.js#L496 + { + let private_key = + hex!("3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1"); + let expected = SigningKey::from_bytes(&private_key) + .unwrap() + .verify_key() + .to_encoded_point(false) + .as_bytes() + .to_vec(); + let r_s = hex!("99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9129ff05af364204442bdb53ab6f18a99ab48acc9326fa689f228040429e3ca66"); + let recovery_param: u8 = 0; + let message_hash = + hex!("82ff40c0a986c6a5cfad4ddf4c3aa6996f1a7837f9c398e17e5de5cbd5a12b28"); + let pubkey = secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap(); + assert_eq!(pubkey, expected); + } + + // Test data from https://github.com/randombit/botan/blob/2.9.0/src/tests/data/pubkey/ecdsa_key_recovery.vec + { + let expected_x = "F3F8BB913AA68589A2C8C607A877AB05252ADBD963E1BE846DDEB8456942AEDC"; + let expected_y = "A2ED51F08CA3EF3DAC0A7504613D54CD539FC1B3CBC92453CD704B6A2D012B2C"; + let expected = hex::decode(format!("04{}{}", expected_x, expected_y)).unwrap(); + let r_s = hex!("E30F2E6A0F705F4FB5F8501BA79C7C0D3FAC847F1AD70B873E9797B17B89B39081F1A4457589F30D76AB9F89E748A68C8A94C30FE0BAC8FB5C0B54EA70BF6D2F"); + let recovery_param: u8 = 0; + let message_hash = + hex!("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + let pubkey = secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap(); + assert_eq!(pubkey, expected); + } + + // Test data calculated via Secp256k1.createSignature from @cosmjs/crypto + { + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + let r_s = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let recovery_param: u8 = 1; + let message_hash = + hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let pubkey = secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap(); + assert_eq!(pubkey, expected); + } + } + + #[test] + fn secp256k1_recover_pubkey_fails_for_invalid_recovery_param() { + let r_s = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let message_hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + + // 2 and 3 are explicitly unsupported + let recovery_param: u8 = 2; + match secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap_err() { + CryptoError::InvalidRecoveryParam { .. } => {} + err => panic!("Unexpected error: {}", err), + } + let recovery_param: u8 = 3; + match secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap_err() { + CryptoError::InvalidRecoveryParam { .. } => {} + err => panic!("Unexpected error: {}", err), + } + + // Other values are garbage + let recovery_param: u8 = 4; + match secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap_err() { + CryptoError::InvalidRecoveryParam { .. } => {} + err => panic!("Unexpected error: {}", err), + } + let recovery_param: u8 = 255; + match secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap_err() { + CryptoError::InvalidRecoveryParam { .. } => {} + err => panic!("Unexpected error: {}", err), + } + } } diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 183d5222ef..fa7bb2b982 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -43,3 +43,4 @@ cosmwasm-schema = { path = "../schema" } # The chrono dependency is only used in an example, which Rust compiles for us. If this causes trouble, remove it. chrono = "0.4" hex = "0.4" +hex-literal = "0.3.1" diff --git a/packages/std/src/errors/mod.rs b/packages/std/src/errors/mod.rs index 598b847f0b..999b6a4079 100644 --- a/packages/std/src/errors/mod.rs +++ b/packages/std/src/errors/mod.rs @@ -1,7 +1,9 @@ +mod recover_pubkey_error; mod std_error; mod system_error; mod verification_error; +pub use recover_pubkey_error::RecoverPubkeyError; pub use std_error::{StdError, StdResult}; pub use system_error::SystemError; pub use verification_error::VerificationError; diff --git a/packages/std/src/errors/recover_pubkey_error.rs b/packages/std/src/errors/recover_pubkey_error.rs new file mode 100644 index 0000000000..04b4486ae8 --- /dev/null +++ b/packages/std/src/errors/recover_pubkey_error.rs @@ -0,0 +1,69 @@ +#[cfg(not(target_arch = "wasm32"))] +use cosmwasm_crypto::CryptoError; +#[cfg(feature = "backtraces")] +use std::backtrace::Backtrace; +use std::fmt::Debug; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RecoverPubkeyError { + #[error("Hash error")] + HashErr, + #[error("Signature error")] + SignatureErr, + #[error("Invalid recovery parameter. Supported values: 0 and 1.")] + InvalidRecoveryParam, + #[error("Unknown error: {error_code}")] + UnknownErr { + error_code: u32, + #[cfg(feature = "backtraces")] + backtrace: Backtrace, + }, +} + +impl RecoverPubkeyError { + pub fn unknown_err(error_code: u32) -> Self { + RecoverPubkeyError::UnknownErr { + error_code, + #[cfg(feature = "backtraces")] + backtrace: Backtrace::capture(), + } + } +} + +impl PartialEq for RecoverPubkeyError { + fn eq(&self, rhs: &RecoverPubkeyError) -> bool { + match self { + RecoverPubkeyError::HashErr => matches!(rhs, RecoverPubkeyError::HashErr), + RecoverPubkeyError::SignatureErr => matches!(rhs, RecoverPubkeyError::SignatureErr), + RecoverPubkeyError::InvalidRecoveryParam => { + matches!(rhs, RecoverPubkeyError::InvalidRecoveryParam) + } + RecoverPubkeyError::UnknownErr { error_code, .. } => { + if let RecoverPubkeyError::UnknownErr { + error_code: rhs_error_code, + .. + } = rhs + { + error_code == rhs_error_code + } else { + false + } + } + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for RecoverPubkeyError { + fn from(original: CryptoError) -> Self { + match original { + CryptoError::MessageError { .. } => panic!("Conversion not supported"), + CryptoError::HashErr { .. } => RecoverPubkeyError::HashErr, + CryptoError::SignatureErr { .. } => RecoverPubkeyError::SignatureErr, + CryptoError::PublicKeyErr { .. } => panic!("Conversion not supported"), + CryptoError::GenericErr { .. } => RecoverPubkeyError::unknown_err(original.code()), + CryptoError::InvalidRecoveryParam { .. } => RecoverPubkeyError::InvalidRecoveryParam, + } + } +} diff --git a/packages/std/src/errors/std_error.rs b/packages/std/src/errors/std_error.rs index 3f0e2d3b8a..72e7442682 100644 --- a/packages/std/src/errors/std_error.rs +++ b/packages/std/src/errors/std_error.rs @@ -2,7 +2,7 @@ use std::backtrace::Backtrace; use thiserror::Error; -use crate::errors::VerificationError; +use crate::errors::{RecoverPubkeyError, VerificationError}; /// Structured error type for init, handle and query. /// @@ -27,6 +27,12 @@ pub enum StdError { #[cfg(feature = "backtraces")] backtrace: Backtrace, }, + #[error("Recover pubkey error: {source}")] + RecoverPubkeyErr { + source: RecoverPubkeyError, + #[cfg(feature = "backtraces")] + backtrace: Backtrace, + }, /// Whenever there is no specific error type available #[error("Generic error: {msg}")] GenericErr { @@ -94,6 +100,14 @@ impl StdError { } } + pub fn recover_pubkey_err(source: RecoverPubkeyError) -> Self { + StdError::RecoverPubkeyErr { + source, + #[cfg(feature = "backtraces")] + backtrace: Backtrace::capture(), + } + } + pub fn generic_err>(msg: S) -> Self { StdError::GenericErr { msg: msg.into(), @@ -183,6 +197,22 @@ impl PartialEq for StdError { false } } + StdError::RecoverPubkeyErr { + source, + #[cfg(feature = "backtraces")] + backtrace: _, + } => { + if let StdError::RecoverPubkeyErr { + source: rhs_source, + #[cfg(feature = "backtraces")] + backtrace: _, + } = rhs + { + source == rhs_source + } else { + false + } + } StdError::GenericErr { msg, #[cfg(feature = "backtraces")] @@ -341,6 +371,12 @@ impl From for StdError { } } +impl From for StdError { + fn from(source: RecoverPubkeyError) -> Self { + Self::recover_pubkey_err(source) + } +} + /// The return type for init, handle and query. Since the error type cannot be serialized to JSON, /// this is only available within the contract and its unit tests. /// diff --git a/packages/std/src/errors/verification_error.rs b/packages/std/src/errors/verification_error.rs index ef35bf26c3..9bb708a47e 100644 --- a/packages/std/src/errors/verification_error.rs +++ b/packages/std/src/errors/verification_error.rs @@ -18,6 +18,8 @@ pub enum VerificationError { SignatureErr, #[error("Public key error")] PublicKeyErr, + #[error("Invalid recovery parameter. Supported values: 0 and 1.")] + InvalidRecoveryParam, #[error("Unknown error: {error_code}")] UnknownErr { error_code: u32, @@ -44,6 +46,9 @@ impl PartialEq for VerificationError { VerificationError::HashErr => matches!(rhs, VerificationError::HashErr), VerificationError::SignatureErr => matches!(rhs, VerificationError::SignatureErr), VerificationError::PublicKeyErr => matches!(rhs, VerificationError::PublicKeyErr), + VerificationError::InvalidRecoveryParam => { + matches!(rhs, VerificationError::InvalidRecoveryParam) + } VerificationError::UnknownErr { error_code, .. } => { if let VerificationError::UnknownErr { error_code: rhs_error_code, @@ -68,6 +73,7 @@ impl From for VerificationError { CryptoError::SignatureErr { .. } => VerificationError::SignatureErr, CryptoError::PublicKeyErr { .. } => VerificationError::PublicKeyErr, CryptoError::GenericErr { .. } => VerificationError::GenericErr, + CryptoError::InvalidRecoveryParam { .. } => VerificationError::InvalidRecoveryParam, } } } diff --git a/packages/std/src/import_helpers.rs b/packages/std/src/import_helpers.rs new file mode 100644 index 0000000000..3b5cb93b91 --- /dev/null +++ b/packages/std/src/import_helpers.rs @@ -0,0 +1,32 @@ +use std::convert::TryInto; + +/// Returns the four most significant bytes +#[allow(dead_code)] // only used in Wasm builds +#[inline] +pub fn from_high_half(data: u64) -> u32 { + (data >> 32).try_into().unwrap() +} + +/// Returns the four least significant bytes +#[allow(dead_code)] // only used in Wasm builds +#[inline] +pub fn from_low_half(data: u64) -> u32 { + (data & 0xFFFFFFFF).try_into().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_high_half_works() { + assert_eq!(from_high_half(0), 0); + assert_eq!(from_high_half(0x1122334455667788), 0x11223344); + } + + #[test] + fn from_low_haf_works() { + assert_eq!(from_low_half(0), 0); + assert_eq!(from_low_half(0x1122334455667788), 0x55667788); + } +} diff --git a/packages/std/src/imports.rs b/packages/std/src/imports.rs index 71e27c1b5d..af7323f331 100644 --- a/packages/std/src/imports.rs +++ b/packages/std/src/imports.rs @@ -2,7 +2,8 @@ use std::vec::Vec; use crate::addresses::{CanonicalAddr, HumanAddr}; use crate::binary::Binary; -use crate::errors::{StdError, StdResult, SystemError, VerificationError}; +use crate::errors::{RecoverPubkeyError, StdError, StdResult, SystemError, VerificationError}; +use crate::import_helpers::{from_high_half, from_low_half}; use crate::memory::{alloc, build_region, consume_region, Region}; use crate::results::SystemResult; #[cfg(feature = "iterator")] @@ -38,6 +39,11 @@ extern "C" { fn humanize_address(source_ptr: u32, destination_ptr: u32) -> u32; fn secp256k1_verify(message_hash_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; + fn secp256k1_recover_pubkey( + message_hash_ptr: u32, + signature_ptr: u32, + recovery_param: u32, + ) -> u64; fn ed25519_verify(message_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; fn debug(source_ptr: u32); @@ -209,6 +215,33 @@ impl Api for ExternalApi { } } + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recover_param: u8, + ) -> Result, RecoverPubkeyError> { + let hash_send = build_region(message_hash); + let hash_send_ptr = &*hash_send as *const Region as u32; + let sig_send = build_region(signature); + let sig_send_ptr = &*sig_send as *const Region as u32; + + let result = + unsafe { secp256k1_recover_pubkey(hash_send_ptr, sig_send_ptr, recover_param.into()) }; + let error_code = from_high_half(result); + let pubkey_ptr = from_low_half(result); + match error_code { + 0 => { + let pubkey = unsafe { consume_region(pubkey_ptr as *mut Region) }; + Ok(pubkey) + } + 3 => Err(RecoverPubkeyError::HashErr), + 4 => Err(RecoverPubkeyError::SignatureErr), + 6 => Err(RecoverPubkeyError::InvalidRecoveryParam), + error_code => Err(RecoverPubkeyError::unknown_err(error_code)), + } + } + fn ed25519_verify( &self, message: &[u8], diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index d8cb610bd3..8a8f04840e 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -10,6 +10,7 @@ mod deps; mod entry_points; mod errors; mod ibc; +mod import_helpers; #[cfg(feature = "iterator")] mod iterator; mod math; @@ -25,7 +26,7 @@ pub use crate::addresses::{CanonicalAddr, HumanAddr}; pub use crate::binary::{Binary, ByteArray}; pub use crate::coins::{coin, coins, has_coins, Coin}; pub use crate::deps::{Deps, DepsMut, OwnedDeps}; -pub use crate::errors::{StdError, StdResult, SystemError, VerificationError}; +pub use crate::errors::{RecoverPubkeyError, StdError, StdResult, SystemError, VerificationError}; #[cfg(feature = "stargate")] pub use crate::ibc::{ ChannelResponse, IbcAcknowledgement, IbcBasicResponse, IbcChannel, IbcEndpoint, IbcMsg, diff --git a/packages/std/src/mock.rs b/packages/std/src/mock.rs index cf97c677f2..afc950358a 100644 --- a/packages/std/src/mock.rs +++ b/packages/std/src/mock.rs @@ -7,7 +7,7 @@ use crate::addresses::{CanonicalAddr, HumanAddr}; use crate::binary::Binary; use crate::coins::Coin; use crate::deps::OwnedDeps; -use crate::errors::{StdError, StdResult, SystemError, VerificationError}; +use crate::errors::{RecoverPubkeyError, StdError, StdResult, SystemError, VerificationError}; #[cfg(feature = "stargate")] use crate::ibc::{IbcChannel, IbcEndpoint, IbcOrder, IbcPacket, IbcTimeoutBlock}; use crate::query::{ @@ -133,6 +133,17 @@ impl Api for MockApi { )?) } + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError> { + let pubkey = + cosmwasm_crypto::secp256k1_recover_pubkey(message_hash, signature, recovery_param)?; + Ok(pubkey.to_vec()) + } + fn ed25519_verify( &self, message: &[u8], @@ -487,6 +498,7 @@ mod tests { use super::*; use crate::query::Delegation; use crate::{coin, coins, from_binary, Decimal, HumanAddr}; + use hex_literal::hex; const SECP256K1_MSG_HASH_HEX: &str = "5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"; @@ -589,6 +601,74 @@ mod tests { assert_eq!(res.unwrap_err(), VerificationError::PublicKeyErr); } + #[test] + fn secp256k1_recover_pubkey_works() { + let api = MockApi::default(); + + // https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 + let hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let signature = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let recovery_param = 1; + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + + let pubkey = api + .secp256k1_recover_pubkey(&hash, &signature, recovery_param) + .unwrap(); + assert_eq!(pubkey, expected); + } + + #[test] + fn secp256k1_recover_pubkey_fails_for_wrong_recovery_param() { + let api = MockApi::default(); + + // https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 + let hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let signature = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let _recovery_param = 1; + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + + // Wrong recovery param leads to different pubkey + let pubkey = api.secp256k1_recover_pubkey(&hash, &signature, 0).unwrap(); + assert_eq!(pubkey.len(), 65); + assert_ne!(pubkey, expected); + + // Invalid recovery param leads to error + let result = api.secp256k1_recover_pubkey(&hash, &signature, 42); + match result.unwrap_err() { + RecoverPubkeyError::InvalidRecoveryParam => {} + err => panic!("Unexpected error: {:?}", err), + } + } + + #[test] + fn secp256k1_recover_pubkey_fails_for_wrong_hash() { + let api = MockApi::default(); + + // https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 + let hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let signature = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let recovery_param = 1; + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + + // Wrong hash + let mut corrupted_hash = hash.clone(); + corrupted_hash[0] ^= 0x01; + let pubkey = api + .secp256k1_recover_pubkey(&corrupted_hash, &signature, recovery_param) + .unwrap(); + assert_eq!(pubkey.len(), 65); + assert_ne!(pubkey, expected); + + // Malformed hash + let mut malformed_hash = hash.to_vec(); + malformed_hash.push(0x8a); + let result = api.secp256k1_recover_pubkey(&malformed_hash, &signature, recovery_param); + match result.unwrap_err() { + RecoverPubkeyError::HashErr => {} + err => panic!("Unexpected error: {:?}", err), + } + } + // Basic "works" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) #[test] fn ed25519_verify_works() { diff --git a/packages/std/src/traits.rs b/packages/std/src/traits.rs index b0bc711ccb..bccc9fb2c6 100644 --- a/packages/std/src/traits.rs +++ b/packages/std/src/traits.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use crate::addresses::{CanonicalAddr, HumanAddr}; use crate::binary::Binary; use crate::coins::Coin; -use crate::errors::{StdError, StdResult, VerificationError}; +use crate::errors::{RecoverPubkeyError, StdError, StdResult, VerificationError}; #[cfg(feature = "iterator")] use crate::iterator::{Order, KV}; use crate::query::{ @@ -74,6 +74,13 @@ pub trait Api { public_key: &[u8], ) -> Result; + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError>; + fn ed25519_verify( &self, message: &[u8], diff --git a/packages/vm/Cargo.toml b/packages/vm/Cargo.toml index 8bd15e5dc8..7e64fc8c88 100644 --- a/packages/vm/Cargo.toml +++ b/packages/vm/Cargo.toml @@ -53,6 +53,7 @@ wasmer-middlewares = { version = "1.0.2" } [dev-dependencies] criterion = "0.3" +hex-literal = "0.3.1" tempfile = "3.1.0" wat = "1.0" clap = "2.33.3" diff --git a/packages/vm/src/compatibility.rs b/packages/vm/src/compatibility.rs index e15fe015a5..48c1fdea0a 100644 --- a/packages/vm/src/compatibility.rs +++ b/packages/vm/src/compatibility.rs @@ -16,6 +16,7 @@ const SUPPORTED_IMPORTS: &[&str] = &[ "env.canonicalize_address", "env.humanize_address", "env.secp256k1_verify", + "env.secp256k1_recover_pubkey", "env.ed25519_verify", "env.debug", "env.query_chain", diff --git a/packages/vm/src/imports.rs b/packages/vm/src/imports.rs index 66e1da377b..2a1a1b143b 100644 --- a/packages/vm/src/imports.rs +++ b/packages/vm/src/imports.rs @@ -1,13 +1,11 @@ //! Import implementations -#[cfg(feature = "iterator")] -use std::convert::TryInto; - -use cosmwasm_crypto::{ed25519_verify, secp256k1_verify}; +use cosmwasm_crypto::{ed25519_verify, secp256k1_recover_pubkey, secp256k1_verify, CryptoError}; use cosmwasm_crypto::{ ECDSA_PUBKEY_MAX_LEN, ECDSA_SIGNATURE_LEN, EDDSA_PUBKEY_LEN, EDDSA_SIGNATURE_LEN, MESSAGE_HASH_MAX_LEN, MESSAGE_MAX_LEN, }; +use std::convert::TryInto; #[cfg(feature = "iterator")] use cosmwasm_std::Order; @@ -23,7 +21,8 @@ use crate::memory::{read_region, write_region}; use crate::serde::to_vec; use crate::GasInfo; -const GAS_COST_VERIFY_SECP256K1_SIGNATURE: u64 = 100; +const GAS_COST_SECP256K1_VERIFY_SIGNATURE: u64 = 100; +const GAS_COST_SECP256K1_RECOVER_PUBKEY_SIGNATURE: u64 = 100; const GAS_COST_VERIFY_ED25519_SIGNATURE: u64 = 100; /// A kibi (kilo binary) @@ -95,6 +94,15 @@ pub fn native_secp256k1_verify( do_secp256k1_verify(env, hash_ptr, signature_ptr, pubkey_ptr) } +pub fn native_secp256k1_recover_pubkey( + env: &Environment, + hash_ptr: u32, + signature_ptr: u32, + recovery_param: u32, +) -> VmResult { + do_secp256k1_recover_pubkey(env, hash_ptr, signature_ptr, recovery_param) +} + pub fn native_ed25519_verify( env: &Environment, message_ptr: u32, @@ -268,15 +276,57 @@ fn do_secp256k1_verify( pubkey_ptr: u32, ) -> VmResult { let hash = read_region(&env.memory(), hash_ptr, MESSAGE_HASH_MAX_LEN)?; - let signature = read_region(&env.memory(), signature_ptr, ECDSA_SIGNATURE_LEN)?; - let pubkey = read_region(&env.memory(), pubkey_ptr, ECDSA_PUBKEY_MAX_LEN)?; let result = secp256k1_verify(&hash, &signature, &pubkey); - let gas_info = GasInfo::with_cost(GAS_COST_VERIFY_SECP256K1_SIGNATURE); + let gas_info = GasInfo::with_cost(GAS_COST_SECP256K1_VERIFY_SIGNATURE); process_gas_info::(env, gas_info)?; - Ok(result.map_or_else(|err| err.code(), |valid| if valid { 0 } else { 1 })) + Ok(result.map_or_else( + |err| match err { + CryptoError::MessageError { .. } + | CryptoError::HashErr { .. } + | CryptoError::SignatureErr { .. } + | CryptoError::PublicKeyErr { .. } + | CryptoError::GenericErr { .. } => err.code(), + CryptoError::InvalidRecoveryParam { .. } => { + panic!("Error must not happen for this call") + } + }, + |valid| if valid { 0 } else { 1 }, + )) +} + +fn do_secp256k1_recover_pubkey( + env: &Environment, + hash_ptr: u32, + signature_ptr: u32, + recover_param: u32, +) -> VmResult { + let hash = read_region(&env.memory(), hash_ptr, MESSAGE_HASH_MAX_LEN)?; + let signature = read_region(&env.memory(), signature_ptr, ECDSA_SIGNATURE_LEN)?; + let recover_param: u8 = match recover_param.try_into() { + Ok(rp) => rp, + Err(_) => return Ok((CryptoError::invalid_recovery_param().code() as u64) << 32), + }; + + let result = secp256k1_recover_pubkey(&hash, &signature, recover_param); + let gas_info = GasInfo::with_cost(GAS_COST_SECP256K1_RECOVER_PUBKEY_SIGNATURE); + process_gas_info::(env, gas_info)?; + match result { + Ok(pubkey) => { + let pubkey_ptr = write_to_contract::(env, pubkey.as_ref())?; + Ok(to_low_half(pubkey_ptr)) + } + Err(err) => match err { + CryptoError::MessageError { .. } + | CryptoError::HashErr { .. } + | CryptoError::SignatureErr { .. } + | CryptoError::InvalidRecoveryParam { .. } + | CryptoError::GenericErr { .. } => Ok(to_high_half(err.code())), + CryptoError::PublicKeyErr { .. } => panic!("Error must not happen for this call"), + }, + } } fn do_ed25519_verify( @@ -286,15 +336,25 @@ fn do_ed25519_verify( pubkey_ptr: u32, ) -> VmResult { let message = read_region(&env.memory(), message_ptr, MESSAGE_MAX_LEN)?; - let signature = read_region(&env.memory(), signature_ptr, EDDSA_SIGNATURE_LEN)?; - let pubkey = read_region(&env.memory(), pubkey_ptr, EDDSA_PUBKEY_LEN)?; let result = ed25519_verify(&message, &signature, &pubkey); let gas_info = GasInfo::with_cost(GAS_COST_VERIFY_ED25519_SIGNATURE); process_gas_info::(env, gas_info)?; - Ok(result.map_or_else(|err| err.code(), |valid| if valid { 0 } else { 1 })) + Ok(result.map_or_else( + |err| match err { + CryptoError::MessageError { .. } + | CryptoError::HashErr { .. } + | CryptoError::SignatureErr { .. } + | CryptoError::PublicKeyErr { .. } + | CryptoError::GenericErr { .. } => err.code(), + CryptoError::InvalidRecoveryParam { .. } => { + panic!("Error must not happen for this call") + } + }, + |valid| if valid { 0 } else { 1 }, + )) } /// Creates a Region in the contract, writes the given data to it and returns the memory location @@ -391,6 +451,26 @@ fn encode_sections(sections: &[Vec]) -> VmResult> { Ok(out_data) } +/// Returns the data shifted by 32 bits towards the most significant bit. +/// +/// This is independent of endianness. But to get the idea, it would be +/// `data || 0x00000000` in big endian representation. +#[inline] +fn to_high_half(data: u32) -> u64 { + // See https://stackoverflow.com/a/58956419/2013738 to understand + // why this is endianness agnostic. + (data as u64) << 32 +} + +/// Returns the data copied to the 4 least significant bytes. +/// +/// This is independent of endianness. But to get the idea, it would be +/// `0x00000000 || data` in big endian representation. +#[inline] +fn to_low_half(data: u32) -> u64 { + data.into() +} + #[cfg(test)] mod tests { use super::*; @@ -398,6 +478,7 @@ mod tests { coins, from_binary, AllBalanceResponse, BankQuery, Binary, Empty, HumanAddr, QueryRequest, SystemError, SystemResult, WasmQuery, }; + use hex_literal::hex; use std::ptr::NonNull; use wasmer::{imports, Function, Instance as WasmerInstance}; @@ -1260,6 +1341,28 @@ mod tests { ) } + #[test] + fn do_secp256k1_recover_pubkey_works() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + // https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 + let hash = hex!("5ae8317d34d1e595e3fa7247db80c0af4320cce1116de187f8f7e2e099c0d8d0"); + let sig = hex!("45c0b7f8c09a9e1f1cea0c25785594427b6bf8f9f878a8af0b1abbb48e16d0920d8becd0c220f67c51217eecfd7184ef0732481c843857e6bc7fc095c4f6b788"); + let recovery_param = 1; + let expected = hex!("044a071e8a6e10aada2b8cf39fa3b5fb3400b04e99ea8ae64ceea1a977dbeaf5d5f8c8fbd10b71ab14cd561f7df8eb6da50f8a8d81ba564342244d26d1d4211595"); + + let hash_ptr = write_data(&env, &hash); + let sig_ptr = write_data(&env, &sig); + let result = + do_secp256k1_recover_pubkey::(&env, hash_ptr, sig_ptr, recovery_param) + .unwrap(); + let error = result >> 32; + let pubkey_ptr: u32 = (result & 0xFFFFFFFF).try_into().unwrap(); + assert_eq!(error, 0); + assert_eq!(force_read(&env, pubkey_ptr), expected); + } + #[test] fn do_ed25519_verify_works() { let api = MockApi::default(); @@ -1784,4 +1887,36 @@ mod tests { let enc = encode_sections(&[vec![0xAA], vec![0xDE, 0xDE], vec![], vec![0xFF; 19]]).unwrap(); assert_eq!(enc, b"\xAA\0\0\0\x01\xDE\xDE\0\0\0\x02\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\0\0\0\x13" as &[u8]); } + + #[test] + fn to_high_half_works() { + assert_eq!( + to_high_half(0), + u64::from_be_bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ); + assert_eq!( + to_high_half(1), + u64::from_be_bytes([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]) + ); + assert_eq!( + to_high_half(u32::from_be_bytes([0x12, 0x34, 0x56, 0x78])), + u64::from_be_bytes([0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00]) + ); + } + + #[test] + fn to_low_half_works() { + assert_eq!( + to_low_half(0), + u64::from_be_bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ); + assert_eq!( + to_low_half(1), + u64::from_be_bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) + ); + assert_eq!( + to_low_half(u32::from_be_bytes([0x12, 0x34, 0x56, 0x78])), + u64::from_be_bytes([0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78]) + ); + } } diff --git a/packages/vm/src/instance.rs b/packages/vm/src/instance.rs index defa5291b3..adee48d7a2 100644 --- a/packages/vm/src/instance.rs +++ b/packages/vm/src/instance.rs @@ -10,7 +10,8 @@ use crate::errors::{CommunicationError, VmError, VmResult}; use crate::features::required_features_from_wasmer_instance; use crate::imports::{ native_canonicalize_address, native_db_read, native_db_remove, native_db_write, native_debug, - native_ed25519_verify, native_humanize_address, native_query_chain, native_secp256k1_verify, + native_ed25519_verify, native_humanize_address, native_query_chain, + native_secp256k1_recover_pubkey, native_secp256k1_verify, }; #[cfg(feature = "iterator")] use crate::imports::{native_db_next, native_db_scan}; @@ -130,6 +131,11 @@ where Function::new_native_with_env(store, env.clone(), native_secp256k1_verify), ); + env_imports.insert( + "secp256k1_recover_pubkey", + Function::new_native_with_env(store, env.clone(), native_secp256k1_recover_pubkey), + ); + // Verifies a message against a signature with a public key, using the ed25519 EdDSA scheme. // Returns 1 on verification success and 0 on failure. // Ownership of input pointers is not transferred to the host.