diff --git a/CHANGELOG.md b/CHANGELOG.md index 6623ef5085..80b047cc1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ and this project adheres to ### Added -- cosmwasm-crypto: Add new crypto-related crate. Add `secp256k1_verify`, ECDSA - secp256k1 signature verification scheme for Cosmos signature formats. ([#780]) +- cosmwasm-crypto: Add `ed25519_verify`, EdDSA ed25519 signature verification + scheme for Tendermint signature and public key formats. ([#771]) +- cosmwasm-crypto: New crypto-related crate. Add `secp256k1_verify`, ECDSA + secp256k1 signature verification scheme for Cosmos signature and public key + formats. ([#780]) - cosmwasm-vm: Add PinnedMemoryCache. ([#696]) - cosmwasm-vm: The new `Cache::analyze` provides a static analyzis of the Wasm bytecode. This is used to tell the caller if the contract exposes IBC entry diff --git a/Cargo.lock b/Cargo.lock index 433ca00369..1c26e2c5fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,7 @@ version = "0.13.2" dependencies = [ "base64", "digest", + "ed25519-zebra", "elliptic-curve", "hex", "k256", @@ -468,6 +469,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -558,6 +572,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/README.md b/README.md index b7bf4aad4d..2494ae9b50 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,11 @@ extern "C" { /// greater than 1 in case of error. fn secp256k1_verify(message_hash_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; + /// verifies a message against a signature with a public key, using the + /// ed25519 EdDSA scheme. + /// Returns 1 on verification success and 0 on failure. + fn ed25519_verify(message_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; + /// Executes a query on the chain (import). Not to be confused with the /// query export, which queries the state of the contract. fn query_chain(request: u32) -> u32; diff --git a/contracts/burner/Cargo.lock b/contracts/burner/Cargo.lock index e68f2d8ac6..4e44461184 100644 --- a/contracts/burner/Cargo.lock +++ b/contracts/burner/Cargo.lock @@ -137,6 +137,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -323,6 +324,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -413,6 +427,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/contracts/crypto-verify/Cargo.lock b/contracts/crypto-verify/Cargo.lock index e12b1b6c55..bdeec3b5c1 100644 --- a/contracts/crypto-verify/Cargo.lock +++ b/contracts/crypto-verify/Cargo.lock @@ -126,6 +126,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -334,6 +335,19 @@ dependencies = [ "sha2", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -424,6 +438,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/contracts/crypto-verify/schema/query_msg.json b/contracts/crypto-verify/schema/query_msg.json index 0fa51b24a8..98c9818683 100644 --- a/contracts/crypto-verify/schema/query_msg.json +++ b/contracts/crypto-verify/schema/query_msg.json @@ -3,12 +3,13 @@ "title": "QueryMsg", "anyOf": [ { + "description": "Cosmos format (secp256k1 verification scheme).", "type": "object", "required": [ - "verify_signature" + "verify_cosmos_signature" ], "properties": { - "verify_signature": { + "verify_cosmos_signature": { "type": "object", "required": [ "message", @@ -44,6 +45,49 @@ } } }, + { + "description": "Tendermint format (ed25519 verification scheme).", + "type": "object", + "required": [ + "verify_tendermint_signature" + ], + "properties": { + "verify_tendermint_signature": { + "type": "object", + "required": [ + "message", + "public_key", + "signature" + ], + "properties": { + "message": { + "description": "Message to verify.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "public_key": { + "description": "Serialized public key. Tendermint format (32 bytes).", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "signature": { + "description": "Serialized signature. Tendermint format (64 bytes).", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + } + }, { "description": "Returns a list of supported verification schemes. No pagination - this is a short list.", "type": "object", diff --git a/contracts/crypto-verify/src/contract.rs b/contracts/crypto-verify/src/contract.rs index 99fd770a5a..41037f302c 100644 --- a/contracts/crypto-verify/src/contract.rs +++ b/contracts/crypto-verify/src/contract.rs @@ -8,7 +8,7 @@ use crate::msg::{ list_verifications, HandleMsg, InitMsg, ListVerificationsResponse, QueryMsg, VerifyResponse, }; -pub const VERSION: &str = "crypto-verify-v1"; +pub const VERSION: &str = "crypto-verify-v2"; #[entry_point] pub fn init(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: InitMsg) -> StdResult { @@ -28,11 +28,21 @@ pub fn handle( #[entry_point] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::VerifySignature { + QueryMsg::VerifyCosmosSignature { message, signature, public_key, - } => to_binary(&query_verify( + } => to_binary(&query_verify_cosmos( + deps, + &message.0, + &signature.0, + &public_key.0, + )?), + QueryMsg::VerifyTendermintSignature { + message, + signature, + public_key, + } => to_binary(&query_verify_tendermint( deps, &message.0, &signature.0, @@ -42,7 +52,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } } -pub fn query_verify( +pub fn query_verify_cosmos( deps: Deps, message: &[u8], signature: &[u8], @@ -59,6 +69,17 @@ pub fn query_verify( } } +pub fn query_verify_tendermint( + deps: Deps, + message: &[u8], + signature: &[u8], + public_key: &[u8], +) -> StdResult { + // Verification + let verifies = deps.api.ed25519_verify(message, signature, public_key)?; + Ok(VerifyResponse { verifies }) +} + pub fn query_list_verifications(deps: Deps) -> StdResult { let verification_schemes: Vec<_> = list_verifications(deps)?; Ok(ListVerificationsResponse { @@ -76,9 +97,15 @@ mod tests { const CREATOR: &str = "creator"; - const MESSAGE_HEX: &str = "5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f0937b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb74401deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7"; - const SIGNATURE_HEX: &str = "207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"; - const PUBLIC_KEY_HEX: &str = "04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"; + const SECP256K1_MESSAGE_HEX: &str = "5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f0937b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb74401deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7"; + const SECP256K1_SIGNATURE_HEX: &str = "207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"; + const SECP256K1_PUBLIC_KEY_HEX: &str = "04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"; + + // TEST 3 test vector from https://tools.ietf.org/html/rfc8032#section-7.1 + const ED25519_MESSAGE_HEX: &str = "af82"; + const ED25519_SIGNATURE_HEX: &str = "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"; + const ED25519_PUBLIC_KEY_HEX: &str = + "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"; fn setup() -> OwnedDeps { let mut deps = mock_dependencies(&[]); @@ -95,14 +122,80 @@ mod tests { } #[test] - fn verify_works() { + fn cosmos_signature_verify_works() { + let deps = setup(); + + let message = hex::decode(SECP256K1_MESSAGE_HEX).unwrap(); + let signature = hex::decode(SECP256K1_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(SECP256K1_PUBLIC_KEY_HEX).unwrap(); + + let verify_msg = QueryMsg::VerifyCosmosSignature { + message: Binary(message), + signature: Binary(signature), + public_key: Binary(public_key), + }; + + 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 cosmos_signature_verify_fails() { + let deps = setup(); + + let mut message = hex::decode(SECP256K1_MESSAGE_HEX).unwrap(); + // alter message + message[0] ^= 0x01; + let signature = hex::decode(SECP256K1_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(SECP256K1_PUBLIC_KEY_HEX).unwrap(); + + let verify_msg = QueryMsg::VerifyCosmosSignature { + message: Binary(message), + signature: Binary(signature), + public_key: Binary(public_key), + }; + + 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 cosmos_signature_verify_errors() { + let deps = setup(); + + let message = hex::decode(SECP256K1_MESSAGE_HEX).unwrap(); + let signature = hex::decode(SECP256K1_SIGNATURE_HEX).unwrap(); + let public_key = vec![]; + + let verify_msg = QueryMsg::VerifyCosmosSignature { + message: Binary(message), + signature: Binary(signature), + public_key: Binary(public_key), + }; + + let res = query(deps.as_ref(), mock_env(), verify_msg); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err(), + StdError::VerificationErr { + source: VerificationError::PublicKeyErr + } + ) + } + + #[test] + fn tendermint_signature_verify_works() { let deps = setup(); - let message = hex::decode(MESSAGE_HEX).unwrap(); - let signature = hex::decode(SIGNATURE_HEX).unwrap(); - let public_key = hex::decode(PUBLIC_KEY_HEX).unwrap(); + let message = hex::decode(ED25519_MESSAGE_HEX).unwrap(); + let signature = hex::decode(ED25519_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBLIC_KEY_HEX).unwrap(); - let verify_msg = QueryMsg::VerifySignature { + let verify_msg = QueryMsg::VerifyTendermintSignature { message: Binary(message), signature: Binary(signature), public_key: Binary(public_key), @@ -115,16 +208,16 @@ mod tests { } #[test] - fn verify_fails() { + fn tendermint_signature_verify_fails() { let deps = setup(); - let mut message = hex::decode(MESSAGE_HEX).unwrap(); + let mut message = hex::decode(ED25519_MESSAGE_HEX).unwrap(); // alter message message[0] ^= 0x01; - let signature = hex::decode(SIGNATURE_HEX).unwrap(); - let public_key = hex::decode(PUBLIC_KEY_HEX).unwrap(); + let signature = hex::decode(ED25519_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBLIC_KEY_HEX).unwrap(); - let verify_msg = QueryMsg::VerifySignature { + let verify_msg = QueryMsg::VerifyTendermintSignature { message: Binary(message), signature: Binary(signature), public_key: Binary(public_key), @@ -137,14 +230,14 @@ mod tests { } #[test] - fn verify_errors() { + fn tendermint_signature_verify_errors() { let deps = setup(); - let message = hex::decode(MESSAGE_HEX).unwrap(); - let signature = hex::decode(SIGNATURE_HEX).unwrap(); + let message = hex::decode(ED25519_MESSAGE_HEX).unwrap(); + let signature = hex::decode(ED25519_SIGNATURE_HEX).unwrap(); let public_key = vec![]; - let verify_msg = QueryMsg::VerifySignature { + let verify_msg = QueryMsg::VerifyTendermintSignature { message: Binary(message), signature: Binary(signature), public_key: Binary(public_key), @@ -171,7 +264,7 @@ mod tests { assert_eq!( res, ListVerificationsResponse { - verification_schemes: vec!["secp256k1".into()] + verification_schemes: vec!["secp256k1".into(), "ed25519".into()] } ); } diff --git a/contracts/crypto-verify/src/msg.rs b/contracts/crypto-verify/src/msg.rs index 84c5d1c344..379473d030 100644 --- a/contracts/crypto-verify/src/msg.rs +++ b/contracts/crypto-verify/src/msg.rs @@ -14,7 +14,8 @@ pub enum HandleMsg {} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - VerifySignature { + /// Cosmos format (secp256k1 verification scheme). + VerifyCosmosSignature { /// Message to verify. message: Binary, /// Serialized signature. Cosmos format (64 bytes). @@ -22,6 +23,15 @@ pub enum QueryMsg { /// Serialized compressed (33 bytes) or uncompressed (65 bytes) public key. public_key: Binary, }, + /// Tendermint format (ed25519 verification scheme). + VerifyTendermintSignature { + /// Message to verify. + message: Binary, + /// Serialized signature. Tendermint format (64 bytes). + signature: Binary, + /// Serialized public key. Tendermint format (32 bytes). + public_key: Binary, + }, /// Returns a list of supported verification schemes. /// No pagination - this is a short list. ListVerificationSchemes {}, @@ -38,5 +48,5 @@ pub struct ListVerificationsResponse { } pub(crate) fn list_verifications(_deps: Deps) -> StdResult> { - Ok(vec!["secp256k1".into()]) + Ok(vec!["secp256k1".into(), "ed25519".into()]) } diff --git a/contracts/crypto-verify/tests/integration.rs b/contracts/crypto-verify/tests/integration.rs index 03a40cbee2..1b50dbbcb8 100644 --- a/contracts/crypto-verify/tests/integration.rs +++ b/contracts/crypto-verify/tests/integration.rs @@ -33,9 +33,14 @@ static WASM: &[u8] = include_bytes!("../target/wasm32-unknown-unknown/release/cr const CREATOR: &str = "creator"; -const MESSAGE_HEX: &str = "5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f0937b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb74401deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7"; -const SIGNATURE_HEX: &str = "207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"; -const PUBLIC_KEY_HEX: &str = "04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"; +const SECP256K1_MESSAGE_HEX: &str = "5c868fedb8026979ebd26f1ba07c27eedf4ff6d10443505a96ecaf21ba8c4f0937b3cd23ffdc3dd429d4cd1905fb8dbcceeff1350020e18b58d2ba70887baa3a9b783ad30d3fbf210331cdd7df8d77defa398cdacdfc2e359c7ba4cae46bb74401deb417f8b912a1aa966aeeba9c39c7dd22479ae2b30719dca2f2206c5eb4b7"; +const SECP256K1_SIGNATURE_HEX: &str = "207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"; +const SECP256K1_PUBLIC_KEY_HEX: &str = "04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"; + +const ED25519_MESSAGE_HEX: &str = "af82"; +const ED25519_SIGNATURE_HEX: &str = "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"; +const ED25519_PUBLIC_KEY_HEX: &str = + "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"; fn setup() -> Instance { let mut deps = mock_instance(WASM, &[]); @@ -52,14 +57,73 @@ fn init_works() { } #[test] -fn verify_works() { +fn cosmos_signature_verify_works() { + let mut deps = setup(); + + let message = hex::decode(SECP256K1_MESSAGE_HEX).unwrap(); + let signature = hex::decode(SECP256K1_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(SECP256K1_PUBLIC_KEY_HEX).unwrap(); + + let verify_msg = QueryMsg::VerifyCosmosSignature { + message: Binary(message), + signature: Binary(signature), + public_key: Binary(public_key), + }; + + 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 cosmos_signature_verify_fails() { + let mut deps = setup(); + + let mut message = hex::decode(SECP256K1_MESSAGE_HEX).unwrap(); + // alter hash + message[0] ^= 0x01; + let signature = hex::decode(SECP256K1_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(SECP256K1_PUBLIC_KEY_HEX).unwrap(); + + let verify_msg = QueryMsg::VerifyCosmosSignature { + message: Binary(message), + signature: Binary(signature), + public_key: Binary(public_key), + }; + + 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 cosmos_signature_verify_errors() { + let mut deps = setup(); + + let message = hex::decode(SECP256K1_MESSAGE_HEX).unwrap(); + let signature = hex::decode(SECP256K1_SIGNATURE_HEX).unwrap(); + let public_key = vec![]; + + let verify_msg = QueryMsg::VerifyCosmosSignature { + message: Binary(message), + signature: Binary(signature), + public_key: Binary(public_key), + }; + let res = query(&mut deps, mock_env(), verify_msg); + assert_eq!(res.unwrap_err(), "Verification error: Public key error") +} + +#[test] +fn tendermint_signature_verify_works() { let mut deps = setup(); - let message = hex::decode(MESSAGE_HEX).unwrap(); - let signature = hex::decode(SIGNATURE_HEX).unwrap(); - let public_key = hex::decode(PUBLIC_KEY_HEX).unwrap(); + let message = hex::decode(ED25519_MESSAGE_HEX).unwrap(); + let signature = hex::decode(ED25519_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBLIC_KEY_HEX).unwrap(); - let verify_msg = QueryMsg::VerifySignature { + let verify_msg = QueryMsg::VerifyTendermintSignature { message: Binary(message), signature: Binary(signature), public_key: Binary(public_key), @@ -72,16 +136,16 @@ fn verify_works() { } #[test] -fn verify_fails() { +fn tendermint_signature_verify_fails() { let mut deps = setup(); - let mut message = hex::decode(MESSAGE_HEX).unwrap(); + let mut message = hex::decode(ED25519_MESSAGE_HEX).unwrap(); // alter hash message[0] ^= 0x01; - let signature = hex::decode(SIGNATURE_HEX).unwrap(); - let public_key = hex::decode(PUBLIC_KEY_HEX).unwrap(); + let signature = hex::decode(ED25519_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBLIC_KEY_HEX).unwrap(); - let verify_msg = QueryMsg::VerifySignature { + let verify_msg = QueryMsg::VerifyTendermintSignature { message: Binary(message), signature: Binary(signature), public_key: Binary(public_key), @@ -94,14 +158,14 @@ fn verify_fails() { } #[test] -fn verify_errors() { +fn tendermint_signature_verify_errors() { let mut deps = setup(); - let message = hex::decode(MESSAGE_HEX).unwrap(); - let signature = hex::decode(SIGNATURE_HEX).unwrap(); + let message = hex::decode(ED25519_MESSAGE_HEX).unwrap(); + let signature = hex::decode(ED25519_SIGNATURE_HEX).unwrap(); let public_key = vec![]; - let verify_msg = QueryMsg::VerifySignature { + let verify_msg = QueryMsg::VerifyTendermintSignature { message: Binary(message), signature: Binary(signature), public_key: Binary(public_key), @@ -122,7 +186,7 @@ fn query_works() { assert_eq!( res, ListVerificationsResponse { - verification_schemes: vec!["secp256k1".into()] + verification_schemes: vec!["secp256k1".into(), "ed25519".into()] } ); } diff --git a/contracts/hackatom/Cargo.lock b/contracts/hackatom/Cargo.lock index 836d72e728..f09bc80aaa 100644 --- a/contracts/hackatom/Cargo.lock +++ b/contracts/hackatom/Cargo.lock @@ -126,6 +126,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -320,6 +321,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -410,6 +424,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/contracts/ibc-reflect-send/Cargo.lock b/contracts/ibc-reflect-send/Cargo.lock index 470b591ad0..ed125f617a 100644 --- a/contracts/ibc-reflect-send/Cargo.lock +++ b/contracts/ibc-reflect-send/Cargo.lock @@ -126,6 +126,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -320,6 +321,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -410,6 +424,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/contracts/ibc-reflect/Cargo.lock b/contracts/ibc-reflect/Cargo.lock index 2c85adbfa4..a3c25ed69e 100644 --- a/contracts/ibc-reflect/Cargo.lock +++ b/contracts/ibc-reflect/Cargo.lock @@ -126,6 +126,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -320,6 +321,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -410,6 +424,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/contracts/queue/Cargo.lock b/contracts/queue/Cargo.lock index 2b10719229..1f92cb957f 100644 --- a/contracts/queue/Cargo.lock +++ b/contracts/queue/Cargo.lock @@ -126,6 +126,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -312,6 +313,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -402,6 +416,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/contracts/reflect/Cargo.lock b/contracts/reflect/Cargo.lock index 7abd9ce80b..66a9ee15e0 100644 --- a/contracts/reflect/Cargo.lock +++ b/contracts/reflect/Cargo.lock @@ -126,6 +126,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -320,6 +321,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -410,6 +424,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/contracts/staking/Cargo.lock b/contracts/staking/Cargo.lock index e1024135ed..7a7ccf40c1 100644 --- a/contracts/staking/Cargo.lock +++ b/contracts/staking/Cargo.lock @@ -126,6 +126,7 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "digest", + "ed25519-zebra", "k256", "thiserror", ] @@ -320,6 +321,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "darling" version = "0.10.2" @@ -416,6 +430,20 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-zebra" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "either" version = "1.6.1" diff --git a/packages/crypto/Cargo.toml b/packages/crypto/Cargo.toml index 09232bb98c..277e7f07b7 100644 --- a/packages/crypto/Cargo.toml +++ b/packages/crypto/Cargo.toml @@ -16,6 +16,7 @@ backtraces = [] [dependencies] k256 = { version = "0.7.2", features = ["ecdsa"] } +ed25519-zebra = "2" digest = "0.9" thiserror = "1.0" diff --git a/packages/crypto/README.md b/packages/crypto/README.md index 165e7e69dd..039b456d37 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -10,6 +10,8 @@ and [cosmwasm-std](`https://crates.io/crates/cosmwasm-std`) crates. - `secp256k1_verify()`: Digital signature verification using the ECDSA sepc256k1 scheme, for Cosmos signature / public key formats. +- `ed25519_verify()`: Digital signature verification using the EdDSA ed25519 + scheme, for Tendemint signature / public key formats. ## License diff --git a/packages/crypto/src/ed25519.rs b/packages/crypto/src/ed25519.rs new file mode 100644 index 0000000000..49ce649286 --- /dev/null +++ b/packages/crypto/src/ed25519.rs @@ -0,0 +1,182 @@ +use ed25519_zebra as ed25519; +use std::convert::TryFrom; + +use crate::errors::{CryptoError, CryptoResult}; + +/// Max length of a message for ed25519 verification in bytes. +/// This is an arbitrary value, for performance / memory contraints. If you need to verify larger +/// messages, let us know. +pub const MESSAGE_MAX_LEN: usize = 131072; + +/// EdDSA (ed25519) parameters +/// Length of a serialized signature +pub const EDDSA_SIGNATURE_LEN: usize = 64; + +/// Length of a serialized public key +pub const EDDSA_PUBKEY_LEN: usize = 32; + +/// EdDSA ed25519 implementation. +/// +/// This function verifies messages against a signature, with the public key of the signer, +/// using the ed25519 elliptic curve digital signature parametrization / algorithm. +/// +/// The maximum currently supported message length is 4096 bytes. +/// The signature and public key are in [Tendermint](https://docs.tendermint.com/v0.32/spec/blockchain/encoding.html#public-key-cryptography) +/// format: +/// - signature: raw ED25519 signature (64 bytes). +/// - public key: raw ED25519 public key (32 bytes). +pub fn ed25519_verify(message: &[u8], signature: &[u8], public_key: &[u8]) -> CryptoResult { + if message.len() > MESSAGE_MAX_LEN { + return Err(CryptoError::msg_err(format!( + "too large: {}", + message.len() + ))); + } + if signature.len() != EDDSA_SIGNATURE_LEN { + return Err(CryptoError::sig_err(format!( + "wrong / unsupported length: {}", + signature.len() + ))); + } + let pubkey_len = public_key.len(); + if pubkey_len == 0 { + return Err(CryptoError::pubkey_err("empty")); + } + if pubkey_len != EDDSA_PUBKEY_LEN { + return Err(CryptoError::pubkey_err(format!( + "wrong / unsupported length: {}", + pubkey_len, + ))); + } + // Deserialize + let signature = ed25519::Signature::try_from(signature) + .map_err(|err| CryptoError::generic_err(err.to_string()))?; + + let public_key = ed25519::VerificationKey::try_from(public_key) + .map_err(|err| CryptoError::generic_err(err.to_string()))?; + + match public_key.verify(&signature, &message) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::OsRng; + + // For generic signature verification + const MSG: &str = "Hello World!"; + + // Cosmos ed25519 signature verification + // TEST 1 from https://tools.ietf.org/html/rfc8032#section-7.1 + const COSMOS_ED25519_MSG: &str = ""; + const COSMOS_ED25519_PRIVATE_KEY_HEX: &str = + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"; + const COSMOS_ED25519_PUBLIC_KEY_HEX: &str = + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"; + const COSMOS_ED25519_SIGNATURE_HEX: &str = "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"; + + // Test data from https://tools.ietf.org/html/rfc8032#section-7.1 + const COSMOS_ED25519_TESTS_JSON: &str = "./testdata/ed25519_tests.json"; + + #[test] + fn test_ed25519_verify() { + let message = MSG.as_bytes(); + // Signing + let secret_key = ed25519::SigningKey::new(&mut OsRng); + let signature = secret_key.sign(&message); + + let public_key = ed25519::VerificationKey::from(&secret_key); + + // Serialization. Types can be converted to raw byte arrays with From/Into + let signature_bytes: [u8; 64] = signature.into(); + let public_key_bytes: [u8; 32] = public_key.into(); + + // Verification + assert!(ed25519_verify(&message, &signature_bytes, &public_key_bytes).unwrap()); + + // Wrong message fails + let bad_message = [message, b"\0"].concat(); + assert!(!ed25519_verify(&bad_message, &signature_bytes, &public_key_bytes).unwrap()); + + // Other pubkey fails + let other_secret_key = ed25519::SigningKey::new(&mut OsRng); + let other_public_key = ed25519::VerificationKey::from(&other_secret_key); + let other_public_key_bytes: [u8; 32] = other_public_key.into(); + assert!(!ed25519_verify(&message, &signature_bytes, &other_public_key_bytes).unwrap()); + } + + #[test] + fn test_cosmos_ed25519_verify() { + let secret_key = ed25519::SigningKey::try_from( + hex::decode(COSMOS_ED25519_PRIVATE_KEY_HEX) + .unwrap() + .as_slice(), + ) + .unwrap(); + let public_key = ed25519::VerificationKey::try_from( + hex::decode(COSMOS_ED25519_PUBLIC_KEY_HEX) + .unwrap() + .as_slice(), + ) + .unwrap(); + let signature = secret_key.sign(&COSMOS_ED25519_MSG.as_bytes()); + + let signature_bytes: [u8; 64] = signature.into(); + let public_key_bytes: [u8; 32] = public_key.into(); + + assert_eq!( + signature_bytes, + hex::decode(&COSMOS_ED25519_SIGNATURE_HEX) + .unwrap() + .as_slice() + ); + + assert!(ed25519_verify( + &COSMOS_ED25519_MSG.as_bytes(), + &signature_bytes, + &public_key_bytes + ) + .unwrap()); + } + + #[test] + fn test_cosmos_extra_ed25519_verify() { + use std::fs::File; + use std::io::BufReader; + + use serde::Deserialize; + + #[derive(Deserialize, Debug)] + struct Encoded { + #[serde(rename = "privkey")] + private_key: String, + #[serde(rename = "pubkey")] + public_key: String, + message: String, + signature: String, + } + + // Open the file in read-only mode with buffer. + let file = File::open(COSMOS_ED25519_TESTS_JSON).unwrap(); + let reader = BufReader::new(file); + + let codes: Vec = serde_json::from_reader(reader).unwrap(); + + for (i, encoded) in (1..).zip(codes) { + let message = hex::decode(&encoded.message).unwrap(); + + let signature = hex::decode(&encoded.signature).unwrap(); + + let public_key = hex::decode(&encoded.public_key).unwrap(); + + // ed25519_verify() works + assert!( + ed25519_verify(&message, &signature, &public_key).unwrap(), + format!("verify() failed (test case {})", i) + ); + } + } +} diff --git a/packages/crypto/src/errors.rs b/packages/crypto/src/errors.rs index bac021941b..4fe84721a9 100644 --- a/packages/crypto/src/errors.rs +++ b/packages/crypto/src/errors.rs @@ -14,6 +14,13 @@ pub enum CryptoError { #[cfg(feature = "backtraces")] backtrace: Backtrace, }, + #[error("Message error: {msg}")] + MessageError { + msg: String, + error_code: u32, + #[cfg(feature = "backtraces")] + backtrace: Backtrace, + }, #[error("Hash error: {msg}")] HashErr { msg: String, @@ -47,6 +54,15 @@ impl CryptoError { } } + pub fn msg_err>(msg: S) -> Self { + CryptoError::MessageError { + msg: msg.into(), + error_code: 2, + #[cfg(feature = "backtraces")] + backtrace: Backtrace::capture(), + } + } + pub fn hash_err>(msg: S) -> Self { CryptoError::HashErr { msg: msg.into(), @@ -91,6 +107,17 @@ mod tests { } } + #[test] + fn msg_err_works() { + let error = CryptoError::msg_err("something went wrong with the msg"); + match error { + CryptoError::MessageError { msg, .. } => { + assert_eq!(msg, "something went wrong with the msg") + } + _ => panic!("wrong error type!"), + } + } + #[test] fn hash_err_works() { let error = CryptoError::hash_err("something went wrong with the hash"); diff --git a/packages/crypto/src/lib.rs b/packages/crypto/src/lib.rs index ef30321cdb..4c6a4e7232 100644 --- a/packages/crypto/src/lib.rs +++ b/packages/crypto/src/lib.rs @@ -4,13 +4,18 @@ //! This crate does not adhere to semantic versioning. #![cfg_attr(feature = "backtraces", feature(backtrace))] -mod crypto; +mod ed25519; mod errors; mod identity_digest; +mod secp256k1; #[doc(hidden)] -pub use crate::crypto::secp256k1_verify; +pub use crate::ed25519::ed25519_verify; #[doc(hidden)] -pub use crate::crypto::{MESSAGE_HASH_MAX_LENGTH, PUBKEY_MAX_LENGTH, SIGNATURE_MAX_LENGTH}; +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; +#[doc(hidden)] +pub use crate::secp256k1::{ECDSA_PUBKEY_MAX_LEN, ECDSA_SIGNATURE_LEN, MESSAGE_HASH_MAX_LEN}; diff --git a/packages/crypto/src/crypto.rs b/packages/crypto/src/secp256k1.rs similarity index 62% rename from packages/crypto/src/crypto.rs rename to packages/crypto/src/secp256k1.rs index 2428655bf5..66cd46052a 100644 --- a/packages/crypto/src/crypto.rs +++ b/packages/crypto/src/secp256k1.rs @@ -10,30 +10,30 @@ use crate::identity_digest::Identity256; /// Max length of a message hash for secp256k1 verification in bytes. /// This is typically a 32 byte output of e.g. SHA-256 or Keccak256. In theory shorter values /// are possible but currently not supported by the implementation. Let us know when you need them. -pub const MESSAGE_HASH_MAX_LENGTH: usize = 32; +pub const MESSAGE_HASH_MAX_LEN: usize = 32; +/// ECDSA (secp256k1) parameters /// Length of a serialized signature -const SIGNATURE_LENGTH: usize = 64; -/// Max length of a serialized signature -pub const SIGNATURE_MAX_LENGTH: usize = SIGNATURE_LENGTH; +pub const ECDSA_SIGNATURE_LEN: usize = 64; /// Compressed public key prefix (variant 1) -const COMPRESSED_PUBKEY_PREFIX_1: u8 = 0x02; +const ECDSA_COMPRESSED_PUBKEY_PREFIX_1: u8 = 0x02; /// Compressed public key prefix (variant 2) -const COMPRESSED_PUBKEY_PREFIX_2: u8 = 0x03; +const ECDSA_COMPRESSED_PUBKEY_PREFIX_2: u8 = 0x03; /// Length of a serialized compressed public key -const COMPRESSED_PUBKEY_LENGTH: usize = 33; +const ECDSA_COMPRESSED_PUBKEY_LEN: usize = 33; /// Uncompressed public key prefix -const UNCOMPRESSED_PUBKEY_PREFIX: u8 = 0x04; +const ECDSA_UNCOMPRESSED_PUBKEY_PREFIX: u8 = 0x04; /// Length of a serialized uncompressed public key -const UNCOMPRESSED_PUBKEY_LENGTH: usize = 65; +const ECDSA_UNCOMPRESSED_PUBKEY_LEN: usize = 65; /// Max length of a serialized public key -pub const PUBKEY_MAX_LENGTH: usize = UNCOMPRESSED_PUBKEY_LENGTH; +pub const ECDSA_PUBKEY_MAX_LEN: usize = ECDSA_UNCOMPRESSED_PUBKEY_LEN; /// ECDSA secp256k1 implementation. /// /// This function verifies message hashes (typically, hashed unsing SHA-256) against a signature, -/// with the public key of the signer. +/// with the public key of the signer, using the secp256k1 elliptic curve digital signature +/// parametrization / algorithm. /// /// The signature and public key are in "Cosmos" format: /// - signature: Serialized "compact" signature (64 bytes). @@ -44,13 +44,13 @@ pub fn secp256k1_verify( signature: &[u8], public_key: &[u8], ) -> CryptoResult { - if message_hash.len() != MESSAGE_HASH_MAX_LENGTH { + if message_hash.len() != MESSAGE_HASH_MAX_LEN { return Err(CryptoError::hash_err(format!( "wrong length: {}", message_hash.len() ))); } - if signature.len() != SIGNATURE_LENGTH { + if signature.len() != ECDSA_SIGNATURE_LEN { return Err(CryptoError::sig_err(format!( "wrong / unsupported length: {}", signature.len() @@ -61,10 +61,11 @@ pub fn secp256k1_verify( return Err(CryptoError::pubkey_err("empty")); } let pubkey_fmt = public_key[0]; - if !(pubkey_len == UNCOMPRESSED_PUBKEY_LENGTH && pubkey_fmt == UNCOMPRESSED_PUBKEY_PREFIX - || pubkey_len == COMPRESSED_PUBKEY_LENGTH - && (pubkey_fmt == COMPRESSED_PUBKEY_PREFIX_1 - || pubkey_fmt == COMPRESSED_PUBKEY_PREFIX_2)) + if !(pubkey_len == ECDSA_UNCOMPRESSED_PUBKEY_LEN + && pubkey_fmt == ECDSA_UNCOMPRESSED_PUBKEY_PREFIX + || pubkey_len == ECDSA_COMPRESSED_PUBKEY_LEN + && (pubkey_fmt == ECDSA_COMPRESSED_PUBKEY_PREFIX_1 + || pubkey_fmt == ECDSA_COMPRESSED_PUBKEY_PREFIX_2)) { return Err(CryptoError::pubkey_err(format!( "wrong / unsupported length/format: {}/{}", @@ -104,23 +105,23 @@ mod tests { }; use sha2::Sha256; - // Generic signature verification + // For generic signature verification const MSG: &str = "Hello World!"; - // Cosmos signature verification + // Cosmos secp256k1 signature verification // tendermint/PubKeySecp256k1 pubkey - const COSMOS_PUBKEY_BASE64: &str = "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"; + const COSMOS_SECP256K1_PUBKEY_BASE64: &str = "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"; - const COSMOS_MSG_HEX1: &str = "0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d12073132333435363712650a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a02080112130a0d0a0575636f736d12043230303010c09a0c1a0c73696d642d74657374696e672001"; - const COSMOS_MSG_HEX2: &str = "0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d12073132333435363712670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180112130a0d0a0575636f736d12043230303010c09a0c1a0c73696d642d74657374696e672001"; - const COSMOS_MSG_HEX3: &str = "0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d12073132333435363712670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180212130a0d0a0575636f736d12043230303010c09a0c1a0c73696d642d74657374696e672001"; + const COSMOS_SECP256K1_MSG_HEX1: &str = "0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d12073132333435363712650a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a02080112130a0d0a0575636f736d12043230303010c09a0c1a0c73696d642d74657374696e672001"; + const COSMOS_SECP256K1_MSG_HEX2: &str = "0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d12073132333435363712670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180112130a0d0a0575636f736d12043230303010c09a0c1a0c73696d642d74657374696e672001"; + const COSMOS_SECP256K1_MSG_HEX3: &str = "0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d12073132333435363712670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a020801180212130a0d0a0575636f736d12043230303010c09a0c1a0c73696d642d74657374696e672001"; - const COSMOS_SIGNATURE_HEX1: &str = "c9dd20e07464d3a688ff4b710b1fbc027e495e797cfa0b4804da2ed117959227772de059808f765aa29b8f92edf30f4c2c5a438e30d3fe6897daa7141e3ce6f9"; - const COSMOS_SIGNATURE_HEX2: &str = "525adc7e61565a509c60497b798c549fbf217bb5cd31b24cc9b419d098cc95330c99ecc4bc72448f85c365a4e3f91299a3d40412fb3751bab82f1940a83a0a4c"; - const COSMOS_SIGNATURE_HEX3: &str = "f3f2ca73806f2abbf6e0fe85f9b8af66f0e9f7f79051fdb8abe5bb8633b17da132e82d577b9d5f7a6dae57a144efc9ccc6eef15167b44b3b22a57240109762af"; + const COSMOS_SECP256K1_SIGNATURE_HEX1: &str = "c9dd20e07464d3a688ff4b710b1fbc027e495e797cfa0b4804da2ed117959227772de059808f765aa29b8f92edf30f4c2c5a438e30d3fe6897daa7141e3ce6f9"; + const COSMOS_SECP256K1_SIGNATURE_HEX2: &str = "525adc7e61565a509c60497b798c549fbf217bb5cd31b24cc9b419d098cc95330c99ecc4bc72448f85c365a4e3f91299a3d40412fb3751bab82f1940a83a0a4c"; + const COSMOS_SECP256K1_SIGNATURE_HEX3: &str = "f3f2ca73806f2abbf6e0fe85f9b8af66f0e9f7f79051fdb8abe5bb8633b17da132e82d577b9d5f7a6dae57a144efc9ccc6eef15167b44b3b22a57240109762af"; // Test data originally from https://github.com/cosmos/cosmjs/blob/v0.24.0-alpha.22/packages/crypto/src/secp256k1.spec.ts#L195-L394 - const COSMOS_TESTS_JSON: &str = "./testdata/secp256k1_tests.json"; + const COSMOS_SECP256K1_TESTS_JSON: &str = "./testdata/secp256k1_tests.json"; #[test] fn test_secp256k1_verify() { @@ -176,14 +177,18 @@ mod tests { #[test] fn test_cosmos_secp256k1_verify() { - let public_key = base64::decode(COSMOS_PUBKEY_BASE64).unwrap(); + let public_key = base64::decode(COSMOS_SECP256K1_PUBKEY_BASE64).unwrap(); for ((i, msg), sig) in (1..) - .zip(&[COSMOS_MSG_HEX1, COSMOS_MSG_HEX2, COSMOS_MSG_HEX3]) .zip(&[ - COSMOS_SIGNATURE_HEX1, - COSMOS_SIGNATURE_HEX2, - COSMOS_SIGNATURE_HEX3, + COSMOS_SECP256K1_MSG_HEX1, + COSMOS_SECP256K1_MSG_HEX2, + COSMOS_SECP256K1_MSG_HEX3, + ]) + .zip(&[ + COSMOS_SECP256K1_SIGNATURE_HEX1, + COSMOS_SECP256K1_SIGNATURE_HEX2, + COSMOS_SECP256K1_SIGNATURE_HEX3, ]) { let message = hex::decode(msg).unwrap(); @@ -217,7 +222,7 @@ mod tests { } // Open the file in read-only mode with buffer. - let file = File::open(COSMOS_TESTS_JSON).unwrap(); + let file = File::open(COSMOS_SECP256K1_TESTS_JSON).unwrap(); let reader = BufReader::new(file); let codes: Vec = serde_json::from_reader(reader).unwrap(); diff --git a/packages/crypto/testdata/ed25519_tests.json b/packages/crypto/testdata/ed25519_tests.json new file mode 100644 index 0000000000..cc86fce9ff --- /dev/null +++ b/packages/crypto/testdata/ed25519_tests.json @@ -0,0 +1,32 @@ +[ + { + "privkey": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "pubkey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "message": "", + "signature": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + }, + { + "privkey": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "pubkey": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", + "message": "72", + "signature": "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00" + }, + { + "privkey": "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7", + "pubkey": "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", + "message": "af82", + "signature": "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a" + }, + { + "privkey": "f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5", + "pubkey": "278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e", + "message": "08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0", + "signature": "0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03" + }, + { + "privkey": "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42", + "pubkey": "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf", + "message": "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + "signature": "dc2a4459e7369633a52b1bf277839a00201009a3efbf3ecb69bea2186c26b58909351fc9ac90b3ecfdfbc7c66431e0303dca179c138ac17ad9bef1177331a704" + } +] diff --git a/packages/std/src/errors/verification_error.rs b/packages/std/src/errors/verification_error.rs index 7e517a0c88..ef35bf26c3 100644 --- a/packages/std/src/errors/verification_error.rs +++ b/packages/std/src/errors/verification_error.rs @@ -10,6 +10,8 @@ use cosmwasm_crypto::CryptoError; pub enum VerificationError { #[error("Generic error")] GenericErr, + #[error("Message error")] + MessageErr, #[error("Hash error")] HashErr, #[error("Signature error")] @@ -38,6 +40,7 @@ impl PartialEq for VerificationError { fn eq(&self, rhs: &VerificationError) -> bool { match self { VerificationError::GenericErr => matches!(rhs, VerificationError::GenericErr), + VerificationError::MessageErr => matches!(rhs, VerificationError::MessageErr), VerificationError::HashErr => matches!(rhs, VerificationError::HashErr), VerificationError::SignatureErr => matches!(rhs, VerificationError::SignatureErr), VerificationError::PublicKeyErr => matches!(rhs, VerificationError::PublicKeyErr), @@ -60,6 +63,7 @@ impl PartialEq for VerificationError { impl From for VerificationError { fn from(original: CryptoError) -> Self { match original { + CryptoError::MessageError { .. } => VerificationError::MessageErr, CryptoError::HashErr { .. } => VerificationError::HashErr, CryptoError::SignatureErr { .. } => VerificationError::SignatureErr, CryptoError::PublicKeyErr { .. } => VerificationError::PublicKeyErr, diff --git a/packages/std/src/imports.rs b/packages/std/src/imports.rs index b75f500ffe..71e27c1b5d 100644 --- a/packages/std/src/imports.rs +++ b/packages/std/src/imports.rs @@ -38,6 +38,7 @@ 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 ed25519_verify(message_ptr: u32, signature_ptr: u32, public_key_ptr: u32) -> u32; fn debug(source_ptr: u32); @@ -199,6 +200,7 @@ impl Api for ExternalApi { match result { 0 => Ok(true), 1 => Ok(false), + 2 => Err(VerificationError::MessageErr), // shouldn't happen 3 => Err(VerificationError::HashErr), 4 => Err(VerificationError::SignatureErr), 5 => Err(VerificationError::PublicKeyErr), @@ -207,6 +209,32 @@ impl Api for ExternalApi { } } + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + let msg_send = build_region(message); + let msg_send_ptr = &*msg_send as *const Region as u32; + let sig_send = build_region(signature); + let sig_send_ptr = &*sig_send as *const Region as u32; + let pubkey_send = build_region(public_key); + let pubkey_send_ptr = &*pubkey_send as *const Region as u32; + + let result = unsafe { ed25519_verify(msg_send_ptr, sig_send_ptr, pubkey_send_ptr) }; + match result { + 0 => Ok(true), + 1 => Ok(false), + 2 => Err(VerificationError::MessageErr), + 3 => Err(VerificationError::HashErr), // shouldn't happen + 4 => Err(VerificationError::SignatureErr), + 5 => Err(VerificationError::PublicKeyErr), + 10 => Err(VerificationError::GenericErr), + error_code => Err(VerificationError::unknown_err(error_code)), + } + } + fn debug(&self, message: &str) { // keep the boxes in scope, so we free it at the end (don't cast to pointers same line as build_region) let region = build_region(message.as_bytes()); diff --git a/packages/std/src/mock.rs b/packages/std/src/mock.rs index db10c90d94..cf97c677f2 100644 --- a/packages/std/src/mock.rs +++ b/packages/std/src/mock.rs @@ -133,6 +133,17 @@ impl Api for MockApi { )?) } + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + Ok(cosmwasm_crypto::ed25519_verify( + message, signature, public_key, + )?) + } + fn debug(&self, message: &str) { println!("{}", message); } @@ -482,6 +493,11 @@ mod tests { const SECP256K1_SIG_HEX: &str = "207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"; const SECP256K1_PUBKEY_HEX: &str = "04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"; + const ED25519_MSG_HEX: &str = "72"; + const ED25519_SIG_HEX: &str = "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"; + const ED25519_PUBKEY_HEX: &str = + "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"; + #[test] fn mock_info_arguments() { let name = HumanAddr("my name".to_string()); @@ -570,7 +586,45 @@ mod tests { let public_key = vec![]; let res = api.secp256k1_verify(&hash, &signature, &public_key); + assert_eq!(res.unwrap_err(), VerificationError::PublicKeyErr); + } + + // Basic "works" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn ed25519_verify_works() { + let api = MockApi::default(); + + let hash = hex::decode(ED25519_MSG_HEX).unwrap(); + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBKEY_HEX).unwrap(); + + assert!(api.ed25519_verify(&hash, &signature, &public_key).unwrap()); + } + + // Basic "fails" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn ed25519_verify_fails() { + let api = MockApi::default(); + + let mut msg = hex::decode(ED25519_MSG_HEX).unwrap(); + // alter msg + msg[0] ^= 0x01; + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = hex::decode(ED25519_PUBKEY_HEX).unwrap(); + + assert!(!api.ed25519_verify(&msg, &signature, &public_key).unwrap()); + } + + // Basic "errors" test. Exhaustive tests on VM's side (packages/vm/src/imports.rs) + #[test] + fn ed25519_verify_errs() { + let api = MockApi::default(); + + let msg = hex::decode(ED25519_MSG_HEX).unwrap(); + let signature = hex::decode(ED25519_SIG_HEX).unwrap(); + let public_key = vec![]; + let res = api.ed25519_verify(&msg, &signature, &public_key); assert_eq!(res.unwrap_err(), VerificationError::PublicKeyErr); } diff --git a/packages/std/src/traits.rs b/packages/std/src/traits.rs index 8375a9464f..b0bc711ccb 100644 --- a/packages/std/src/traits.rs +++ b/packages/std/src/traits.rs @@ -74,6 +74,13 @@ pub trait Api { public_key: &[u8], ) -> Result; + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result; + /// Emits a debugging message that is handled depending on the environment (typically printed to console or ignored). /// Those messages are not persisted to chain. fn debug(&self, message: &str); diff --git a/packages/vm/src/compatibility.rs b/packages/vm/src/compatibility.rs index 117eecf13c..e15fe015a5 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.ed25519_verify", "env.debug", "env.query_chain", #[cfg(feature = "iterator")] @@ -321,6 +322,7 @@ mod tests { (import "env" "canonicalize_address" (func (param i32 i32) (result i32))) (import "env" "humanize_address" (func (param i32 i32) (result i32))) (import "env" "secp256k1_verify" (func (param i32 i32 i32) (result i32))) + (import "env" "ed25519_verify" (func (param i32 i32 i32) (result i32))) )"#, ) .unwrap(); diff --git a/packages/vm/src/imports.rs b/packages/vm/src/imports.rs index 9752de4b9c..6887ae0f49 100644 --- a/packages/vm/src/imports.rs +++ b/packages/vm/src/imports.rs @@ -3,9 +3,12 @@ #[cfg(feature = "iterator")] use std::convert::TryInto; -use cosmwasm_crypto::secp256k1_verify; use cosmwasm_crypto::CryptoError; -use cosmwasm_crypto::{MESSAGE_HASH_MAX_LENGTH, PUBKEY_MAX_LENGTH, SIGNATURE_MAX_LENGTH}; +use cosmwasm_crypto::{ed25519_verify, secp256k1_verify}; +use cosmwasm_crypto::{ + ECDSA_PUBKEY_MAX_LEN, ECDSA_SIGNATURE_LEN, EDDSA_PUBKEY_LEN, EDDSA_SIGNATURE_LEN, + MESSAGE_HASH_MAX_LEN, MESSAGE_MAX_LEN, +}; #[cfg(feature = "iterator")] use cosmwasm_std::Order; @@ -22,6 +25,7 @@ use crate::serde::to_vec; use crate::GasInfo; const GAS_COST_VERIFY_SECP256K1_SIGNATURE: u64 = 100; +const GAS_COST_VERIFY_ED25519_SIGNATURE: u64 = 100; /// A kibi (kilo binary) const KI: usize = 1024; @@ -92,6 +96,15 @@ pub fn native_secp256k1_verify( do_secp256k1_verify(env, hash_ptr, signature_ptr, pubkey_ptr) } +pub fn native_ed25519_verify( + env: &Environment, + message_ptr: u32, + signature_ptr: u32, + pubkey_ptr: u32, +) -> VmResult { + do_ed25519_verify(env, message_ptr, signature_ptr, pubkey_ptr) +} + pub fn native_query_chain( env: &Environment, request_ptr: u32, @@ -255,11 +268,11 @@ fn do_secp256k1_verify( signature_ptr: u32, pubkey_ptr: u32, ) -> VmResult { - let hash = read_region(&env.memory(), hash_ptr, MESSAGE_HASH_MAX_LENGTH)?; + let hash = read_region(&env.memory(), hash_ptr, MESSAGE_HASH_MAX_LEN)?; - let signature = read_region(&env.memory(), signature_ptr, SIGNATURE_MAX_LENGTH)?; + let signature = read_region(&env.memory(), signature_ptr, ECDSA_SIGNATURE_LEN)?; - let pubkey = read_region(&env.memory(), pubkey_ptr, PUBKEY_MAX_LENGTH)?; + 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); @@ -268,6 +281,34 @@ fn do_secp256k1_verify( match result { Ok(true) => Ok(0), Ok(false) => Ok(1), + Err(CryptoError::MessageError { error_code, .. }) => Ok(error_code), + Err(CryptoError::HashErr { error_code, .. }) => Ok(error_code), + Err(CryptoError::SignatureErr { error_code, .. }) => Ok(error_code), + Err(CryptoError::PublicKeyErr { error_code, .. }) => Ok(error_code), + Err(CryptoError::GenericErr { error_code, .. }) => Ok(error_code), + } +} + +fn do_ed25519_verify( + env: &Environment, + message_ptr: u32, + signature_ptr: u32, + 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?)).into()) + match result { + Ok(true) => Ok(0), + Ok(false) => Ok(1), + Err(CryptoError::MessageError { error_code, .. }) => Ok(error_code), Err(CryptoError::HashErr { error_code, .. }) => Ok(error_code), Err(CryptoError::SignatureErr { error_code, .. }) => Ok(error_code), Err(CryptoError::PublicKeyErr { error_code, .. }) => Ok(error_code), @@ -409,6 +450,11 @@ mod tests { const ECDSA_SIG_HEX: &str = "207082eb2c3dfa0b454e0906051270ba4074ac93760ba9e7110cd9471475111151eb0dbbc9920e72146fb564f99d039802bf6ef2561446eb126ef364d21ee9c4"; const ECDSA_PUBKEY_HEX: &str = "04051c1ee2190ecfb174bfe4f90763f2b4ff7517b70a2aec1876ebcfd644c4633fb03f3cfbd94b1f376e34592d9d41ccaf640bb751b00a1fadeb0c01157769eb73"; + const EDDSA_MSG_HEX: &str = ""; + const EDDSA_SIG_HEX: &str = "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"; + const EDDSA_PUBKEY_HEX: &str = + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"; + fn make_instance(api: MA) -> (Environment, Box) { let gas_limit = TESTING_GAS_LIMIT; let env = Environment::new(api, gas_limit, false); @@ -1024,7 +1070,7 @@ mod tests { VmError::CommunicationErr { source: CommunicationError::RegionLengthTooBig { length, .. }, .. - } => assert_eq!(length, MESSAGE_HASH_MAX_LENGTH + 1), + } => assert_eq!(length, MESSAGE_HASH_MAX_LEN + 1), e => panic!("Unexpected error: {:?}", e), } } @@ -1088,7 +1134,7 @@ mod tests { VmError::CommunicationErr { source: CommunicationError::RegionLengthTooBig { length, .. }, .. - } => assert_eq!(length, SIGNATURE_MAX_LENGTH + 1), + } => assert_eq!(length, ECDSA_SIGNATURE_LEN + 1), e => panic!("Unexpected error: {:?}", e), } } @@ -1172,7 +1218,7 @@ mod tests { VmError::CommunicationErr { source: CommunicationError::RegionLengthTooBig { length, .. }, .. - } => assert_eq!(length, PUBKEY_MAX_LENGTH + 1), + } => assert_eq!(length, ECDSA_PUBKEY_MAX_LEN + 1), e => panic!("Unexpected error: {:?}", e), } } @@ -1190,6 +1236,7 @@ mod tests { // reduce / break pubkey pubkey.pop(); let pubkey_ptr = write_data(&env, &pubkey); + assert_eq!( do_secp256k1_verify::(&env, hash_ptr, sig_ptr, pubkey_ptr).unwrap(), 5 // mapped PublicKeyErr @@ -1219,11 +1266,11 @@ mod tests { let api = MockApi::default(); let (env, mut _instance) = make_instance(api.clone()); - let hash = vec![0x22; MESSAGE_HASH_MAX_LENGTH]; + let hash = vec![0x22; MESSAGE_HASH_MAX_LEN]; let hash_ptr = write_data(&env, &hash); - let sig = vec![0x22; SIGNATURE_MAX_LENGTH]; + let sig = vec![0x22; ECDSA_SIGNATURE_LEN]; let sig_ptr = write_data(&env, &sig); - let pubkey = vec![0x04; PUBKEY_MAX_LENGTH]; + let pubkey = vec![0x04; ECDSA_PUBKEY_MAX_LEN]; let pubkey_ptr = write_data(&env, &pubkey); assert_eq!( @@ -1232,6 +1279,232 @@ mod tests { ) } + #[test] + fn do_ed25519_verify_works() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + let sig_ptr = write_data(&env, &sig); + let pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 0 + ); + } + + #[test] + fn do_ed25519_verify_wrong_msg_verify_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let mut msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + // alter msg + msg.push(0x01); + let msg_ptr = write_data(&env, &msg); + let sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + let sig_ptr = write_data(&env, &sig); + let pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 1 + ); + } + + #[test] + fn do_ed25519_verify_larger_msg_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let mut msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + // extend / break msg + msg.extend_from_slice(&[0x00; MESSAGE_MAX_LEN + 1]); + let msg_ptr = write_data(&env, &msg); + let sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + let sig_ptr = write_data(&env, &sig); + let pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + let pubkey_ptr = write_data(&env, &pubkey); + + let result = do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr); + match result.unwrap_err() { + VmError::CommunicationErr { + source: CommunicationError::RegionLengthTooBig { length, .. }, + .. + } => assert_eq!(length, msg.len()), + e => panic!("Unexpected error: {:?}", e), + } + } + + #[test] + fn do_ed25519_verify_wrong_sig_verify_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let mut sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + // alter sig + sig[0] ^= 0x01; + let sig_ptr = write_data(&env, &sig); + let pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 1 + ); + } + + #[test] + fn do_ed25519_verify_larger_sig_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let mut sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + // extend / break sig + sig.push(0x00); + let sig_ptr = write_data(&env, &sig); + let pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + let pubkey_ptr = write_data(&env, &pubkey); + + let result = do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr); + match result.unwrap_err() { + VmError::CommunicationErr { + source: CommunicationError::RegionLengthTooBig { length, .. }, + .. + } => assert_eq!(length, EDDSA_SIGNATURE_LEN + 1), + e => panic!("Unexpected error: {:?}", e), + } + } + + #[test] + fn do_ed25519_verify_shorter_sig_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let mut sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + // reduce / break sig + sig.pop(); + let sig_ptr = write_data(&env, &sig); + let pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 4 // mapped SignatureErr + ) + } + + #[test] + fn do_ed25519_verify_wrong_pubkey_verify_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + let sig_ptr = write_data(&env, &sig); + let mut pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + // alter pubkey + pubkey[1] ^= 0x01; + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 1 + ); + } + + #[test] + fn do_ed25519_verify_larger_pubkey_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + let sig_ptr = write_data(&env, &sig); + let mut pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + // extend / break pubkey + pubkey.push(0x00); + let pubkey_ptr = write_data(&env, &pubkey); + + let result = do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr); + match result.unwrap_err() { + VmError::CommunicationErr { + source: CommunicationError::RegionLengthTooBig { length, .. }, + .. + } => assert_eq!(length, EDDSA_PUBKEY_LEN + 1), + e => panic!("Unexpected error: {:?}", e), + } + } + + #[test] + fn do_ed25519_verify_shorter_pubkey_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + let sig_ptr = write_data(&env, &sig); + let mut pubkey = hex::decode(EDDSA_PUBKEY_HEX).unwrap(); + // reduce / break pubkey + pubkey.pop(); + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 5 // mapped PublicKeyErr + ) + } + + #[test] + fn do_ed25519_verify_empty_pubkey_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = hex::decode(EDDSA_MSG_HEX).unwrap(); + let msg_ptr = write_data(&env, &msg); + let sig = hex::decode(EDDSA_SIG_HEX).unwrap(); + let sig_ptr = write_data(&env, &sig); + let pubkey = vec![]; + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 5 // mapped PublicKeyError + ) + } + + #[test] + fn do_ed25519_verify_wrong_data_fails() { + let api = MockApi::default(); + let (env, mut _instance) = make_instance(api.clone()); + + let msg = vec![0x22; MESSAGE_HASH_MAX_LEN]; + let msg_ptr = write_data(&env, &msg); + let sig = vec![0x22; EDDSA_SIGNATURE_LEN]; + let sig_ptr = write_data(&env, &sig); + let pubkey = vec![0x04; EDDSA_PUBKEY_LEN]; + let pubkey_ptr = write_data(&env, &pubkey); + + assert_eq!( + do_ed25519_verify::(&env, msg_ptr, sig_ptr, pubkey_ptr).unwrap(), + 10 // mapped GenericErr + ) + } + #[test] fn do_query_chain_works() { let api = MockApi::default(); diff --git a/packages/vm/src/instance.rs b/packages/vm/src/instance.rs index 8027d6d13e..defa5291b3 100644 --- a/packages/vm/src/instance.rs +++ b/packages/vm/src/instance.rs @@ -10,7 +10,7 @@ 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_humanize_address, native_query_chain, native_secp256k1_verify, + native_ed25519_verify, native_humanize_address, native_query_chain, native_secp256k1_verify, }; #[cfg(feature = "iterator")] use crate::imports::{native_db_next, native_db_scan}; @@ -130,6 +130,14 @@ where Function::new_native_with_env(store, env.clone(), native_secp256k1_verify), ); + // 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. + env_imports.insert( + "ed25519_verify", + Function::new_native_with_env(store, env.clone(), native_ed25519_verify), + ); + // Allows the contract to emit debug logs that the host can either process or ignore. // This is never written to chain. // Takes a pointer argument of a memory region that must contain an UTF-8 encoded string.