From fd901f514018ef268875b6b052222b3da4ffd569 Mon Sep 17 00:00:00 2001 From: bluenote Date: Tue, 31 Jan 2023 10:31:24 -0800 Subject: [PATCH 01/16] add verifier --- Cargo.lock | 27 +++++++++++++++++++ .../cw-verifier-middleware/Cargo.toml | 11 ++++++++ .../external/cw-verifier-middleware/README.md | 0 .../cw-verifier-middleware/src/error.rs | 11 ++++++++ .../cw-verifier-middleware/src/lib.rs | 5 ++++ .../cw-verifier-middleware/src/msg.rs | 5 ++++ .../cw-verifier-middleware/src/verify.rs | 23 ++++++++++++++++ 7 files changed, 82 insertions(+) create mode 100644 contracts/external/cw-verifier-middleware/Cargo.toml create mode 100644 contracts/external/cw-verifier-middleware/README.md create mode 100644 contracts/external/cw-verifier-middleware/src/error.rs create mode 100644 contracts/external/cw-verifier-middleware/src/lib.rs create mode 100644 contracts/external/cw-verifier-middleware/src/msg.rs create mode 100644 contracts/external/cw-verifier-middleware/src/verify.rs diff --git a/Cargo.lock b/Cargo.lock index 15a36b447..894218d06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -852,6 +852,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-verifier-middleware" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "secp256k1", + "thiserror", +] + [[package]] name = "cw-vesting" version = "2.0.3" @@ -2989,6 +2998,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4124a35fe33ae14259c490fd70fa199a32b9ce9502f2ee6bc4f81ec06fa65894" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642a62736682fdd8c71da0eb273e453c8ac74e33b9fb310e22ba5b03ec7651ff" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.8.2" diff --git a/contracts/external/cw-verifier-middleware/Cargo.toml b/contracts/external/cw-verifier-middleware/Cargo.toml new file mode 100644 index 000000000..f4a911513 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cw-verifier-middleware" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = { workspace = true} +thiserror = { workspace = true } +secp256k1 = "0.26.0" \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/README.md b/contracts/external/cw-verifier-middleware/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs new file mode 100644 index 000000000..ac4d54e14 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -0,0 +1,11 @@ + +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), +} + + diff --git a/contracts/external/cw-verifier-middleware/src/lib.rs b/contracts/external/cw-verifier-middleware/src/lib.rs new file mode 100644 index 000000000..89542c0a1 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/lib.rs @@ -0,0 +1,5 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod verify; +pub mod error; +pub mod msg; diff --git a/contracts/external/cw-verifier-middleware/src/msg.rs b/contracts/external/cw-verifier-middleware/src/msg.rs new file mode 100644 index 000000000..8a182ee93 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/msg.rs @@ -0,0 +1,5 @@ + + +pub struct ExecuteMsg { + +} \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs new file mode 100644 index 000000000..3302d67b4 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::{Response, Binary, Timestamp +}; +use crate::{error::ContractError, msg::ExecuteMsg}; + + +pub fn execute_wrap(msg: Message) -> Result{ + // 1. verify signature + secp256k1::verify(&msg.signature, &msg.payload, &msg.public_key)?; + + return Ok(Response::default()) +} + +pub struct Message { + pub payload: Payload, + pub signature: Binary, + pub public_key: secp256k1::PublicKey, +} + +pub struct Payload { + pub nonce: u64, + pub msg: ExecuteMsg, + pub expiration: Option, +} \ No newline at end of file From 23922f49b22eae00e879484827a0f60e1ff79899 Mon Sep 17 00:00:00 2001 From: bluenote Date: Tue, 31 Jan 2023 13:57:48 -0800 Subject: [PATCH 02/16] comment out --- contracts/external/cw-verifier-middleware/src/verify.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 3302d67b4..314d6b785 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -5,7 +5,7 @@ use crate::{error::ContractError, msg::ExecuteMsg}; pub fn execute_wrap(msg: Message) -> Result{ // 1. verify signature - secp256k1::verify(&msg.signature, &msg.payload, &msg.public_key)?; + // secp256k1::verify(&msg.signature, &msg.payload, &msg.public_key)?; return Ok(Response::default()) } From 5b6250c4336e1f0c55747014795eab8c04e016ab Mon Sep 17 00:00:00 2001 From: bluenote Date: Sun, 5 Feb 2023 22:50:52 -0800 Subject: [PATCH 03/16] flesh out logic --- Cargo.toml | 2 + .../cw-verifier-middleware/Cargo.toml | 9 +- .../cw-verifier-middleware/src/error.rs | 15 ++- .../cw-verifier-middleware/src/lib.rs | 2 + .../cw-verifier-middleware/src/msg.rs | 3 - .../cw-verifier-middleware/src/state.rs | 4 + .../cw-verifier-middleware/src/verify.rs | 97 +++++++++++++++++-- 7 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 contracts/external/cw-verifier-middleware/src/state.rs diff --git a/Cargo.toml b/Cargo.toml index c0581023b..7826214ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,8 @@ serde = { version = "1.0", default-features = false, features = ["derive"]} syn = { version = "1.0", features = ["derive"] } thiserror = { version = "1.0.30" } wynd-utils = "0.4.1" +secp256k1 = "0.26.0" +sha2 = "0.10.6" # One commit ahead of version 0.3.0. Allows initialization with an # optional owner. diff --git a/contracts/external/cw-verifier-middleware/Cargo.toml b/contracts/external/cw-verifier-middleware/Cargo.toml index f4a911513..949515ba3 100644 --- a/contracts/external/cw-verifier-middleware/Cargo.toml +++ b/contracts/external/cw-verifier-middleware/Cargo.toml @@ -8,4 +8,11 @@ edition = "2021" [dependencies] cosmwasm-std = { workspace = true} thiserror = { workspace = true } -secp256k1 = "0.26.0" \ No newline at end of file +secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } +sha2 = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } + +[dev-dependencies] + +secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index ac4d54e14..fe8bfc98e 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -1,11 +1,24 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{StdError, OverflowError}; use thiserror::Error; +use secp256k1::Error as SecpError; #[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("{0}")] + Secp256k1Error(#[from] SecpError), + + #[error("Invalid nonce")] + InvalidNonce, + + #[error("Message expiration has passed")] + MessageExpired, } diff --git a/contracts/external/cw-verifier-middleware/src/lib.rs b/contracts/external/cw-verifier-middleware/src/lib.rs index 89542c0a1..235a65adc 100644 --- a/contracts/external/cw-verifier-middleware/src/lib.rs +++ b/contracts/external/cw-verifier-middleware/src/lib.rs @@ -3,3 +3,5 @@ pub mod verify; pub mod error; pub mod msg; +pub mod state; + diff --git a/contracts/external/cw-verifier-middleware/src/msg.rs b/contracts/external/cw-verifier-middleware/src/msg.rs index 8a182ee93..139597f9c 100644 --- a/contracts/external/cw-verifier-middleware/src/msg.rs +++ b/contracts/external/cw-verifier-middleware/src/msg.rs @@ -1,5 +1,2 @@ -pub struct ExecuteMsg { - -} \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/state.rs b/contracts/external/cw-verifier-middleware/src/state.rs new file mode 100644 index 000000000..230d0001b --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/state.rs @@ -0,0 +1,4 @@ +use cosmwasm_std::Uint128; +use cw_storage_plus::{Item}; + +pub const NONCE: Item = Item::new("nonce"); \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 314d6b785..34407c1c1 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -1,23 +1,100 @@ -use cosmwasm_std::{Response, Binary, Timestamp +use cosmwasm_std::{Binary, Timestamp, to_binary, DepsMut, Env, Addr, MessageInfo, Uint128, StdError, OverflowError }; -use crate::{error::ContractError, msg::ExecuteMsg}; +use sha2::{Sha256, Digest}; +use crate::{error::ContractError, state::NONCE}; +use secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, ecdsa::Signature,}; +use cosmwasm_schema::{cw_serde}; +pub fn verify(deps: DepsMut, env: Env, mut info: MessageInfo, wrapped_msg: WrappedMessage) -> Result{ + let secp = Secp256k1::verification_only(); -pub fn execute_wrap(msg: Message) -> Result{ - // 1. verify signature - // secp256k1::verify(&msg.signature, &msg.payload, &msg.public_key)?; + // Serialize the inner message + let msg_ser = to_binary(&wrapped_msg.payload)?; - return Ok(Response::default()) + // Hash the serialized payload using SHA-256 + let msg_hash = Sha256::digest(&msg_ser); + + // Verify the signature + let msg_secp = SecpMessage::from_slice(&msg_hash)?; + let public_key = PublicKey::from_slice(&wrapped_msg.public_key).unwrap(); + let signature = Signature::from_der(&wrapped_msg.signature).unwrap(); + secp.verify_ecdsa(&msg_secp, &signature, &public_key)?; + + // Validate that the message has the correct nonce + let nonce = NONCE.load(deps.storage)?; + if wrapped_msg.payload.nonce != nonce.u128() { + return Err(ContractError::InvalidNonce { }); + } + + // Increment nonce + NONCE.update(deps.storage, |nonce| nonce.checked_add(Uint128::from(1u128)).map_err(|e| StdError::from(e)))?; + + // Validate that the message has not expired + if let Some(expiration) = wrapped_msg.payload.expiration { + if expiration < env.block.time { + return Err(ContractError::MessageExpired { }); + } + } + + // Set the message sender to the address corresponding to the provided public key. (pk_to_addr) + let sender = pk_to_addr(wrapped_msg.public_key); + info.sender = sender; + + // Return the msg; caller will deserialize + return Ok(wrapped_msg.payload.msg) } -pub struct Message { +#[cw_serde] +pub struct WrappedMessage { pub payload: Payload, pub signature: Binary, - pub public_key: secp256k1::PublicKey, + pub public_key: Binary, } +#[cw_serde] pub struct Payload { - pub nonce: u64, - pub msg: ExecuteMsg, + pub nonce: u128, + pub msg: Binary, pub expiration: Option, +} + +// mock pk_to_addr +pub fn pk_to_addr(_pk: Binary) -> Addr { + return Addr::unchecked("dummy_addr") +} + +mod tests { + use cosmwasm_std::{Binary, to_binary,testing::{mock_dependencies, mock_env, mock_info},}; + use secp256k1::{SecretKey, ffi::PublicKey, rand::{self, rngs::OsRng}, Secp256k1, Message}; + use sha2::{Sha256, Digest}; + + use crate::verify::verify; + + use super::{Payload, WrappedMessage}; + + #[test] + fn test_verify_signature() { + let payload = Payload { + nonce: 0, + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjoiZXlKaFpHMXBiaUk2Ym5Wc2JDd2lZWFYwYjIxaGRHbGpZV3hzZVY5aFpHUmZZM2N5TUhNaU9uUnlkV1VzSW1GMWRHOXRZWFJwWTJGc2JIbGZZV1JrWDJOM056SXhjeUk2ZEhKMVpTd2laR1Z6WTNKcGNIUnBiMjRpT2lJeElpd2lhVzFoWjJWZmRYSnNJanB1ZFd4c0xDSnVZVzFsSWpvaWRHVnpkQ0lzSW5CeWIzQnZjMkZzWDIxdlpIVnNaWE5mYVc1emRHRnVkR2xoZEdWZmFXNW1ieUk2VzNzaVlXUnRhVzRpT25zaVkyOXlaVjl0YjJSMWJHVWlPbnQ5ZlN3aVkyOWtaVjlwWkNJNk1UWTVOQ3dpYkdGaVpXd2lPaUpFUVU5ZmRHVnpkRjlFWVc5UWNtOXdiM05oYkZOcGJtZHNaU0lzSW0xelp5STZJbVY1U21oaVIzaDJaREU1ZVZwWVduWmtSMngxV25sSk5scHRSbk5qTWxWelNXMU9jMkl6VG14WU0wSjVZak5DZG1NeVJuTllNamwxV0RKV05GcFhUakZrUjJ4MlltdzViVmxYYkhOa1dFcHNTV3B3TUdOdVZteE1RMHAwV1Zob1ptUnRPVEJoVnpWdVdETkNiR050YkhaYVEwazJaWGxLTUdGWE1XeEphbTh5VFVSUk5FMUVRamxNUTBwMFlWYzFabVJ0T1RCaFZ6VnVXRE5DYkdOdGJIWmFRMGsyWW01V2MySkRkMmxpTWpWelpWWTVkRnBYTVdsYVdFcDZXREpXTkZwWFRqRmtSMVZwVDI1U2VXUlhWWE5KYmtKNVdsWTVkMk50T1hkaU0wNXNXREpzZFZwdE9HbFBibk5wWWxjNWEyUlhlR3hZTWpGb1pWWTVkMk50T1hkaU0wNXNTV3B3TjBsdGJIVmFiVGhwVDI1emFWbFhVblJoVnpScFQyNXphVmt5T1hsYVZqbDBZakpTTVdKSFZXbFBiblE1WmxOM2FWa3lPV3RhVmpsd1drTkpOazFVV1RWTmFYZHBZa2RHYVZwWGQybFBhVXBGVVZVNVptUkhWbnBrUmpsM1kyMVZkR05JU25aalJ6bDZXbE14UlZsWE9WRmpiVGwzWWpOT2FHSkdUbkJpYldSeldsTkpjMGx0TVhwYWVVazJTVzFXTlZOdGRHRlhSVW95V1hwS2MwMUdaM2xpU0ZaaFlsUm9jRlF5TURGTlYwcElaRE5PU21KV1dUQmFSV1JYWkZkTmVXSklXbWxoVldzeVdsUk5kMk13YkhSUFdHUmhWbnBXYlZrd2FFdGtiVTVJVDFod1dsWXphRzFaZWs1WFlWZEtXR0pJY0dwTmJYZ3lXVzFzU2s1c2NIUlNiazVxVFd4Wk5VbHVNVGxtVTNkcFpFZG9lVnBZVG05aU1uaHJTV3B3TjBsdVVtOWpiVlo2WVVjNWMxcEdPWGhrVnpsNVpGY3dhVTl1YzJsaldGWjJZMjVXZEVscWNEZEpia0pzWTIxT2JHSnVVV2xQYVVsM1RHcEpkMGx1TUhOSmJsSnZZMjFXZW1GSE9YTmFRMGsyWlhsS2RGbFhjSFpqYld3d1pWTkpObVV6TVRsbVdERTVJbjFkTENKMmIzUnBibWRmYlc5a2RXeGxYMmx1YzNSaGJuUnBZWFJsWDJsdVptOGlPbnNpWVdSdGFXNGlPbnNpWTI5eVpWOXRiMlIxYkdVaU9udDlmU3dpWTI5a1pWOXBaQ0k2TVRZNU5pd2liR0ZpWld3aU9pSkVRVTlmZEdWemRGOUVZVzlXYjNScGJtZERkelFpTENKdGMyY2lPaUpsZVVwcVpIcFNabG96U25aa1dFSm1XVEk1YTFwV09YQmFRMGsyVFZSWk1rOURkMmxoVnpWd1pFZHNhR0pHT1hSYVZ6RnBXbGhLZWtscWNHSmxlVXBvV2tkU2VVbHFiMmxoYmxaMVlucEdNbU5ZYURKbFdHTXlZVE5DTlU0emFIRk5SekY2WlVodk1VNHpUakprTWpRd1kwUkNjbHB0VWpGT1JGcHlaR3BDZDJGNVNYTkpibVJzWVZka2IyUkRTVFpOV0RGa1psRTlQU0o5ZlE9PSIsImxhYmVsIjoidGVzdCJ9fQ==").unwrap(), + expiration: None, + }; + + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + let wrapped_msg = WrappedMessage { + payload, + signature: sig.serialize_compact().into(), + public_key: public_key.serialize().into(), + }; + + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + assert!(verify(deps.as_mut(), env, info, wrapped_msg).is_ok()); + } } \ No newline at end of file From ceabcabb72958058d4a8b59de1966cebcee84b11 Mon Sep 17 00:00:00 2001 From: bluenote Date: Mon, 6 Feb 2023 11:19:15 -0800 Subject: [PATCH 04/16] incorporate benskey's changes --- Cargo.toml | 4 + .../cw-verifier-middleware/Cargo.toml | 5 + .../external/cw-verifier-middleware/README.md | 3 + .../cw-verifier-middleware/src/error.rs | 10 +- .../cw-verifier-middleware/src/lib.rs | 3 + .../cw-verifier-middleware/src/msg.rs | 16 +++ .../cw-verifier-middleware/src/state.rs | 4 +- .../cw-verifier-middleware/src/testing/mod.rs | 1 + .../src/testing/tests.rs | 44 ++++++ .../cw-verifier-middleware/src/verify.rs | 131 +++++++++--------- 10 files changed, 152 insertions(+), 69 deletions(-) create mode 100644 contracts/external/cw-verifier-middleware/src/testing/mod.rs create mode 100644 contracts/external/cw-verifier-middleware/src/testing/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 7826214ce..b873e7bb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,10 @@ thiserror = { version = "1.0.30" } wynd-utils = "0.4.1" secp256k1 = "0.26.0" sha2 = "0.10.6" +bip32 = "0.4.0" +hex = "0.4.3" +ripemd = "0.1.3" +bech32 = "0.9.1" # One commit ahead of version 0.3.0. Allows initialization with an # optional owner. diff --git a/contracts/external/cw-verifier-middleware/Cargo.toml b/contracts/external/cw-verifier-middleware/Cargo.toml index 949515ba3..ee1fc46ea 100644 --- a/contracts/external/cw-verifier-middleware/Cargo.toml +++ b/contracts/external/cw-verifier-middleware/Cargo.toml @@ -12,6 +12,11 @@ secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } sha2 = { workspace = true } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } +cw-utils = {workspace = true} +hex = { workspace = true } +bip32 = { workspace = true } +ripemd = { workspace = true } +bech32 = { workspace = true } [dev-dependencies] diff --git a/contracts/external/cw-verifier-middleware/README.md b/contracts/external/cw-verifier-middleware/README.md index e69de29bb..8bef1d4d5 100644 --- a/contracts/external/cw-verifier-middleware/README.md +++ b/contracts/external/cw-verifier-middleware/README.md @@ -0,0 +1,3 @@ +TODO + +![](https://user-images.githubusercontent.com/30676292/214428970-aabed2eb-7271-4a91-a641-23d004f04512.png) diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index fe8bfc98e..2a297dc45 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -1,5 +1,5 @@ -use cosmwasm_std::{StdError, OverflowError}; +use cosmwasm_std::{StdError, OverflowError, VerificationError}; use thiserror::Error; use secp256k1::Error as SecpError; @@ -9,16 +9,16 @@ pub enum ContractError { Std(#[from] StdError), #[error("{0}")] - OverflowError(#[from] OverflowError), - - #[error("{0}")] - Secp256k1Error(#[from] SecpError), + VerificationError(#[from] VerificationError), #[error("Invalid nonce")] InvalidNonce, #[error("Message expiration has passed")] MessageExpired, + + #[error("Message signature is invalid")] + SignatureInvalid, } diff --git a/contracts/external/cw-verifier-middleware/src/lib.rs b/contracts/external/cw-verifier-middleware/src/lib.rs index 235a65adc..dbed528fc 100644 --- a/contracts/external/cw-verifier-middleware/src/lib.rs +++ b/contracts/external/cw-verifier-middleware/src/lib.rs @@ -5,3 +5,6 @@ pub mod error; pub mod msg; pub mod state; + +#[cfg(test)] +mod testing; \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/msg.rs b/contracts/external/cw-verifier-middleware/src/msg.rs index 139597f9c..a99b402f6 100644 --- a/contracts/external/cw-verifier-middleware/src/msg.rs +++ b/contracts/external/cw-verifier-middleware/src/msg.rs @@ -1,2 +1,18 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Binary, Uint128}; +use cw_utils::Expiration; +#[cw_serde] +pub struct WrappedMessage { + pub payload: Payload, + // Assumes 'payload' has been hashed, signed, and base64 encoded + pub signature: Binary, + pub public_key: Binary, +} +#[cw_serde] +pub struct Payload { + pub nonce: Uint128, + pub msg: Binary, + pub expiration: Option, +} diff --git a/contracts/external/cw-verifier-middleware/src/state.rs b/contracts/external/cw-verifier-middleware/src/state.rs index 230d0001b..44568cd3e 100644 --- a/contracts/external/cw-verifier-middleware/src/state.rs +++ b/contracts/external/cw-verifier-middleware/src/state.rs @@ -1,4 +1,4 @@ use cosmwasm_std::Uint128; -use cw_storage_plus::{Item}; +use cw_storage_plus::{Item, Map}; -pub const NONCE: Item = Item::new("nonce"); \ No newline at end of file +pub const NONCES: Map<&str, Uint128> = Map::new("pk_to_nonce"); \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/testing/mod.rs b/contracts/external/cw-verifier-middleware/src/testing/mod.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/testing/mod.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs new file mode 100644 index 000000000..2fcf513c7 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{to_binary, Uint128, testing::{mock_dependencies, mock_env, mock_info}}; +use secp256k1::{Secp256k1, Message, rand::rngs::OsRng}; +use sha2::{Sha256, Digest}; + +use crate::{verify::{ec_pk_to_bech32_address, ADDR_PREFIX, verify}, msg::{Payload, WrappedMessage}}; + +#[test] +fn test_generate_juno_addr_from_pk() { + + let juno_address = "juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw".to_string(); + let juno_pk = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79".to_string(); + + let generated_address = ec_pk_to_bech32_address( + juno_pk, + ADDR_PREFIX, + ).unwrap(); + assert_eq!(generated_address, juno_address); +} + +#[test] +fn test_verify() { + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjoiZXlKaFpHMXBiaUk2Ym5Wc2JDd2lZWFYwYjIxaGRHbGpZV3hzZVY5aFpHUmZZM2N5TUhNaU9uUnlkV1VzSW1GMWRHOXRZWFJwWTJGc2JIbGZZV1JrWDJOM056SXhjeUk2ZEhKMVpTd2laR1Z6WTNKcGNIUnBiMjRpT2lJeElpd2lhVzFoWjJWZmRYSnNJanB1ZFd4c0xDSnVZVzFsSWpvaWRHVnpkQ0lzSW5CeWIzQnZjMkZzWDIxdlpIVnNaWE5mYVc1emRHRnVkR2xoZEdWZmFXNW1ieUk2VzNzaVlXUnRhVzRpT25zaVkyOXlaVjl0YjJSMWJHVWlPbnQ5ZlN3aVkyOWtaVjlwWkNJNk1UWTVOQ3dpYkdGaVpXd2lPaUpFUVU5ZmRHVnpkRjlFWVc5UWNtOXdiM05oYkZOcGJtZHNaU0lzSW0xelp5STZJbVY1U21oaVIzaDJaREU1ZVZwWVduWmtSMngxV25sSk5scHRSbk5qTWxWelNXMU9jMkl6VG14WU0wSjVZak5DZG1NeVJuTllNamwxV0RKV05GcFhUakZrUjJ4MlltdzViVmxYYkhOa1dFcHNTV3B3TUdOdVZteE1RMHAwV1Zob1ptUnRPVEJoVnpWdVdETkNiR050YkhaYVEwazJaWGxLTUdGWE1XeEphbTh5VFVSUk5FMUVRamxNUTBwMFlWYzFabVJ0T1RCaFZ6VnVXRE5DYkdOdGJIWmFRMGsyWW01V2MySkRkMmxpTWpWelpWWTVkRnBYTVdsYVdFcDZXREpXTkZwWFRqRmtSMVZwVDI1U2VXUlhWWE5KYmtKNVdsWTVkMk50T1hkaU0wNXNXREpzZFZwdE9HbFBibk5wWWxjNWEyUlhlR3hZTWpGb1pWWTVkMk50T1hkaU0wNXNTV3B3TjBsdGJIVmFiVGhwVDI1emFWbFhVblJoVnpScFQyNXphVmt5T1hsYVZqbDBZakpTTVdKSFZXbFBiblE1WmxOM2FWa3lPV3RhVmpsd1drTkpOazFVV1RWTmFYZHBZa2RHYVZwWGQybFBhVXBGVVZVNVptUkhWbnBrUmpsM1kyMVZkR05JU25aalJ6bDZXbE14UlZsWE9WRmpiVGwzWWpOT2FHSkdUbkJpYldSeldsTkpjMGx0TVhwYWVVazJTVzFXTlZOdGRHRlhSVW95V1hwS2MwMUdaM2xpU0ZaaFlsUm9jRlF5TURGTlYwcElaRE5PU21KV1dUQmFSV1JYWkZkTmVXSklXbWxoVldzeVdsUk5kMk13YkhSUFdHUmhWbnBXYlZrd2FFdGtiVTVJVDFod1dsWXphRzFaZWs1WFlWZEtXR0pJY0dwTmJYZ3lXVzFzU2s1c2NIUlNiazVxVFd4Wk5VbHVNVGxtVTNkcFpFZG9lVnBZVG05aU1uaHJTV3B3TjBsdVVtOWpiVlo2WVVjNWMxcEdPWGhrVnpsNVpGY3dhVTl1YzJsaldGWjJZMjVXZEVscWNEZEpia0pzWTIxT2JHSnVVV2xQYVVsM1RHcEpkMGx1TUhOSmJsSnZZMjFXZW1GSE9YTmFRMGsyWlhsS2RGbFhjSFpqYld3d1pWTkpObVV6TVRsbVdERTVJbjFkTENKMmIzUnBibWRmYlc5a2RXeGxYMmx1YzNSaGJuUnBZWFJsWDJsdVptOGlPbnNpWVdSdGFXNGlPbnNpWTI5eVpWOXRiMlIxYkdVaU9udDlmU3dpWTI5a1pWOXBaQ0k2TVRZNU5pd2liR0ZpWld3aU9pSkVRVTlmZEdWemRGOUVZVzlXYjNScGJtZERkelFpTENKdGMyY2lPaUpsZVVwcVpIcFNabG96U25aa1dFSm1XVEk1YTFwV09YQmFRMGsyVFZSWk1rOURkMmxoVnpWd1pFZHNhR0pHT1hSYVZ6RnBXbGhLZWtscWNHSmxlVXBvV2tkU2VVbHFiMmxoYmxaMVlucEdNbU5ZYURKbFdHTXlZVE5DTlU0emFIRk5SekY2WlVodk1VNHpUakprTWpRd1kwUkNjbHB0VWpGT1JGcHlaR3BDZDJGNVNYTkpibVJzWVZka2IyUkRTVFpOV0RGa1psRTlQU0o5ZlE9PSIsImxhYmVsIjoidGVzdCJ9fQ==").unwrap(), + expiration: None, + }; + + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + let wrapped_msg = WrappedMessage { + payload, + signature: sig.serialize_compact().into(), + public_key: public_key.serialize().into(), + }; + + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + assert!(verify(deps.as_mut(), env, info, wrapped_msg).is_ok()); +} \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 34407c1c1..0cc65597c 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -1,10 +1,15 @@ +use bech32::{Variant, ToBase32}; use cosmwasm_std::{Binary, Timestamp, to_binary, DepsMut, Env, Addr, MessageInfo, Uint128, StdError, OverflowError }; +use cw_utils::Expiration; +use ripemd::Ripemd160; use sha2::{Sha256, Digest}; -use crate::{error::ContractError, state::NONCE}; -use secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, ecdsa::Signature,}; +use crate::{error::ContractError, state::NONCES, msg::WrappedMessage}; +use secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, ecdsa::Signature}; use cosmwasm_schema::{cw_serde}; +pub const ADDR_PREFIX: &str = "juno"; + pub fn verify(deps: DepsMut, env: Env, mut info: MessageInfo, wrapped_msg: WrappedMessage) -> Result{ let secp = Secp256k1::verification_only(); @@ -15,86 +20,88 @@ pub fn verify(deps: DepsMut, env: Env, mut info: MessageInfo, wrapped_msg: Wrapp let msg_hash = Sha256::digest(&msg_ser); // Verify the signature - let msg_secp = SecpMessage::from_slice(&msg_hash)?; - let public_key = PublicKey::from_slice(&wrapped_msg.public_key).unwrap(); - let signature = Signature::from_der(&wrapped_msg.signature).unwrap(); - secp.verify_ecdsa(&msg_secp, &signature, &public_key)?; + let sig_valid = deps.api.secp256k1_verify( + msg_hash.as_slice(), + &wrapped_msg.signature, + wrapped_msg.public_key.as_slice(), + )?; + + if !sig_valid { + return Err(ContractError::SignatureInvalid { }) + } // Validate that the message has the correct nonce - let nonce = NONCE.load(deps.storage)?; - if wrapped_msg.payload.nonce != nonce.u128() { + // todo: how to instantiate nonce first time + let pk_base64 = wrapped_msg.public_key.to_base64(); + let nonce = NONCES.load(deps.storage, &pk_base64)?; + if wrapped_msg.payload.nonce != nonce { return Err(ContractError::InvalidNonce { }); } // Increment nonce - NONCE.update(deps.storage, |nonce| nonce.checked_add(Uint128::from(1u128)).map_err(|e| StdError::from(e)))?; + NONCES.update(deps.storage, &pk_base64, |nonce: Option| {nonce.unwrap_or_default().checked_add(Uint128::from(1u128)).map_err(|e| StdError::from(e))})?; // Validate that the message has not expired if let Some(expiration) = wrapped_msg.payload.expiration { - if expiration < env.block.time { + if expiration.is_expired(&env.block) { return Err(ContractError::MessageExpired { }); } } // Set the message sender to the address corresponding to the provided public key. (pk_to_addr) - let sender = pk_to_addr(wrapped_msg.public_key); - info.sender = sender; + info.sender = ec_pk_to_bech32_address(wrapped_msg.public_key.to_base64(), ADDR_PREFIX)?; // Return the msg; caller will deserialize return Ok(wrapped_msg.payload.msg) } -#[cw_serde] -pub struct WrappedMessage { - pub payload: Payload, - pub signature: Binary, - pub public_key: Binary, -} -#[cw_serde] -pub struct Payload { - pub nonce: u128, - pub msg: Binary, - pub expiration: Option, -} +// takes an uncompressed EC public key and a prefix +pub fn ec_pk_to_bech32_address(hex_pk: String, prefix: &str) -> Result { + if hex_pk.clone().len() != 130 { + return Err(ContractError::Std( + StdError::InvalidHex { + msg: "unexpected hex encoded uncompressed public key length".to_string() + } + )); + } -// mock pk_to_addr -pub fn pk_to_addr(_pk: Binary) -> Addr { - return Addr::unchecked("dummy_addr") + // get the raw public key bytes + let decoded_pk = hex::decode(hex_pk); + let raw_pk = match decoded_pk { + Ok(pk) => pk, + Err(e) => return Err(ContractError::Std( + StdError::InvalidHex { msg: e.to_string() }) + ), + }; + + // extract the compressed version of public key + let public_key = secp256k1::PublicKey::from_slice(raw_pk.as_slice()); + let raw_pk = match public_key { + Ok(pk) => pk.serialize().to_vec(), + Err(e) => return Err(ContractError::Std( + StdError::GenericErr { msg: e.to_string() }, + )), + }; + + // sha256 the raw public key + let pk_sha256 = Sha256::digest(raw_pk); + + // take the ripemd160 of the sha256 of the raw pk + let address_raw = Ripemd160::digest(pk_sha256); + + // encode the prefix and the raw address bytes with Bech32 + let bech32 = bech32::encode( + &prefix, + address_raw.to_base32(), + Variant::Bech32, + ); + + match bech32 { + Ok(addr) => Ok(Addr::unchecked(addr)), + Err(e) => Err(ContractError::Std( + StdError::generic_err(e.to_string()) + )), + } } -mod tests { - use cosmwasm_std::{Binary, to_binary,testing::{mock_dependencies, mock_env, mock_info},}; - use secp256k1::{SecretKey, ffi::PublicKey, rand::{self, rngs::OsRng}, Secp256k1, Message}; - use sha2::{Sha256, Digest}; - - use crate::verify::verify; - - use super::{Payload, WrappedMessage}; - - #[test] - fn test_verify_signature() { - let payload = Payload { - nonce: 0, - msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjoiZXlKaFpHMXBiaUk2Ym5Wc2JDd2lZWFYwYjIxaGRHbGpZV3hzZVY5aFpHUmZZM2N5TUhNaU9uUnlkV1VzSW1GMWRHOXRZWFJwWTJGc2JIbGZZV1JrWDJOM056SXhjeUk2ZEhKMVpTd2laR1Z6WTNKcGNIUnBiMjRpT2lJeElpd2lhVzFoWjJWZmRYSnNJanB1ZFd4c0xDSnVZVzFsSWpvaWRHVnpkQ0lzSW5CeWIzQnZjMkZzWDIxdlpIVnNaWE5mYVc1emRHRnVkR2xoZEdWZmFXNW1ieUk2VzNzaVlXUnRhVzRpT25zaVkyOXlaVjl0YjJSMWJHVWlPbnQ5ZlN3aVkyOWtaVjlwWkNJNk1UWTVOQ3dpYkdGaVpXd2lPaUpFUVU5ZmRHVnpkRjlFWVc5UWNtOXdiM05oYkZOcGJtZHNaU0lzSW0xelp5STZJbVY1U21oaVIzaDJaREU1ZVZwWVduWmtSMngxV25sSk5scHRSbk5qTWxWelNXMU9jMkl6VG14WU0wSjVZak5DZG1NeVJuTllNamwxV0RKV05GcFhUakZrUjJ4MlltdzViVmxYYkhOa1dFcHNTV3B3TUdOdVZteE1RMHAwV1Zob1ptUnRPVEJoVnpWdVdETkNiR050YkhaYVEwazJaWGxLTUdGWE1XeEphbTh5VFVSUk5FMUVRamxNUTBwMFlWYzFabVJ0T1RCaFZ6VnVXRE5DYkdOdGJIWmFRMGsyWW01V2MySkRkMmxpTWpWelpWWTVkRnBYTVdsYVdFcDZXREpXTkZwWFRqRmtSMVZwVDI1U2VXUlhWWE5KYmtKNVdsWTVkMk50T1hkaU0wNXNXREpzZFZwdE9HbFBibk5wWWxjNWEyUlhlR3hZTWpGb1pWWTVkMk50T1hkaU0wNXNTV3B3TjBsdGJIVmFiVGhwVDI1emFWbFhVblJoVnpScFQyNXphVmt5T1hsYVZqbDBZakpTTVdKSFZXbFBiblE1WmxOM2FWa3lPV3RhVmpsd1drTkpOazFVV1RWTmFYZHBZa2RHYVZwWGQybFBhVXBGVVZVNVptUkhWbnBrUmpsM1kyMVZkR05JU25aalJ6bDZXbE14UlZsWE9WRmpiVGwzWWpOT2FHSkdUbkJpYldSeldsTkpjMGx0TVhwYWVVazJTVzFXTlZOdGRHRlhSVW95V1hwS2MwMUdaM2xpU0ZaaFlsUm9jRlF5TURGTlYwcElaRE5PU21KV1dUQmFSV1JYWkZkTmVXSklXbWxoVldzeVdsUk5kMk13YkhSUFdHUmhWbnBXYlZrd2FFdGtiVTVJVDFod1dsWXphRzFaZWs1WFlWZEtXR0pJY0dwTmJYZ3lXVzFzU2s1c2NIUlNiazVxVFd4Wk5VbHVNVGxtVTNkcFpFZG9lVnBZVG05aU1uaHJTV3B3TjBsdVVtOWpiVlo2WVVjNWMxcEdPWGhrVnpsNVpGY3dhVTl1YzJsaldGWjJZMjVXZEVscWNEZEpia0pzWTIxT2JHSnVVV2xQYVVsM1RHcEpkMGx1TUhOSmJsSnZZMjFXZW1GSE9YTmFRMGsyWlhsS2RGbFhjSFpqYld3d1pWTkpObVV6TVRsbVdERTVJbjFkTENKMmIzUnBibWRmYlc5a2RXeGxYMmx1YzNSaGJuUnBZWFJsWDJsdVptOGlPbnNpWVdSdGFXNGlPbnNpWTI5eVpWOXRiMlIxYkdVaU9udDlmU3dpWTI5a1pWOXBaQ0k2TVRZNU5pd2liR0ZpWld3aU9pSkVRVTlmZEdWemRGOUVZVzlXYjNScGJtZERkelFpTENKdGMyY2lPaUpsZVVwcVpIcFNabG96U25aa1dFSm1XVEk1YTFwV09YQmFRMGsyVFZSWk1rOURkMmxoVnpWd1pFZHNhR0pHT1hSYVZ6RnBXbGhLZWtscWNHSmxlVXBvV2tkU2VVbHFiMmxoYmxaMVlucEdNbU5ZYURKbFdHTXlZVE5DTlU0emFIRk5SekY2WlVodk1VNHpUakprTWpRd1kwUkNjbHB0VWpGT1JGcHlaR3BDZDJGNVNYTkpibVJzWVZka2IyUkRTVFpOV0RGa1psRTlQU0o5ZlE9PSIsImxhYmVsIjoidGVzdCJ9fQ==").unwrap(), - expiration: None, - }; - - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); - let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); - let msg = Message::from_slice(&msg_hash).unwrap(); - let sig = secp.sign_ecdsa(&msg, &secret_key); - - let wrapped_msg = WrappedMessage { - payload, - signature: sig.serialize_compact().into(), - public_key: public_key.serialize().into(), - }; - - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("creator", &[]); - assert!(verify(deps.as_mut(), env, info, wrapped_msg).is_ok()); - } -} \ No newline at end of file From 3eca59f002f8aae3ba348cf5cac2e94e153f3a3c Mon Sep 17 00:00:00 2001 From: bluenote Date: Wed, 15 Feb 2023 13:52:42 -0800 Subject: [PATCH 05/16] wip --- Cargo.lock | 22 ++++ Cargo.toml | 1 + .../cw-verifier-middleware/src/msg.rs | 5 +- .../cw-verifier-middleware/src/state.rs | 11 +- .../src/testing/tests.rs | 37 +++--- .../cw-verifier-middleware/src/verify.rs | 109 +++++++++++------- 6 files changed, 125 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 894218d06..fdde2c3d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,12 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bip32" version = "0.4.0" @@ -165,6 +171,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bitcoin_hashes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" + [[package]] name = "bitflags" version = "1.3.2" @@ -856,8 +868,16 @@ dependencies = [ name = "cw-verifier-middleware" version = "0.1.0" dependencies = [ + "bech32", + "bip32", + "cosmwasm-schema", "cosmwasm-std", + "cw-storage-plus 1.0.1 (git+https://github.com/DA0-DA0/cw-storage-plus.git)", + "cw-utils 0.16.0", + "hex", + "ripemd", "secp256k1", + "sha2 0.10.6", "thiserror", ] @@ -3004,6 +3024,8 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4124a35fe33ae14259c490fd70fa199a32b9ce9502f2ee6bc4f81ec06fa65894" dependencies = [ + "bitcoin_hashes", + "rand", "secp256k1-sys", ] diff --git a/Cargo.toml b/Cargo.toml index b873e7bb0..e9d267403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ bech32 = "0.9.1" cw-ownable = { git = "https://github.com/steak-enjoyers/cw-plus-plus", rev = "50d4d9333305894457e5028072a0465f4635b15b" } cw-admin-factory = { path = "./contracts/external/cw-admin-factory" } +cw-verifier-middleware = { path = "./contracts/external/cw-verifier-middleware"} cw-denom = { path = "./packages/cw-denom", version = "*" } cw-hooks = { path = "./packages/cw-hooks", version = "*" } cw-wormhole = { path = "./packages/cw-wormhole", version = "*" } diff --git a/contracts/external/cw-verifier-middleware/src/msg.rs b/contracts/external/cw-verifier-middleware/src/msg.rs index a99b402f6..41b1978e1 100644 --- a/contracts/external/cw-verifier-middleware/src/msg.rs +++ b/contracts/external/cw-verifier-middleware/src/msg.rs @@ -6,13 +6,14 @@ use cw_utils::Expiration; pub struct WrappedMessage { pub payload: Payload, // Assumes 'payload' has been hashed, signed, and base64 encoded - pub signature: Binary, - pub public_key: Binary, + pub signature: Binary, + pub public_key: String, } #[cw_serde] pub struct Payload { pub nonce: Uint128, + pub contract_address: String, pub msg: Binary, pub expiration: Option, } diff --git a/contracts/external/cw-verifier-middleware/src/state.rs b/contracts/external/cw-verifier-middleware/src/state.rs index 44568cd3e..ebbd15bc4 100644 --- a/contracts/external/cw-verifier-middleware/src/state.rs +++ b/contracts/external/cw-verifier-middleware/src/state.rs @@ -1,4 +1,11 @@ -use cosmwasm_std::Uint128; +use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::{Item, Map}; -pub const NONCES: Map<&str, Uint128> = Map::new("pk_to_nonce"); \ No newline at end of file +/// Nonce for each public key +pub const NONCES: Map<&str, Uint128> = Map::new("pk_to_nonce"); + +/// Contract address for which this middleware is used. +/// We require the contract address as part of the +/// payload to prevent replay attacks across contracts (a nonce may be used multiple times if there is no other +/// way to determine that it has already be used). +pub const CONTRACT_ADDRESS: Item = Item::new("contract_address"); diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index 2fcf513c7..1a9ad6326 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -1,19 +1,23 @@ -use cosmwasm_std::{to_binary, Uint128, testing::{mock_dependencies, mock_env, mock_info}}; -use secp256k1::{Secp256k1, Message, rand::rngs::OsRng}; -use sha2::{Sha256, Digest}; +use std::fmt::LowerHex; -use crate::{verify::{ec_pk_to_bech32_address, ADDR_PREFIX, verify}, msg::{Payload, WrappedMessage}}; +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, Uint128, +}; +use secp256k1::{rand::rngs::OsRng, Message, Secp256k1}; +use sha2::{Digest, Sha256}; + +use crate::{ + msg::{Payload, WrappedMessage}, + verify::{ec_pk_to_bech32_address, verify, ADDR_PREFIX}, +}; #[test] fn test_generate_juno_addr_from_pk() { - let juno_address = "juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw".to_string(); let juno_pk = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79".to_string(); - let generated_address = ec_pk_to_bech32_address( - juno_pk, - ADDR_PREFIX, - ).unwrap(); + let generated_address = ec_pk_to_bech32_address(juno_pk, ADDR_PREFIX).unwrap(); assert_eq!(generated_address, juno_address); } @@ -23,6 +27,7 @@ fn test_verify() { nonce: Uint128::from(0u128), msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjoiZXlKaFpHMXBiaUk2Ym5Wc2JDd2lZWFYwYjIxaGRHbGpZV3hzZVY5aFpHUmZZM2N5TUhNaU9uUnlkV1VzSW1GMWRHOXRZWFJwWTJGc2JIbGZZV1JrWDJOM056SXhjeUk2ZEhKMVpTd2laR1Z6WTNKcGNIUnBiMjRpT2lJeElpd2lhVzFoWjJWZmRYSnNJanB1ZFd4c0xDSnVZVzFsSWpvaWRHVnpkQ0lzSW5CeWIzQnZjMkZzWDIxdlpIVnNaWE5mYVc1emRHRnVkR2xoZEdWZmFXNW1ieUk2VzNzaVlXUnRhVzRpT25zaVkyOXlaVjl0YjJSMWJHVWlPbnQ5ZlN3aVkyOWtaVjlwWkNJNk1UWTVOQ3dpYkdGaVpXd2lPaUpFUVU5ZmRHVnpkRjlFWVc5UWNtOXdiM05oYkZOcGJtZHNaU0lzSW0xelp5STZJbVY1U21oaVIzaDJaREU1ZVZwWVduWmtSMngxV25sSk5scHRSbk5qTWxWelNXMU9jMkl6VG14WU0wSjVZak5DZG1NeVJuTllNamwxV0RKV05GcFhUakZrUjJ4MlltdzViVmxYYkhOa1dFcHNTV3B3TUdOdVZteE1RMHAwV1Zob1ptUnRPVEJoVnpWdVdETkNiR050YkhaYVEwazJaWGxLTUdGWE1XeEphbTh5VFVSUk5FMUVRamxNUTBwMFlWYzFabVJ0T1RCaFZ6VnVXRE5DYkdOdGJIWmFRMGsyWW01V2MySkRkMmxpTWpWelpWWTVkRnBYTVdsYVdFcDZXREpXTkZwWFRqRmtSMVZwVDI1U2VXUlhWWE5KYmtKNVdsWTVkMk50T1hkaU0wNXNXREpzZFZwdE9HbFBibk5wWWxjNWEyUlhlR3hZTWpGb1pWWTVkMk50T1hkaU0wNXNTV3B3TjBsdGJIVmFiVGhwVDI1emFWbFhVblJoVnpScFQyNXphVmt5T1hsYVZqbDBZakpTTVdKSFZXbFBiblE1WmxOM2FWa3lPV3RhVmpsd1drTkpOazFVV1RWTmFYZHBZa2RHYVZwWGQybFBhVXBGVVZVNVptUkhWbnBrUmpsM1kyMVZkR05JU25aalJ6bDZXbE14UlZsWE9WRmpiVGwzWWpOT2FHSkdUbkJpYldSeldsTkpjMGx0TVhwYWVVazJTVzFXTlZOdGRHRlhSVW95V1hwS2MwMUdaM2xpU0ZaaFlsUm9jRlF5TURGTlYwcElaRE5PU21KV1dUQmFSV1JYWkZkTmVXSklXbWxoVldzeVdsUk5kMk13YkhSUFdHUmhWbnBXYlZrd2FFdGtiVTVJVDFod1dsWXphRzFaZWs1WFlWZEtXR0pJY0dwTmJYZ3lXVzFzU2s1c2NIUlNiazVxVFd4Wk5VbHVNVGxtVTNkcFpFZG9lVnBZVG05aU1uaHJTV3B3TjBsdVVtOWpiVlo2WVVjNWMxcEdPWGhrVnpsNVpGY3dhVTl1YzJsaldGWjJZMjVXZEVscWNEZEpia0pzWTIxT2JHSnVVV2xQYVVsM1RHcEpkMGx1TUhOSmJsSnZZMjFXZW1GSE9YTmFRMGsyWlhsS2RGbFhjSFpqYld3d1pWTkpObVV6TVRsbVdERTVJbjFkTENKMmIzUnBibWRmYlc5a2RXeGxYMmx1YzNSaGJuUnBZWFJsWDJsdVptOGlPbnNpWVdSdGFXNGlPbnNpWTI5eVpWOXRiMlIxYkdVaU9udDlmU3dpWTI5a1pWOXBaQ0k2TVRZNU5pd2liR0ZpWld3aU9pSkVRVTlmZEdWemRGOUVZVzlXYjNScGJtZERkelFpTENKdGMyY2lPaUpsZVVwcVpIcFNabG96U25aa1dFSm1XVEk1YTFwV09YQmFRMGsyVFZSWk1rOURkMmxoVnpWd1pFZHNhR0pHT1hSYVZ6RnBXbGhLZWtscWNHSmxlVXBvV2tkU2VVbHFiMmxoYmxaMVlucEdNbU5ZYURKbFdHTXlZVE5DTlU0emFIRk5SekY2WlVodk1VNHpUakprTWpRd1kwUkNjbHB0VWpGT1JGcHlaR3BDZDJGNVNYTkpibVJzWVZka2IyUkRTVFpOV0RGa1psRTlQU0o5ZlE9PSIsImxhYmVsIjoidGVzdCJ9fQ==").unwrap(), expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), }; let secp = Secp256k1::new(); @@ -31,14 +36,20 @@ fn test_verify() { let msg = Message::from_slice(&msg_hash).unwrap(); let sig = secp.sign_ecdsa(&msg, &secret_key); + // println!("pub key {}", public_key); + + let pub_key = "CiECK/U4YJxozQDJMTU2AtnjWFcywHj+eErS5PKc9Iamr6I="; let wrapped_msg = WrappedMessage { payload, signature: sig.serialize_compact().into(), - public_key: public_key.serialize().into(), + public_key: public_key, }; + // println!("pub key {}", wrapped_msg.public_key); + let mut deps = mock_dependencies(); - let env = mock_env(); + let env = mock_env(); let info = mock_info("creator", &[]); - assert!(verify(deps.as_mut(), env, info, wrapped_msg).is_ok()); -} \ No newline at end of file + let err = verify(deps.as_mut(), env, info, wrapped_msg).err().unwrap(); + println!("{}", err); +} diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 0cc65597c..27a8952a2 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -1,87 +1,117 @@ -use bech32::{Variant, ToBase32}; -use cosmwasm_std::{Binary, Timestamp, to_binary, DepsMut, Env, Addr, MessageInfo, Uint128, StdError, OverflowError +use crate::{ + error::ContractError, + msg::WrappedMessage, + state::{CONTRACT_ADDRESS, NONCES}, +}; +use bech32::{ToBase32, Variant}; +use cosmwasm_schema::{cw_serde, serde::Serialize}; +use cosmwasm_std::{ + to_binary, Addr, Binary, DepsMut, Env, MessageInfo, OverflowError, StdError, Timestamp, Uint128, }; use cw_utils::Expiration; use ripemd::Ripemd160; -use sha2::{Sha256, Digest}; -use crate::{error::ContractError, state::NONCES, msg::WrappedMessage}; -use secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, ecdsa::Signature}; -use cosmwasm_schema::{cw_serde}; +use secp256k1::{ecdsa::Signature, Message as SecpMessage, PublicKey, Secp256k1}; +use sha2::{Digest, Sha256}; pub const ADDR_PREFIX: &str = "juno"; -pub fn verify(deps: DepsMut, env: Env, mut info: MessageInfo, wrapped_msg: WrappedMessage) -> Result{ - let secp = Secp256k1::verification_only(); - +pub fn verify( + deps: DepsMut, + env: Env, + mut info: MessageInfo, + wrapped_msg: WrappedMessage, +) -> Result { // Serialize the inner message let msg_ser = to_binary(&wrapped_msg.payload)?; // Hash the serialized payload using SHA-256 let msg_hash = Sha256::digest(&msg_ser); + println!("hex public key: {:?}", wrapped_msg.public_key.as_bytes()); // Verify the signature let sig_valid = deps.api.secp256k1_verify( - msg_hash.as_slice(), - &wrapped_msg.signature, - wrapped_msg.public_key.as_slice(), + msg_hash.as_slice(), + &wrapped_msg.signature, + to_binary(&wrapped_msg.public_key)?.as_slice(), )?; + println!("after verification"); + if !sig_valid { - return Err(ContractError::SignatureInvalid { }) + return Err(ContractError::SignatureInvalid {}); } // Validate that the message has the correct nonce - // todo: how to instantiate nonce first time - let pk_base64 = wrapped_msg.public_key.to_base64(); - let nonce = NONCES.load(deps.storage, &pk_base64)?; + let nonce = NONCES + .may_load(deps.storage, &wrapped_msg.public_key)? + .unwrap_or(Uint128::from(0u128)); + if wrapped_msg.payload.nonce != nonce { - return Err(ContractError::InvalidNonce { }); + return Err(ContractError::InvalidNonce {}); } - // Increment nonce - NONCES.update(deps.storage, &pk_base64, |nonce: Option| {nonce.unwrap_or_default().checked_add(Uint128::from(1u128)).map_err(|e| StdError::from(e))})?; + // Increment nonce + NONCES.update( + deps.storage, + &wrapped_msg.public_key, + |nonce: Option| { + nonce + .unwrap_or(Uint128::from(0u128)) + .checked_add(Uint128::from(1u128)) + .map_err(|e| StdError::from(e)) + }, + )?; // Validate that the message has not expired if let Some(expiration) = wrapped_msg.payload.expiration { if expiration.is_expired(&env.block) { - return Err(ContractError::MessageExpired { }); + return Err(ContractError::MessageExpired {}); } } + println!("{:?}", wrapped_msg.public_key); + // Set the message sender to the address corresponding to the provided public key. (pk_to_addr) - info.sender = ec_pk_to_bech32_address(wrapped_msg.public_key.to_base64(), ADDR_PREFIX)?; + info.sender = ec_pk_to_bech32_address(wrapped_msg.public_key.to_string(), ADDR_PREFIX)?; // Return the msg; caller will deserialize - return Ok(wrapped_msg.payload.msg) + return Ok(wrapped_msg.payload.msg); } +pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractError> { + CONTRACT_ADDRESS.save(deps.storage, &env.contract.address.to_string())?; + Ok(()) +} // takes an uncompressed EC public key and a prefix pub fn ec_pk_to_bech32_address(hex_pk: String, prefix: &str) -> Result { + println!("hex_pk: {:?}", hex_pk.len()); if hex_pk.clone().len() != 130 { - return Err(ContractError::Std( - StdError::InvalidHex { - msg: "unexpected hex encoded uncompressed public key length".to_string() - } - )); + return Err(ContractError::Std(StdError::InvalidHex { + msg: "unexpected hex encoded uncompressed public key length".to_string(), + })); } // get the raw public key bytes let decoded_pk = hex::decode(hex_pk); let raw_pk = match decoded_pk { Ok(pk) => pk, - Err(e) => return Err(ContractError::Std( - StdError::InvalidHex { msg: e.to_string() }) - ), + Err(e) => { + return Err(ContractError::Std(StdError::InvalidHex { + msg: e.to_string(), + })) + } }; // extract the compressed version of public key let public_key = secp256k1::PublicKey::from_slice(raw_pk.as_slice()); let raw_pk = match public_key { Ok(pk) => pk.serialize().to_vec(), - Err(e) => return Err(ContractError::Std( - StdError::GenericErr { msg: e.to_string() }, - )), + Err(e) => { + return Err(ContractError::Std(StdError::GenericErr { + msg: e.to_string(), + })) + } }; // sha256 the raw public key @@ -89,19 +119,12 @@ pub fn ec_pk_to_bech32_address(hex_pk: String, prefix: &str) -> Result Ok(Addr::unchecked(addr)), - Err(e) => Err(ContractError::Std( - StdError::generic_err(e.to_string()) - )), + Err(e) => Err(ContractError::Std(StdError::generic_err(e.to_string()))), } } - From 36ddaac3aff0f4275c87ec43e61003ae210c85cb Mon Sep 17 00:00:00 2001 From: bluenote Date: Wed, 22 Feb 2023 10:42:03 -0800 Subject: [PATCH 06/16] use correct encodings --- .../cw-verifier-middleware/src/error.rs | 13 ++-- .../cw-verifier-middleware/src/msg.rs | 8 ++- .../src/testing/tests.rs | 34 ++++----- .../cw-verifier-middleware/src/verify.rs | 70 ++++++------------- 4 files changed, 53 insertions(+), 72 deletions(-) diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index 2a297dc45..fa0e38f27 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -1,13 +1,15 @@ - -use cosmwasm_std::{StdError, OverflowError, VerificationError}; +use cosmwasm_std::{OverflowError, StdError, VerificationError}; +use hex::FromHexError; use thiserror::Error; -use secp256k1::Error as SecpError; #[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + FromHexError(#[from] FromHexError), + #[error("{0}")] VerificationError(#[from] VerificationError), @@ -19,6 +21,7 @@ pub enum ContractError { #[error("Message signature is invalid")] SignatureInvalid, -} - + #[error("Invalid uncompressed public key hex string length; expected 130 bytes, got {length}")] + InvalidPublicKeyLength { length: usize }, +} diff --git a/contracts/external/cw-verifier-middleware/src/msg.rs b/contracts/external/cw-verifier-middleware/src/msg.rs index 41b1978e1..59b91874c 100644 --- a/contracts/external/cw-verifier-middleware/src/msg.rs +++ b/contracts/external/cw-verifier-middleware/src/msg.rs @@ -1,13 +1,13 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Binary, Uint128}; +use cosmwasm_std::{Binary, HexBinary, Uint128}; use cw_utils::Expiration; #[cw_serde] pub struct WrappedMessage { pub payload: Payload, - // Assumes 'payload' has been hashed, signed, and base64 encoded + // Assumes signature is 'payload' hashed, signed, and base64 encoded pub signature: Binary, - pub public_key: String, + pub public_key: HexBinary, // hex encoded } #[cw_serde] @@ -16,4 +16,6 @@ pub struct Payload { pub contract_address: String, pub msg: Binary, pub expiration: Option, + pub bech32_prefix: String, + pub version: String, } diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index 1a9ad6326..01c3f038e 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -1,55 +1,57 @@ -use std::fmt::LowerHex; - use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, - to_binary, Addr, Uint128, + to_binary, Addr, HexBinary, Uint128, }; use secp256k1::{rand::rngs::OsRng, Message, Secp256k1}; use sha2::{Digest, Sha256}; use crate::{ msg::{Payload, WrappedMessage}, - verify::{ec_pk_to_bech32_address, verify, ADDR_PREFIX}, + verify::{pk_to_addr, verify}, }; #[test] -fn test_generate_juno_addr_from_pk() { - let juno_address = "juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw".to_string(); +fn test_pk_to_addr() { + let juno_address = Addr::unchecked("juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw"); let juno_pk = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79".to_string(); - let generated_address = ec_pk_to_bech32_address(juno_pk, ADDR_PREFIX).unwrap(); + let generated_address = pk_to_addr(juno_pk, "juno").unwrap(); assert_eq!(generated_address, juno_address); } #[test] -fn test_verify() { +fn test_verify_success() { + // This test generates a payload in which the signature is of base64 format, and the public key is of hex format. + // The test then calls verify to validate that the signature is correctly verified. + let payload = Payload { nonce: Uint128::from(0u128), msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjoiZXlKaFpHMXBiaUk2Ym5Wc2JDd2lZWFYwYjIxaGRHbGpZV3hzZVY5aFpHUmZZM2N5TUhNaU9uUnlkV1VzSW1GMWRHOXRZWFJwWTJGc2JIbGZZV1JrWDJOM056SXhjeUk2ZEhKMVpTd2laR1Z6WTNKcGNIUnBiMjRpT2lJeElpd2lhVzFoWjJWZmRYSnNJanB1ZFd4c0xDSnVZVzFsSWpvaWRHVnpkQ0lzSW5CeWIzQnZjMkZzWDIxdlpIVnNaWE5mYVc1emRHRnVkR2xoZEdWZmFXNW1ieUk2VzNzaVlXUnRhVzRpT25zaVkyOXlaVjl0YjJSMWJHVWlPbnQ5ZlN3aVkyOWtaVjlwWkNJNk1UWTVOQ3dpYkdGaVpXd2lPaUpFUVU5ZmRHVnpkRjlFWVc5UWNtOXdiM05oYkZOcGJtZHNaU0lzSW0xelp5STZJbVY1U21oaVIzaDJaREU1ZVZwWVduWmtSMngxV25sSk5scHRSbk5qTWxWelNXMU9jMkl6VG14WU0wSjVZak5DZG1NeVJuTllNamwxV0RKV05GcFhUakZrUjJ4MlltdzViVmxYYkhOa1dFcHNTV3B3TUdOdVZteE1RMHAwV1Zob1ptUnRPVEJoVnpWdVdETkNiR050YkhaYVEwazJaWGxLTUdGWE1XeEphbTh5VFVSUk5FMUVRamxNUTBwMFlWYzFabVJ0T1RCaFZ6VnVXRE5DYkdOdGJIWmFRMGsyWW01V2MySkRkMmxpTWpWelpWWTVkRnBYTVdsYVdFcDZXREpXTkZwWFRqRmtSMVZwVDI1U2VXUlhWWE5KYmtKNVdsWTVkMk50T1hkaU0wNXNXREpzZFZwdE9HbFBibk5wWWxjNWEyUlhlR3hZTWpGb1pWWTVkMk50T1hkaU0wNXNTV3B3TjBsdGJIVmFiVGhwVDI1emFWbFhVblJoVnpScFQyNXphVmt5T1hsYVZqbDBZakpTTVdKSFZXbFBiblE1WmxOM2FWa3lPV3RhVmpsd1drTkpOazFVV1RWTmFYZHBZa2RHYVZwWGQybFBhVXBGVVZVNVptUkhWbnBrUmpsM1kyMVZkR05JU25aalJ6bDZXbE14UlZsWE9WRmpiVGwzWWpOT2FHSkdUbkJpYldSeldsTkpjMGx0TVhwYWVVazJTVzFXTlZOdGRHRlhSVW95V1hwS2MwMUdaM2xpU0ZaaFlsUm9jRlF5TURGTlYwcElaRE5PU21KV1dUQmFSV1JYWkZkTmVXSklXbWxoVldzeVdsUk5kMk13YkhSUFdHUmhWbnBXYlZrd2FFdGtiVTVJVDFod1dsWXphRzFaZWs1WFlWZEtXR0pJY0dwTmJYZ3lXVzFzU2s1c2NIUlNiazVxVFd4Wk5VbHVNVGxtVTNkcFpFZG9lVnBZVG05aU1uaHJTV3B3TjBsdVVtOWpiVlo2WVVjNWMxcEdPWGhrVnpsNVpGY3dhVTl1YzJsaldGWjJZMjVXZEVscWNEZEpia0pzWTIxT2JHSnVVV2xQYVVsM1RHcEpkMGx1TUhOSmJsSnZZMjFXZW1GSE9YTmFRMGsyWlhsS2RGbFhjSFpqYld3d1pWTkpObVV6TVRsbVdERTVJbjFkTENKMmIzUnBibWRmYlc5a2RXeGxYMmx1YzNSaGJuUnBZWFJsWDJsdVptOGlPbnNpWVdSdGFXNGlPbnNpWTI5eVpWOXRiMlIxYkdVaU9udDlmU3dpWTI5a1pWOXBaQ0k2TVRZNU5pd2liR0ZpWld3aU9pSkVRVTlmZEdWemRGOUVZVzlXYjNScGJtZERkelFpTENKdGMyY2lPaUpsZVVwcVpIcFNabG96U25aa1dFSm1XVEk1YTFwV09YQmFRMGsyVFZSWk1rOURkMmxoVnpWd1pFZHNhR0pHT1hSYVZ6RnBXbGhLZWtscWNHSmxlVXBvV2tkU2VVbHFiMmxoYmxaMVlucEdNbU5ZYURKbFdHTXlZVE5DTlU0emFIRk5SekY2WlVodk1VNHpUakprTWpRd1kwUkNjbHB0VWpGT1JGcHlaR3BDZDJGNVNYTkpibVJzWVZka2IyUkRTVFpOV0RGa1psRTlQU0o5ZlE9PSIsImxhYmVsIjoidGVzdCJ9fQ==").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + version: "version-1".to_string(), }; + // Generate a keypair let secp = Secp256k1::new(); let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + + // Hash and sign the payload let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); let msg = Message::from_slice(&msg_hash).unwrap(); let sig = secp.sign_ecdsa(&msg, &secret_key); - // println!("pub key {}", public_key); - - let pub_key = "CiECK/U4YJxozQDJMTU2AtnjWFcywHj+eErS5PKc9Iamr6I="; + // Wrap the message + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); let wrapped_msg = WrappedMessage { payload, signature: sig.serialize_compact().into(), - public_key: public_key, + public_key: hex_encoded, }; - // println!("pub key {}", wrapped_msg.public_key); - let mut deps = mock_dependencies(); let env = mock_env(); let info = mock_info("creator", &[]); - let err = verify(deps.as_mut(), env, info, wrapped_msg).err().unwrap(); - println!("{}", err); + verify(deps.as_mut(), env, info, wrapped_msg).unwrap(); } diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 27a8952a2..c7bed91f5 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -4,16 +4,12 @@ use crate::{ state::{CONTRACT_ADDRESS, NONCES}, }; use bech32::{ToBase32, Variant}; -use cosmwasm_schema::{cw_serde, serde::Serialize}; -use cosmwasm_std::{ - to_binary, Addr, Binary, DepsMut, Env, MessageInfo, OverflowError, StdError, Timestamp, Uint128, -}; -use cw_utils::Expiration; +use cosmwasm_std::{to_binary, Addr, Binary, DepsMut, Env, MessageInfo, StdError, Uint128}; + use ripemd::Ripemd160; -use secp256k1::{ecdsa::Signature, Message as SecpMessage, PublicKey, Secp256k1}; use sha2::{Digest, Sha256}; -pub const ADDR_PREFIX: &str = "juno"; +const EXPECTED_HEX_PK_LEN: usize = 130; pub fn verify( deps: DepsMut, @@ -27,23 +23,20 @@ pub fn verify( // Hash the serialized payload using SHA-256 let msg_hash = Sha256::digest(&msg_ser); - println!("hex public key: {:?}", wrapped_msg.public_key.as_bytes()); // Verify the signature let sig_valid = deps.api.secp256k1_verify( msg_hash.as_slice(), &wrapped_msg.signature, - to_binary(&wrapped_msg.public_key)?.as_slice(), + wrapped_msg.public_key.as_slice(), )?; - println!("after verification"); - if !sig_valid { return Err(ContractError::SignatureInvalid {}); } // Validate that the message has the correct nonce let nonce = NONCES - .may_load(deps.storage, &wrapped_msg.public_key)? + .may_load(deps.storage, &wrapped_msg.public_key.to_hex())? .unwrap_or(Uint128::from(0u128)); if wrapped_msg.payload.nonce != nonce { @@ -53,7 +46,7 @@ pub fn verify( // Increment nonce NONCES.update( deps.storage, - &wrapped_msg.public_key, + &wrapped_msg.public_key.to_string(), |nonce: Option| { nonce .unwrap_or(Uint128::from(0u128)) @@ -69,10 +62,11 @@ pub fn verify( } } - println!("{:?}", wrapped_msg.public_key); - - // Set the message sender to the address corresponding to the provided public key. (pk_to_addr) - info.sender = ec_pk_to_bech32_address(wrapped_msg.public_key.to_string(), ADDR_PREFIX)?; + // Set the message sender to the address corresponding to the provided public key. + info.sender = pk_to_addr( + wrapped_msg.public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes + &wrapped_msg.payload.bech32_prefix, + )?; // Return the msg; caller will deserialize return Ok(wrapped_msg.payload.msg); @@ -83,44 +77,24 @@ pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractE Ok(()) } -// takes an uncompressed EC public key and a prefix -pub fn ec_pk_to_bech32_address(hex_pk: String, prefix: &str) -> Result { - println!("hex_pk: {:?}", hex_pk.len()); - if hex_pk.clone().len() != 130 { - return Err(ContractError::Std(StdError::InvalidHex { - msg: "unexpected hex encoded uncompressed public key length".to_string(), - })); +// Takes an uncompressed hex-encoded EC public key and a bech32 prefix and derives the bech32 address. +pub fn pk_to_addr(hex_pk: String, prefix: &str) -> Result { + if hex_pk.len() != EXPECTED_HEX_PK_LEN { + return Err(ContractError::InvalidPublicKeyLength { + length: hex_pk.len(), + }); } - // get the raw public key bytes - let decoded_pk = hex::decode(hex_pk); - let raw_pk = match decoded_pk { - Ok(pk) => pk, - Err(e) => { - return Err(ContractError::Std(StdError::InvalidHex { - msg: e.to_string(), - })) - } - }; - - // extract the compressed version of public key - let public_key = secp256k1::PublicKey::from_slice(raw_pk.as_slice()); - let raw_pk = match public_key { - Ok(pk) => pk.serialize().to_vec(), - Err(e) => { - return Err(ContractError::Std(StdError::GenericErr { - msg: e.to_string(), - })) - } - }; + // Decode PK from hex + let raw_pk = hex::decode(hex_pk)?; - // sha256 the raw public key + // sha256 hash the raw public key let pk_sha256 = Sha256::digest(raw_pk); - // take the ripemd160 of the sha256 of the raw pk + // Take the ripemd160 of the sha256 of the raw pk let address_raw = Ripemd160::digest(pk_sha256); - // encode the prefix and the raw address bytes with Bech32 + // Encode the prefix and the raw address bytes with bech32 let bech32 = bech32::encode(&prefix, address_raw.to_base32(), Variant::Bech32); match bech32 { From a2e3f21d03639f5ccf901325e9cc8943f1aa0453 Mon Sep 17 00:00:00 2001 From: bluenote Date: Wed, 22 Feb 2023 14:29:11 -0800 Subject: [PATCH 07/16] add both compressed and uncompressed support --- .../cw-verifier-middleware/src/error.rs | 10 ++++- .../src/testing/tests.rs | 15 ++++++- .../cw-verifier-middleware/src/verify.rs | 40 +++++++++++-------- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index fa0e38f27..753990dc5 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -1,5 +1,7 @@ -use cosmwasm_std::{OverflowError, StdError, VerificationError}; +use bech32::Error as Bech32Error; +use cosmwasm_std::{StdError, VerificationError}; use hex::FromHexError; +use secp256k1::Error as Secp256k1Error; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -13,6 +15,12 @@ pub enum ContractError { #[error("{0}")] VerificationError(#[from] VerificationError), + #[error("{0}")] + Bech32Error(#[from] Bech32Error), + + #[error("{0}")] + Secp256k1Error(#[from] Secp256k1Error), + #[error("Invalid nonce")] InvalidNonce, diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index 01c3f038e..559221093 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -11,11 +11,22 @@ use crate::{ }; #[test] -fn test_pk_to_addr() { +fn test_pk_to_addr_uncompressed() { let juno_address = Addr::unchecked("juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw"); let juno_pk = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79".to_string(); - let generated_address = pk_to_addr(juno_pk, "juno").unwrap(); + let deps = mock_dependencies(); + let generated_address = pk_to_addr(deps.as_ref(), juno_pk, &"juno").unwrap(); + assert_eq!(generated_address, juno_address); +} + +#[test] +fn test_pk_to_addr_compressed() { + let juno_address = Addr::unchecked("juno1vqxvyw6kpy7xj0msxz57svwn4p0kfdu46kv0pk"); + let juno_pk = "022bf538609c68cd00c931353602d9e3585732c078fe784ad2e4f29cf486a6afa2".to_string(); + + let deps = mock_dependencies(); + let generated_address = pk_to_addr(deps.as_ref(), juno_pk, &"juno").unwrap(); assert_eq!(generated_address, juno_address); } diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index c7bed91f5..51e25a36b 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -4,12 +4,13 @@ use crate::{ state::{CONTRACT_ADDRESS, NONCES}, }; use bech32::{ToBase32, Variant}; -use cosmwasm_std::{to_binary, Addr, Binary, DepsMut, Env, MessageInfo, StdError, Uint128}; +use cosmwasm_std::{to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, StdError, Uint128}; use ripemd::Ripemd160; use sha2::{Digest, Sha256}; -const EXPECTED_HEX_PK_LEN: usize = 130; +const UNCOMPRESSED_HEX_PK_LEN: usize = 130; +const COMPRESSED_HEX_PK_LEN: usize = 66; pub fn verify( deps: DepsMut, @@ -64,6 +65,7 @@ pub fn verify( // Set the message sender to the address corresponding to the provided public key. info.sender = pk_to_addr( + deps.as_ref(), wrapped_msg.public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes &wrapped_msg.payload.bech32_prefix, )?; @@ -77,16 +79,24 @@ pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractE Ok(()) } -// Takes an uncompressed hex-encoded EC public key and a bech32 prefix and derives the bech32 address. -pub fn pk_to_addr(hex_pk: String, prefix: &str) -> Result { - if hex_pk.len() != EXPECTED_HEX_PK_LEN { - return Err(ContractError::InvalidPublicKeyLength { - length: hex_pk.len(), - }); - } - +// Takes an compressed or uncompressed hex-encoded EC public key and a bech32 prefix and derives the bech32 address. +pub fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result { // Decode PK from hex - let raw_pk = hex::decode(hex_pk)?; + let raw_pk = hex::decode(&hex_pk)?; + + let raw_pk: Vec = match hex_pk.len() { + COMPRESSED_HEX_PK_LEN => Ok::, ContractError>(raw_pk), + UNCOMPRESSED_HEX_PK_LEN => { + let public_key = secp256k1::PublicKey::from_slice(raw_pk.as_slice())?; + // serialize will convert pk to compressed format + Ok(public_key.serialize().to_vec()) + } + _ => { + return Err(ContractError::InvalidPublicKeyLength { + length: hex_pk.len(), + }) + } + }?; // sha256 hash the raw public key let pk_sha256 = Sha256::digest(raw_pk); @@ -95,10 +105,8 @@ pub fn pk_to_addr(hex_pk: String, prefix: &str) -> Result { let address_raw = Ripemd160::digest(pk_sha256); // Encode the prefix and the raw address bytes with bech32 - let bech32 = bech32::encode(&prefix, address_raw.to_base32(), Variant::Bech32); + let bech32 = bech32::encode(&prefix, address_raw.to_base32(), Variant::Bech32)?; - match bech32 { - Ok(addr) => Ok(Addr::unchecked(addr)), - Err(e) => Err(ContractError::Std(StdError::generic_err(e.to_string()))), - } + // Return validated addr + Ok(deps.api.addr_validate(&bech32)?) } From 6e2856e0e1d3c64cb9591149c3ce5c99147ac8ba Mon Sep 17 00:00:00 2001 From: bluenote Date: Wed, 22 Feb 2023 14:50:51 -0800 Subject: [PATCH 08/16] add more tests --- .../src/testing/tests.rs | 103 +++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index 559221093..9ad6a2dec 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -6,7 +6,9 @@ use secp256k1::{rand::rngs::OsRng, Message, Secp256k1}; use sha2::{Digest, Sha256}; use crate::{ + error::ContractError, msg::{Payload, WrappedMessage}, + state::NONCES, verify::{pk_to_addr, verify}, }; @@ -37,7 +39,7 @@ fn test_verify_success() { let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjoiZXlKaFpHMXBiaUk2Ym5Wc2JDd2lZWFYwYjIxaGRHbGpZV3hzZVY5aFpHUmZZM2N5TUhNaU9uUnlkV1VzSW1GMWRHOXRZWFJwWTJGc2JIbGZZV1JrWDJOM056SXhjeUk2ZEhKMVpTd2laR1Z6WTNKcGNIUnBiMjRpT2lJeElpd2lhVzFoWjJWZmRYSnNJanB1ZFd4c0xDSnVZVzFsSWpvaWRHVnpkQ0lzSW5CeWIzQnZjMkZzWDIxdlpIVnNaWE5mYVc1emRHRnVkR2xoZEdWZmFXNW1ieUk2VzNzaVlXUnRhVzRpT25zaVkyOXlaVjl0YjJSMWJHVWlPbnQ5ZlN3aVkyOWtaVjlwWkNJNk1UWTVOQ3dpYkdGaVpXd2lPaUpFUVU5ZmRHVnpkRjlFWVc5UWNtOXdiM05oYkZOcGJtZHNaU0lzSW0xelp5STZJbVY1U21oaVIzaDJaREU1ZVZwWVduWmtSMngxV25sSk5scHRSbk5qTWxWelNXMU9jMkl6VG14WU0wSjVZak5DZG1NeVJuTllNamwxV0RKV05GcFhUakZrUjJ4MlltdzViVmxYYkhOa1dFcHNTV3B3TUdOdVZteE1RMHAwV1Zob1ptUnRPVEJoVnpWdVdETkNiR050YkhaYVEwazJaWGxLTUdGWE1XeEphbTh5VFVSUk5FMUVRamxNUTBwMFlWYzFabVJ0T1RCaFZ6VnVXRE5DYkdOdGJIWmFRMGsyWW01V2MySkRkMmxpTWpWelpWWTVkRnBYTVdsYVdFcDZXREpXTkZwWFRqRmtSMVZwVDI1U2VXUlhWWE5KYmtKNVdsWTVkMk50T1hkaU0wNXNXREpzZFZwdE9HbFBibk5wWWxjNWEyUlhlR3hZTWpGb1pWWTVkMk50T1hkaU0wNXNTV3B3TjBsdGJIVmFiVGhwVDI1emFWbFhVblJoVnpScFQyNXphVmt5T1hsYVZqbDBZakpTTVdKSFZXbFBiblE1WmxOM2FWa3lPV3RhVmpsd1drTkpOazFVV1RWTmFYZHBZa2RHYVZwWGQybFBhVXBGVVZVNVptUkhWbnBrUmpsM1kyMVZkR05JU25aalJ6bDZXbE14UlZsWE9WRmpiVGwzWWpOT2FHSkdUbkJpYldSeldsTkpjMGx0TVhwYWVVazJTVzFXTlZOdGRHRlhSVW95V1hwS2MwMUdaM2xpU0ZaaFlsUm9jRlF5TURGTlYwcElaRE5PU21KV1dUQmFSV1JYWkZkTmVXSklXbWxoVldzeVdsUk5kMk13YkhSUFdHUmhWbnBXYlZrd2FFdGtiVTVJVDFod1dsWXphRzFaZWs1WFlWZEtXR0pJY0dwTmJYZ3lXVzFzU2s1c2NIUlNiazVxVFd4Wk5VbHVNVGxtVTNkcFpFZG9lVnBZVG05aU1uaHJTV3B3TjBsdVVtOWpiVlo2WVVjNWMxcEdPWGhrVnpsNVpGY3dhVTl1YzJsaldGWjJZMjVXZEVscWNEZEpia0pzWTIxT2JHSnVVV2xQYVVsM1RHcEpkMGx1TUhOSmJsSnZZMjFXZW1GSE9YTmFRMGsyWlhsS2RGbFhjSFpqYld3d1pWTkpObVV6TVRsbVdERTVJbjFkTENKMmIzUnBibWRmYlc5a2RXeGxYMmx1YzNSaGJuUnBZWFJsWDJsdVptOGlPbnNpWVdSdGFXNGlPbnNpWTI5eVpWOXRiMlIxYkdVaU9udDlmU3dpWTI5a1pWOXBaQ0k2TVRZNU5pd2liR0ZpWld3aU9pSkVRVTlmZEdWemRGOUVZVzlXYjNScGJtZERkelFpTENKdGMyY2lPaUpsZVVwcVpIcFNabG96U25aa1dFSm1XVEk1YTFwV09YQmFRMGsyVFZSWk1rOURkMmxoVnpWd1pFZHNhR0pHT1hSYVZ6RnBXbGhLZWtscWNHSmxlVXBvV2tkU2VVbHFiMmxoYmxaMVlucEdNbU5ZYURKbFdHTXlZVE5DTlU0emFIRk5SekY2WlVodk1VNHpUakprTWpRd1kwUkNjbHB0VWpGT1JGcHlaR3BDZDJGNVNYTkpibVJzWVZka2IyUkRTVFpOV0RGa1psRTlQU0o5ZlE9PSIsImxhYmVsIjoidGVzdCJ9fQ==").unwrap(), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "juno".to_string(), @@ -58,11 +60,108 @@ fn test_verify_success() { let wrapped_msg = WrappedMessage { payload, signature: sig.serialize_compact().into(), - public_key: hex_encoded, + public_key: hex_encoded.clone(), }; + // Verify let mut deps = mock_dependencies(); let env = mock_env(); let info = mock_info("creator", &[]); verify(deps.as_mut(), env, info, wrapped_msg).unwrap(); + + // Verify nonce was incremented correctly + let nonce = NONCES.load(&deps.storage, &hex_encoded.to_hex()).unwrap(); + assert_eq!(nonce, Uint128::from(1u128)) +} + +#[test] +fn test_verify_pk_invalid() { + // This test generates a payload in which the signature is of base64 format, and the public key is of hex format. + // The test then calls verify with an incorrectly formatted public key to validate that there is an error in parsing the public key. + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("test").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + version: "version-1".to_string(), + }; + + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, _) = secp.generate_keypair(&mut OsRng); + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + // Wrap the message but with incorrect public key + let wrapped_msg = WrappedMessage { + payload, + signature: sig.serialize_compact().into(), + public_key: Vec::from("incorrect_public_key").into(), + }; + + // Verify with incorrect public key + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + let result = verify(deps.as_mut(), env, info, wrapped_msg); + + // Ensure that there was a pub key verification error + assert!(matches!(result, Err(ContractError::VerificationError(_)))); +} + +#[test] +fn test_verify_wrong_pk() { + // This test generates a payload in which the signature is of base64 format, and the public key is of hex format. + // The test then calls verify with an correctly formatted but different public key (that doesn't correspond to the signature) to validate that the signature is not verified. + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("test").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + version: "version-1".to_string(), + }; + + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, _) = secp.generate_keypair(&mut OsRng); + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + // Generate another keypair + let secp = Secp256k1::new(); + let (_, public_key) = secp.generate_keypair(&mut OsRng); + + // Wrap the message but with incorrect public key + let wrapped_msg = WrappedMessage { + payload, + signature: sig.serialize_compact().into(), + public_key: HexBinary::from(public_key.serialize_uncompressed()), + }; + + // Verify with incorrect public key + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + let result = verify(deps.as_mut(), env, info, wrapped_msg); + + // Ensure that there was a signature verification error + assert!(matches!(result, Err(ContractError::SignatureInvalid))); } + +/* +Moar tests to write: +signature is invalid / malformed +incorrect nonce +expired message +load a keypair corresponding to pre-known address and validate that address in info was set correctly +*/ From 7be826c5b7368f3c8f849e7415cb424cefe9736b Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 4 Mar 2023 22:42:03 +0100 Subject: [PATCH 09/16] tests for pk to address generation; signature verification; nonce; expired msg --- .../cw-verifier-middleware/src/state.rs | 2 +- .../src/testing/tests.rs | 241 +++++++++++++++++- 2 files changed, 229 insertions(+), 14 deletions(-) diff --git a/contracts/external/cw-verifier-middleware/src/state.rs b/contracts/external/cw-verifier-middleware/src/state.rs index ebbd15bc4..ad5b21ad3 100644 --- a/contracts/external/cw-verifier-middleware/src/state.rs +++ b/contracts/external/cw-verifier-middleware/src/state.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Uint128; use cw_storage_plus::{Item, Map}; /// Nonce for each public key diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index 9ad6a2dec..410b1ae03 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -12,24 +12,75 @@ use crate::{ verify::{pk_to_addr, verify}, }; +pub const JUNO_ADDRESS: &str = "juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw"; +pub const COMPRESSED_PK: &str = "03f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28"; +pub const UNCOMPRESSED_PK: &str = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79"; +pub const JUNO_PREFIX: &str = "juno"; + #[test] fn test_pk_to_addr_uncompressed() { - let juno_address = Addr::unchecked("juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw"); - let juno_pk = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79".to_string(); let deps = mock_dependencies(); - let generated_address = pk_to_addr(deps.as_ref(), juno_pk, &"juno").unwrap(); - assert_eq!(generated_address, juno_address); + let generated_address = pk_to_addr( + deps.as_ref(), + UNCOMPRESSED_PK.to_string(), + JUNO_PREFIX, + ).unwrap(); + + assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); } #[test] fn test_pk_to_addr_compressed() { - let juno_address = Addr::unchecked("juno1vqxvyw6kpy7xj0msxz57svwn4p0kfdu46kv0pk"); - let juno_pk = "022bf538609c68cd00c931353602d9e3585732c078fe784ad2e4f29cf486a6afa2".to_string(); let deps = mock_dependencies(); - let generated_address = pk_to_addr(deps.as_ref(), juno_pk, &"juno").unwrap(); - assert_eq!(generated_address, juno_address); + let generated_address = pk_to_addr( + deps.as_ref(), + COMPRESSED_PK.to_string(), + JUNO_PREFIX, + ).unwrap(); + assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); +} + +#[test] +fn test_pk_to_addr_invalid_hex_length() { + + let invalid_length_pk = "".to_string(); + let deps = mock_dependencies(); + let err: ContractError = pk_to_addr( + deps.as_ref(), + invalid_length_pk, + JUNO_PREFIX, + ).unwrap_err(); + + assert!(matches!(err, ContractError::InvalidPublicKeyLength { .. })); +} + +#[test] +fn test_pk_to_addr_not_hex_pk() { + + let non_hex_pk = "03zzzzcd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28".to_string(); + let deps = mock_dependencies(); + let err: ContractError = pk_to_addr( + deps.as_ref(), + non_hex_pk, + JUNO_PREFIX, + ).unwrap_err(); + + assert!(matches!(err, ContractError::FromHexError { .. })); +} + +#[test] +fn test_pk_to_addr_bech32_invalid_human_readable_part() { + + let deps = mock_dependencies(); + let err: ContractError = pk_to_addr( + deps.as_ref(), + UNCOMPRESSED_PK.to_string(), + "jUnO", + ).unwrap_err(); + + assert!(matches!(err, ContractError::Bech32Error { .. })); } #[test] @@ -42,7 +93,7 @@ fn test_verify_success() { msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), - bech32_prefix: "juno".to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), version: "version-1".to_string(), }; @@ -124,7 +175,7 @@ fn test_verify_wrong_pk() { msg: to_binary("test").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), - bech32_prefix: "juno".to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), version: "version-1".to_string(), }; @@ -158,10 +209,174 @@ fn test_verify_wrong_pk() { assert!(matches!(result, Err(ContractError::SignatureInvalid))); } +#[test] +fn test_verify_incorrect_nonce() { + + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // get a default wrapped message and verify it + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), + version: "version-1".to_string(), + }; + let wrapped_msg = get_wrapped_msg(payload); + verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap(); + + // skip a nonce iteration + let invalid_nonce_payload = Payload { + nonce: Uint128::from(3u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), + version: "version-1".to_string(), + }; + let wrapped_msg = get_wrapped_msg(invalid_nonce_payload); + let err = verify(deps.as_mut(), env, info, wrapped_msg).unwrap_err(); + + // verify the invalid nonce error + assert!(matches!(err, ContractError::InvalidNonce)); +} + +#[test] +fn test_verify_expired_message() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // get an expired message + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + expiration: Some(cw_utils::Expiration::AtHeight(0)), + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), + version: "version-1".to_string(), + }; + let wrapped_msg = get_wrapped_msg(payload); + + let err: ContractError = verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap_err(); + + assert!(matches!(err, ContractError::MessageExpired)); +} + +#[test] +fn test_verify_invalid_signature() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), + version: "version-1".to_string(), + }; + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); + + // Wrap a different message with the existing signature + let different_payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "cosmos".to_string(), + version: "version-1".to_string(), + }; + + let wrapped_msg = WrappedMessage { + payload: different_payload, + signature: sig.serialize_compact().into(), + public_key: hex_encoded.clone(), + }; + + + let err: ContractError = verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap_err(); + + assert!(matches!(err, ContractError::SignatureInvalid { .. })); +} + +#[test] +fn test_verify_malformed_signature() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), + version: "version-1".to_string(), + }; + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); + let serialized_sig = sig.serialize_compact(); + + // join two signatures for unexpected format + let malformed_sig = [serialized_sig, serialized_sig].concat(); + let wrapped_msg = WrappedMessage { + payload, + signature: malformed_sig.into(), + public_key: hex_encoded.clone(), + }; + + let err: ContractError = verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap_err(); + assert!(matches!(err, ContractError::VerificationError { .. })); +} + /* Moar tests to write: -signature is invalid / malformed -incorrect nonce -expired message +nonce overflow? load a keypair corresponding to pre-known address and validate that address in info was set correctly */ + + +// signs a given payload and returns the wrapped message +fn get_wrapped_msg(payload: Payload) -> WrappedMessage { + + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + // Wrap the message + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); + WrappedMessage { + payload, + signature: sig.serialize_compact().into(), + public_key: hex_encoded.clone(), + } +} \ No newline at end of file From 12b7a5b7e190825ca5b6d0488f77a870776f52a6 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 5 Mar 2023 18:33:34 +0100 Subject: [PATCH 10/16] returning MessageInfo along with msg in verify() --- .../src/testing/adversarial_tests.rs | 6 +++ .../cw-verifier-middleware/src/testing/mod.rs | 1 + .../src/testing/tests.rs | 44 ++++++++++++++++++- .../cw-verifier-middleware/src/verify.rs | 6 +-- 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 contracts/external/cw-verifier-middleware/src/testing/adversarial_tests.rs diff --git a/contracts/external/cw-verifier-middleware/src/testing/adversarial_tests.rs b/contracts/external/cw-verifier-middleware/src/testing/adversarial_tests.rs new file mode 100644 index 000000000..f4c513987 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/testing/adversarial_tests.rs @@ -0,0 +1,6 @@ +// TODO: test where a wrapped message passed to the verifier contains +// a payload with a large (in size) msg that was pre-signed +#[test] +fn test_verify_big_payload() { +} + diff --git a/contracts/external/cw-verifier-middleware/src/testing/mod.rs b/contracts/external/cw-verifier-middleware/src/testing/mod.rs index 14f00389d..4ddd54de5 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/mod.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/mod.rs @@ -1 +1,2 @@ mod tests; +mod adversarial_tests; \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index 410b1ae03..9d5006808 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -353,10 +353,52 @@ fn test_verify_malformed_signature() { assert!(matches!(err, ContractError::VerificationError { .. })); } +#[test] +fn test_verify_sets_sender_to_pk_address_representation() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: JUNO_PREFIX.to_string(), + version: "version-1".to_string(), + }; + + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + // Wrap the message + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); + let wrapped_msg = WrappedMessage { + payload: payload.clone(), + signature: sig.serialize_compact().into(), + public_key: hex_encoded.clone(), + }; + + let (verified_msg, verified_info) = verify(deps.as_mut(), env.clone(), info, wrapped_msg).unwrap(); + + // pk_to_addr is tested above so assumed to be working + // TODO: generate a new keypair to avoid generating in every test + let respective_address = pk_to_addr(deps.as_ref(), hex_encoded.to_string(), JUNO_PREFIX).unwrap(); + + // assert that info.sender is set to the expected representation of pk + assert_eq!(verified_info.sender, respective_address); + assert_eq!(verified_msg, payload.msg); +} + /* Moar tests to write: nonce overflow? -load a keypair corresponding to pre-known address and validate that address in info was set correctly */ diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 51e25a36b..d4b7729b3 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -17,7 +17,7 @@ pub fn verify( env: Env, mut info: MessageInfo, wrapped_msg: WrappedMessage, -) -> Result { +) -> Result<(Binary, MessageInfo), ContractError> { // Serialize the inner message let msg_ser = to_binary(&wrapped_msg.payload)?; @@ -70,8 +70,8 @@ pub fn verify( &wrapped_msg.payload.bech32_prefix, )?; - // Return the msg; caller will deserialize - return Ok(wrapped_msg.payload.msg); + // Return info with updater sender and msg to be deserialized by caller + return Ok((wrapped_msg.payload.msg, info)); } pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractError> { From 0ca721f166acc8931db9aa5581dce476ec10f40b Mon Sep 17 00:00:00 2001 From: bluenote Date: Thu, 2 Mar 2023 14:37:11 -0700 Subject: [PATCH 11/16] add more tests --- Cargo.lock | 1 + Cargo.toml | 2 + .../cw-verifier-middleware/Cargo.toml | 1 + .../cw-verifier-middleware/src/error.rs | 4 + .../cw-verifier-middleware/src/msg.rs | 3 +- .../cw-verifier-middleware/src/state.rs | 6 +- .../src/testing/tests.rs | 252 +++++++++++------- .../cw-verifier-middleware/src/verify.rs | 103 +++++-- 8 files changed, 249 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdde2c3d3..bf7935852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -877,6 +877,7 @@ dependencies = [ "hex", "ripemd", "secp256k1", + "serde_json", "sha2 0.10.6", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index e9d267403..f58eddbd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ cw4 = "0.16" cw4-group = "0.16" cw721 = "0.16" cw721-base = "0.16" +serde = "1.0" +serde_json = "1.0" proc-macro2 = "1.0" quote = "1.0" rand = "0.8" diff --git a/contracts/external/cw-verifier-middleware/Cargo.toml b/contracts/external/cw-verifier-middleware/Cargo.toml index ee1fc46ea..4c6f3ac09 100644 --- a/contracts/external/cw-verifier-middleware/Cargo.toml +++ b/contracts/external/cw-verifier-middleware/Cargo.toml @@ -17,6 +17,7 @@ hex = { workspace = true } bip32 = { workspace = true } ripemd = { workspace = true } bech32 = { workspace = true } +serde_json = { workspace = true} [dev-dependencies] diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index 753990dc5..5db037c5f 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -2,6 +2,7 @@ use bech32::Error as Bech32Error; use cosmwasm_std::{StdError, VerificationError}; use hex::FromHexError; use secp256k1::Error as Secp256k1Error; +use serde_json::Error as SerdeError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -21,6 +22,9 @@ pub enum ContractError { #[error("{0}")] Secp256k1Error(#[from] Secp256k1Error), + #[error("{0}")] + SerdeError(#[from] SerdeError), + #[error("Invalid nonce")] InvalidNonce, diff --git a/contracts/external/cw-verifier-middleware/src/msg.rs b/contracts/external/cw-verifier-middleware/src/msg.rs index 59b91874c..4fde0ce4b 100644 --- a/contracts/external/cw-verifier-middleware/src/msg.rs +++ b/contracts/external/cw-verifier-middleware/src/msg.rs @@ -14,8 +14,9 @@ pub struct WrappedMessage { pub struct Payload { pub nonce: Uint128, pub contract_address: String, + pub chain_id: String, pub msg: Binary, pub expiration: Option, pub bech32_prefix: String, - pub version: String, + pub contract_version: String, } diff --git a/contracts/external/cw-verifier-middleware/src/state.rs b/contracts/external/cw-verifier-middleware/src/state.rs index ad5b21ad3..b2db0705b 100644 --- a/contracts/external/cw-verifier-middleware/src/state.rs +++ b/contracts/external/cw-verifier-middleware/src/state.rs @@ -1,8 +1,8 @@ -use cosmwasm_std::Uint128; +use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::{Item, Map}; -/// Nonce for each public key -pub const NONCES: Map<&str, Uint128> = Map::new("pk_to_nonce"); +/// Nonce for each public key, contract addr, contract version thruple +pub const NONCES: Map<(&str, &Addr, &str), Uint128> = Map::new("pk_to_nonce"); /// Contract address for which this middleware is used. /// We require the contract address as part of the diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index 9d5006808..b855bffc0 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -1,91 +1,72 @@ use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, - to_binary, Addr, HexBinary, Uint128, + to_binary, Addr, BlockInfo, HexBinary, Uint128, VerificationError, }; -use secp256k1::{rand::rngs::OsRng, Message, Secp256k1}; +use cw_utils::Expiration; +use secp256k1::{hashes::hex::ToHex, rand::rngs::OsRng, Message, PublicKey, Secp256k1, SecretKey}; use sha2::{Digest, Sha256}; use crate::{ error::ContractError, msg::{Payload, WrappedMessage}, state::NONCES, - verify::{pk_to_addr, verify}, + verify::{get_sign_doc, pk_to_addr, verify}, }; pub const JUNO_ADDRESS: &str = "juno1muw4rz9ml44wc6vssqrzkys4nuc3gylrxj4flw"; -pub const COMPRESSED_PK: &str = "03f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28"; +pub const COMPRESSED_PK: &str = + "03f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28"; pub const UNCOMPRESSED_PK: &str = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79"; pub const JUNO_PREFIX: &str = "juno"; #[test] fn test_pk_to_addr_uncompressed() { - let deps = mock_dependencies(); - let generated_address = pk_to_addr( - deps.as_ref(), - UNCOMPRESSED_PK.to_string(), - JUNO_PREFIX, - ).unwrap(); + let generated_address = + pk_to_addr(deps.as_ref(), UNCOMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); } #[test] fn test_pk_to_addr_compressed() { - let deps = mock_dependencies(); - let generated_address = pk_to_addr( - deps.as_ref(), - COMPRESSED_PK.to_string(), - JUNO_PREFIX, - ).unwrap(); + let generated_address = + pk_to_addr(deps.as_ref(), COMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); } #[test] fn test_pk_to_addr_invalid_hex_length() { - let invalid_length_pk = "".to_string(); let deps = mock_dependencies(); - let err: ContractError = pk_to_addr( - deps.as_ref(), - invalid_length_pk, - JUNO_PREFIX, - ).unwrap_err(); + let err: ContractError = pk_to_addr(deps.as_ref(), invalid_length_pk, JUNO_PREFIX).unwrap_err(); assert!(matches!(err, ContractError::InvalidPublicKeyLength { .. })); } #[test] fn test_pk_to_addr_not_hex_pk() { - - let non_hex_pk = "03zzzzcd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28".to_string(); + let non_hex_pk = + "03zzzzcd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28".to_string(); let deps = mock_dependencies(); - let err: ContractError = pk_to_addr( - deps.as_ref(), - non_hex_pk, - JUNO_PREFIX, - ).unwrap_err(); + let err: ContractError = pk_to_addr(deps.as_ref(), non_hex_pk, JUNO_PREFIX).unwrap_err(); assert!(matches!(err, ContractError::FromHexError { .. })); } #[test] fn test_pk_to_addr_bech32_invalid_human_readable_part() { - let deps = mock_dependencies(); - let err: ContractError = pk_to_addr( - deps.as_ref(), - UNCOMPRESSED_PK.to_string(), - "jUnO", - ).unwrap_err(); + let err: ContractError = + pk_to_addr(deps.as_ref(), UNCOMPRESSED_PK.to_string(), "jUnO").unwrap_err(); assert!(matches!(err, ContractError::Bech32Error { .. })); } #[test] fn test_verify_success() { - // This test generates a payload in which the signature is of base64 format, and the public key is of hex format. + // This test generates a payload in which the signature is base64 encoded, and the public key is hex encoded. // The test then calls verify to validate that the signature is correctly verified. let payload = Payload { @@ -93,35 +74,30 @@ fn test_verify_success() { msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), - bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; - // Generate a keypair - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); - - // Hash and sign the payload - let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); - let msg = Message::from_slice(&msg_hash).unwrap(); - let sig = secp.sign_ecdsa(&msg, &secret_key); - - // Wrap the message - let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); - let wrapped_msg = WrappedMessage { - payload, - signature: sig.serialize_compact().into(), - public_key: hex_encoded.clone(), - }; + let wrapped_msg = get_wrapped_msg(payload); // Verify let mut deps = mock_dependencies(); let env = mock_env(); let info = mock_info("creator", &[]); - verify(deps.as_mut(), env, info, wrapped_msg).unwrap(); + verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap(); // Verify nonce was incremented correctly - let nonce = NONCES.load(&deps.storage, &hex_encoded.to_hex()).unwrap(); + let nonce = NONCES + .load( + &deps.storage, + ( + &wrapped_msg.public_key.to_hex(), + &Addr::unchecked(payload.contract_address), + &payload.contract_version, + ), + ) + .unwrap(); assert_eq!(nonce, Uint128::from(1u128)) } @@ -136,7 +112,8 @@ fn test_verify_pk_invalid() { expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "juno".to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; // Generate a keypair @@ -159,7 +136,7 @@ fn test_verify_pk_invalid() { let mut deps = mock_dependencies(); let env = mock_env(); let info = mock_info("creator", &[]); - let result = verify(deps.as_mut(), env, info, wrapped_msg); + let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); // Ensure that there was a pub key verification error assert!(matches!(result, Err(ContractError::VerificationError(_)))); @@ -167,16 +144,14 @@ fn test_verify_pk_invalid() { #[test] fn test_verify_wrong_pk() { - // This test generates a payload in which the signature is of base64 format, and the public key is of hex format. - // The test then calls verify with an correctly formatted but different public key (that doesn't correspond to the signature) to validate that the signature is not verified. - let payload = Payload { nonce: Uint128::from(0u128), msg: to_binary("test").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), - bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; // Generate a keypair @@ -203,7 +178,7 @@ fn test_verify_wrong_pk() { let mut deps = mock_dependencies(); let env = mock_env(); let info = mock_info("creator", &[]); - let result = verify(deps.as_mut(), env, info, wrapped_msg); + let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); // Ensure that there was a signature verification error assert!(matches!(result, Err(ContractError::SignatureInvalid))); @@ -211,7 +186,6 @@ fn test_verify_wrong_pk() { #[test] fn test_verify_incorrect_nonce() { - let mut deps = mock_dependencies(); let env = mock_env(); let info = mock_info("creator", &[]); @@ -223,10 +197,11 @@ fn test_verify_incorrect_nonce() { expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; let wrapped_msg = get_wrapped_msg(payload); - verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap(); + verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap(); // skip a nonce iteration let invalid_nonce_payload = Payload { @@ -235,10 +210,11 @@ fn test_verify_incorrect_nonce() { expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; let wrapped_msg = get_wrapped_msg(invalid_nonce_payload); - let err = verify(deps.as_mut(), env, info, wrapped_msg).unwrap_err(); + let err = verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap_err(); // verify the invalid nonce error assert!(matches!(err, ContractError::InvalidNonce)); @@ -257,11 +233,13 @@ fn test_verify_expired_message() { expiration: Some(cw_utils::Expiration::AtHeight(0)), contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; let wrapped_msg = get_wrapped_msg(payload); - - let err: ContractError = verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap_err(); + + let err: ContractError = + verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); assert!(matches!(err, ContractError::MessageExpired)); } @@ -282,9 +260,10 @@ fn test_verify_invalid_signature() { expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; - + // Hash and sign the payload let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); let msg = Message::from_slice(&msg_hash).unwrap(); @@ -292,14 +271,15 @@ fn test_verify_invalid_signature() { let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); - // Wrap a different message with the existing signature + // Wrap a different message with the existing signature let different_payload = Payload { nonce: Uint128::from(0u128), msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "cosmos".to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; let wrapped_msg = WrappedMessage { @@ -308,8 +288,8 @@ fn test_verify_invalid_signature() { public_key: hex_encoded.clone(), }; - - let err: ContractError = verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap_err(); + let err: ContractError = + verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); assert!(matches!(err, ContractError::SignatureInvalid { .. })); } @@ -330,9 +310,10 @@ fn test_verify_malformed_signature() { expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; - + // Hash and sign the payload let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); let msg = Message::from_slice(&msg_hash).unwrap(); @@ -349,7 +330,8 @@ fn test_verify_malformed_signature() { public_key: hex_encoded.clone(), }; - let err: ContractError = verify(deps.as_mut(), env.clone(), info.clone(), wrapped_msg).unwrap_err(); + let err: ContractError = + verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); assert!(matches!(err, ContractError::VerificationError { .. })); } @@ -365,7 +347,8 @@ fn test_verify_sets_sender_to_pk_address_representation() { expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), - version: "version-1".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), }; // Generate a keypair @@ -385,30 +368,109 @@ fn test_verify_sets_sender_to_pk_address_representation() { public_key: hex_encoded.clone(), }; - let (verified_msg, verified_info) = verify(deps.as_mut(), env.clone(), info, wrapped_msg).unwrap(); + let (verified_msg, verified_info) = + verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap(); // pk_to_addr is tested above so assumed to be working // TODO: generate a new keypair to avoid generating in every test - let respective_address = pk_to_addr(deps.as_ref(), hex_encoded.to_string(), JUNO_PREFIX).unwrap(); + let respective_address = + pk_to_addr(deps.as_ref(), hex_encoded.to_string(), JUNO_PREFIX).unwrap(); // assert that info.sender is set to the expected representation of pk assert_eq!(verified_info.sender, respective_address); assert_eq!(verified_msg, payload.msg); } -/* -Moar tests to write: -nonce overflow? -*/ +#[test] +fn test_verify_wrong_payload() { + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("test").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), + }; + + let wrong_payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("test").unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juney".to_string(), + contract_version: "version-0".to_string(), + chain_id: "juno-1".to_string(), + }; + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); -// signs a given payload and returns the wrapped message -fn get_wrapped_msg(payload: Payload) -> WrappedMessage { + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + // Wrap the message with wrong payload + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); + let wrapped_msg = WrappedMessage { + payload: wrong_payload.clone(), + signature: sig.serialize_compact().into(), + public_key: hex_encoded.clone(), + }; + + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("creator", &[]); + let err = verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap_err(); + println!("{:?}", err); +} + +// Verify that sender's address is set correctly in info. +#[test] +fn test_verify_correct_address() { + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary("test").unwrap(), + expiration: Some(Expiration::AtHeight(10)), + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), + }; + + let wrapped_msg = get_wrapped_msg(payload); + + // Verify with public key + let mut deps = mock_dependencies(); + let env = mock_env(); + + let info = mock_info("creator", &[]); + let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); + let addr = pk_to_addr(deps.as_ref(), wrapped_msg.public_key.to_hex(), JUNO_PREFIX).unwrap(); + + // Ensure that there was an error + assert_eq!(info.sender, addr); +} + +// signs a given payload and returns the wrapped message +fn get_wrapped_msg(deps: DepsMut, payload: Payload) -> WrappedMessage { // Generate a keypair let secp = Secp256k1::new(); let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + // Generate signdoc + let signer_addr = pk_to_addr( + deps.as_ref(), + public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes + &payload.bech32_prefix, + ) + .unwrap(); + + let sign_doc = get_sign_doc(signer_addr.as_str(), payload.clone(), &"juno-1").unwrap(); + // Hash and sign the payload let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); let msg = Message::from_slice(&msg_hash).unwrap(); @@ -421,4 +483,12 @@ fn get_wrapped_msg(payload: Payload) -> WrappedMessage { signature: sig.serialize_compact().into(), public_key: hex_encoded.clone(), } -} \ No newline at end of file +} + +/* +Moar tests to write: +wrong version +load a keypair corresponding to pre-known address and validate that address in info was set correctly +test integrating with another contract +wrong contract address +*/ diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index d4b7729b3..bcd68b112 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -4,22 +4,40 @@ use crate::{ state::{CONTRACT_ADDRESS, NONCES}, }; use bech32::{ToBase32, Variant}; -use cosmwasm_std::{to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, StdError, Uint128}; +use cosmwasm_schema::schemars::_serde_json::json; +use cosmwasm_std::{ + to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, StdError, StdResult, Uint128, +}; use ripemd::Ripemd160; +use secp256k1::hashes::serde_macros; use sha2::{Digest, Sha256}; const UNCOMPRESSED_HEX_PK_LEN: usize = 130; const COMPRESSED_HEX_PK_LEN: usize = 66; -pub fn verify( - deps: DepsMut, +pub fn verify<'a>( + deps: DepsMut<'a>, env: Env, - mut info: MessageInfo, + info: &'a mut MessageInfo, wrapped_msg: WrappedMessage, -) -> Result<(Binary, MessageInfo), ContractError> { - // Serialize the inner message - let msg_ser = to_binary(&wrapped_msg.payload)?; +) -> Result<(Binary, &'a mut MessageInfo), ContractError> { + let payload = wrapped_msg.payload; + + let signer_addr = pk_to_addr( + deps.as_ref(), + wrapped_msg.public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes + &payload.bech32_prefix, + )?; + + let payload_ser = + serde_json::to_string(&payload).map_err(|e| ContractError::SerdeError(e.into()))?; + + // Convert message to signDoc format + let sign_doc = get_sign_doc(&signer_addr.as_str(), &payload_ser, &payload.chain_id)?; + + // Serialize the payload + let msg_ser = to_binary(&sign_doc)?; // Hash the serialized payload using SHA-256 let msg_hash = Sha256::digest(&msg_ser); @@ -35,43 +53,42 @@ pub fn verify( return Err(ContractError::SignatureInvalid {}); } + let validated_contract_addr = deps.api.addr_validate(&payload.contract_address)?; + let pk = wrapped_msg.public_key.to_hex(); + let nonce_key = ( + pk.as_str(), + &validated_contract_addr, + payload.contract_version.as_str(), + ); + // Validate that the message has the correct nonce let nonce = NONCES - .may_load(deps.storage, &wrapped_msg.public_key.to_hex())? + .may_load(deps.storage, nonce_key)? .unwrap_or(Uint128::from(0u128)); - if wrapped_msg.payload.nonce != nonce { + if payload.nonce != nonce { return Err(ContractError::InvalidNonce {}); } // Increment nonce - NONCES.update( - deps.storage, - &wrapped_msg.public_key.to_string(), - |nonce: Option| { - nonce - .unwrap_or(Uint128::from(0u128)) - .checked_add(Uint128::from(1u128)) - .map_err(|e| StdError::from(e)) - }, - )?; + NONCES.update(deps.storage, nonce_key, |nonce: Option| { + nonce + .unwrap_or(Uint128::from(0u128)) + .checked_add(Uint128::from(1u128)) + .map_err(|e| StdError::from(e)) + })?; // Validate that the message has not expired - if let Some(expiration) = wrapped_msg.payload.expiration { + if let Some(expiration) = payload.expiration { if expiration.is_expired(&env.block) { return Err(ContractError::MessageExpired {}); } } - // Set the message sender to the address corresponding to the provided public key. - info.sender = pk_to_addr( - deps.as_ref(), - wrapped_msg.public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes - &wrapped_msg.payload.bech32_prefix, - )?; + info.sender = signer_addr; // Return info with updater sender and msg to be deserialized by caller - return Ok((wrapped_msg.payload.msg, info)); + return Ok((payload.msg, info)); } pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractError> { @@ -80,7 +97,7 @@ pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractE } // Takes an compressed or uncompressed hex-encoded EC public key and a bech32 prefix and derives the bech32 address. -pub fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result { +pub(crate) fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result { // Decode PK from hex let raw_pk = hex::decode(&hex_pk)?; @@ -110,3 +127,33 @@ pub fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result Result { + let doc = json!({ + "account_number": "0", + "chain_id": chain_id, + "fee": { + "amount": [], + "gas": "0" + }, + "memo": "", + "msgs": [ + { + "type": "cw-verifier", + "value": { + "data": message, + "signer": signer + } + } + ], + "sequence": "0" + }); + + serde_json::to_string(&doc).map_err(|e| ContractError::SerdeError(e.into())) +} From 5a535a9d7683ee5406485ad5d98e05f2f33d4e4e Mon Sep 17 00:00:00 2001 From: bluenote Date: Sun, 19 Mar 2023 23:26:09 -0700 Subject: [PATCH 12/16] add sign doc test --- .../cw-verifier-middleware/src/error.rs | 12 +- .../src/testing/tests.rs | 226 ++++++------------ .../cw-verifier-middleware/src/verify.rs | 15 +- 3 files changed, 94 insertions(+), 159 deletions(-) diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index 5db037c5f..1711d3988 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -1,4 +1,5 @@ use bech32::Error as Bech32Error; +use cosmwasm_std::OverflowError; use cosmwasm_std::{StdError, VerificationError}; use hex::FromHexError; use secp256k1::Error as Secp256k1Error; @@ -23,7 +24,10 @@ pub enum ContractError { Secp256k1Error(#[from] Secp256k1Error), #[error("{0}")] - SerdeError(#[from] SerdeError), + OverflowError(#[from] OverflowError), + + #[error("{0}")] + SerdeError(String), #[error("Invalid nonce")] InvalidNonce, @@ -37,3 +41,9 @@ pub enum ContractError { #[error("Invalid uncompressed public key hex string length; expected 130 bytes, got {length}")] InvalidPublicKeyLength { length: usize }, } + +impl From for ContractError { + fn from(error: SerdeError) -> Self { + ContractError::SerdeError(error.to_string()) + } +} diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index b855bffc0..dfd01368f 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, - to_binary, Addr, BlockInfo, HexBinary, Uint128, VerificationError, + to_binary, Addr, BlockInfo, DepsMut, HexBinary, Uint128, VerificationError, }; use cw_utils::Expiration; use secp256k1::{hashes::hex::ToHex, rand::rngs::OsRng, Message, PublicKey, Secp256k1, SecretKey}; @@ -66,10 +66,11 @@ fn test_pk_to_addr_bech32_invalid_human_readable_part() { #[test] fn test_verify_success() { - // This test generates a payload in which the signature is base64 encoded, and the public key is hex encoded. - // The test then calls verify to validate that the signature is correctly verified. + fn test_verify_success() { + // This test generates a payload in which the signature is base64 encoded, and the public key is hex encoded. + // The test then calls verify to validate that the signature is correctly verified. - let payload = Payload { + let payload = Payload { nonce: Uint128::from(0u128), msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, @@ -79,30 +80,31 @@ fn test_verify_success() { chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(payload); - - // Verify - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("creator", &[]); - verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap(); - - // Verify nonce was incremented correctly - let nonce = NONCES - .load( - &deps.storage, - ( - &wrapped_msg.public_key.to_hex(), - &Addr::unchecked(payload.contract_address), - &payload.contract_version, - ), - ) - .unwrap(); - assert_eq!(nonce, Uint128::from(1u128)) + let mut deps = mock_dependencies(); + let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload.clone()); + + // Verify + let env = mock_env(); + let mut info = mock_info("creator", &[]); + verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()).unwrap(); + + // Verify nonce was incremented correctly + let nonce = NONCES + .load( + &deps.storage, + ( + &wrapped_msg.public_key.to_hex(), + &Addr::unchecked(payload.contract_address), + &payload.contract_version, + ), + ) + .unwrap(); + assert_eq!(nonce, Uint128::from(1u128)) + } } #[test] -fn test_verify_pk_invalid() { +fn test_verify_invalid_pk() { // This test generates a payload in which the signature is of base64 format, and the public key is of hex format. // The test then calls verify with an incorrectly formatted public key to validate that there is an error in parsing the public key. @@ -116,30 +118,23 @@ fn test_verify_pk_invalid() { chain_id: "juno-1".to_string(), }; - // Generate a keypair - let secp = Secp256k1::new(); - let (secret_key, _) = secp.generate_keypair(&mut OsRng); - - // Hash and sign the payload - let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); - let msg = Message::from_slice(&msg_hash).unwrap(); - let sig = secp.sign_ecdsa(&msg, &secret_key); + // Generate wrapped message + let mut deps = mock_dependencies(); + let mut wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); - // Wrap the message but with incorrect public key - let wrapped_msg = WrappedMessage { - payload, - signature: sig.serialize_compact().into(), - public_key: Vec::from("incorrect_public_key").into(), - }; + // Set public key to invalid + wrapped_msg.public_key = Vec::from("incorrect_public_key").into(); // Verify with incorrect public key - let mut deps = mock_dependencies(); let env = mock_env(); - let info = mock_info("creator", &[]); + let mut info = mock_info("creator", &[]); let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); - // Ensure that there was a pub key verification error - assert!(matches!(result, Err(ContractError::VerificationError(_)))); + // Ensure that there was a pub key parsing error + assert!(matches!( + result, + Err(ContractError::InvalidPublicKeyLength { .. }) + )); } #[test] @@ -177,7 +172,7 @@ fn test_verify_wrong_pk() { // Verify with incorrect public key let mut deps = mock_dependencies(); let env = mock_env(); - let info = mock_info("creator", &[]); + let mut info = mock_info("creator", &[]); let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); // Ensure that there was a signature verification error @@ -188,7 +183,7 @@ fn test_verify_wrong_pk() { fn test_verify_incorrect_nonce() { let mut deps = mock_dependencies(); let env = mock_env(); - let info = mock_info("creator", &[]); + let mut info = mock_info("creator", &[]); // get a default wrapped message and verify it let payload = Payload { @@ -200,7 +195,7 @@ fn test_verify_incorrect_nonce() { contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(payload); + let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap(); // skip a nonce iteration @@ -213,7 +208,7 @@ fn test_verify_incorrect_nonce() { contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(invalid_nonce_payload); + let wrapped_msg = get_wrapped_msg(deps.as_mut(), invalid_nonce_payload); let err = verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap_err(); // verify the invalid nonce error @@ -224,7 +219,7 @@ fn test_verify_incorrect_nonce() { fn test_verify_expired_message() { let mut deps = mock_dependencies(); let env = mock_env(); - let info = mock_info("creator", &[]); + let mut info = mock_info("creator", &[]); // get an expired message let payload = Payload { @@ -236,7 +231,7 @@ fn test_verify_expired_message() { contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(payload); + let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); let err: ContractError = verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); @@ -245,10 +240,10 @@ fn test_verify_expired_message() { } #[test] -fn test_verify_invalid_signature() { +fn test_verify_wrong_payload() { let mut deps = mock_dependencies(); let env = mock_env(); - let info = mock_info("creator", &[]); + let mut info = mock_info("creator", &[]); // Generate a keypair let secp = Secp256k1::new(); @@ -298,11 +293,7 @@ fn test_verify_invalid_signature() { fn test_verify_malformed_signature() { let mut deps = mock_dependencies(); let env = mock_env(); - let info = mock_info("creator", &[]); - - // Generate a keypair - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + let mut info = mock_info("creator", &[]); let payload = Payload { nonce: Uint128::from(0u128), @@ -314,78 +305,47 @@ fn test_verify_malformed_signature() { chain_id: "juno-1".to_string(), }; - // Hash and sign the payload - let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); - let msg = Message::from_slice(&msg_hash).unwrap(); - let sig = secp.sign_ecdsa(&msg, &secret_key); - - let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); - let serialized_sig = sig.serialize_compact(); - - // join two signatures for unexpected format - let malformed_sig = [serialized_sig, serialized_sig].concat(); - let wrapped_msg = WrappedMessage { - payload, - signature: malformed_sig.into(), - public_key: hex_encoded.clone(), - }; + let mut wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); + let malformed_sig = Vec::from("malformed signature"); + wrapped_msg.signature = malformed_sig.into(); let err: ContractError = verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); assert!(matches!(err, ContractError::VerificationError { .. })); } +// Verify that sender's address is set correctly in info. #[test] -fn test_verify_sets_sender_to_pk_address_representation() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("creator", &[]); - +fn test_verify_correct_address() { let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + msg: to_binary("test").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), - bech32_prefix: JUNO_PREFIX.to_string(), + bech32_prefix: "juno".to_string(), contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; + let mut deps = mock_dependencies(); - // Generate a keypair - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); - - // Hash and sign the payload - let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); - let msg = Message::from_slice(&msg_hash).unwrap(); - let sig = secp.sign_ecdsa(&msg, &secret_key); - - // Wrap the message - let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); - let wrapped_msg = WrappedMessage { - payload: payload.clone(), - signature: sig.serialize_compact().into(), - public_key: hex_encoded.clone(), - }; + let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); - let (verified_msg, verified_info) = - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap(); + let mut env = mock_env(); + env.block.height = 1; + let mut info = mock_info("creator", &[]); + verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()).unwrap(); - // pk_to_addr is tested above so assumed to be working - // TODO: generate a new keypair to avoid generating in every test - let respective_address = - pk_to_addr(deps.as_ref(), hex_encoded.to_string(), JUNO_PREFIX).unwrap(); + let addr = pk_to_addr(deps.as_ref(), wrapped_msg.public_key.to_hex(), JUNO_PREFIX).unwrap(); - // assert that info.sender is set to the expected representation of pk - assert_eq!(verified_info.sender, respective_address); - assert_eq!(verified_msg, payload.msg); + assert_eq!(info.sender, addr); } +// Generate a validly signed message but without creating a sign doc first. #[test] -fn test_verify_wrong_payload() { +fn test_verify_no_sign_doc() { let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("test").unwrap(), + msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "juno".to_string(), @@ -393,15 +353,7 @@ fn test_verify_wrong_payload() { chain_id: "juno-1".to_string(), }; - let wrong_payload = Payload { - nonce: Uint128::from(0u128), - msg: to_binary("test").unwrap(), - expiration: None, - contract_address: Addr::unchecked("contract_address").to_string(), - bech32_prefix: "juney".to_string(), - contract_version: "version-0".to_string(), - chain_id: "juno-1".to_string(), - }; + let mut deps = mock_dependencies(); // Generate a keypair let secp = Secp256k1::new(); @@ -412,47 +364,19 @@ fn test_verify_wrong_payload() { let msg = Message::from_slice(&msg_hash).unwrap(); let sig = secp.sign_ecdsa(&msg, &secret_key); - // Wrap the message with wrong payload + // Wrap the message let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); let wrapped_msg = WrappedMessage { - payload: wrong_payload.clone(), + payload, signature: sig.serialize_compact().into(), public_key: hex_encoded.clone(), }; - let mut deps = mock_dependencies(); + // Verify should fail let env = mock_env(); - let info = mock_info("creator", &[]); - let err = verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap_err(); - println!("{:?}", err); -} - -// Verify that sender's address is set correctly in info. -#[test] -fn test_verify_correct_address() { - let payload = Payload { - nonce: Uint128::from(0u128), - msg: to_binary("test").unwrap(), - expiration: Some(Expiration::AtHeight(10)), - contract_address: Addr::unchecked("contract_address").to_string(), - bech32_prefix: "juno".to_string(), - contract_version: "version-1".to_string(), - chain_id: "juno-1".to_string(), - }; - - let wrapped_msg = get_wrapped_msg(payload); - - // Verify with public key - let mut deps = mock_dependencies(); - let env = mock_env(); - - let info = mock_info("creator", &[]); - let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); - - let addr = pk_to_addr(deps.as_ref(), wrapped_msg.public_key.to_hex(), JUNO_PREFIX).unwrap(); - - // Ensure that there was an error - assert_eq!(info.sender, addr); + let mut info = mock_info("creator", &[]); + let res = verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()); + assert!(matches!(res, Err(ContractError::SignatureInvalid { .. }))); } // signs a given payload and returns the wrapped message @@ -469,10 +393,12 @@ fn get_wrapped_msg(deps: DepsMut, payload: Payload) -> WrappedMessage { ) .unwrap(); - let sign_doc = get_sign_doc(signer_addr.as_str(), payload.clone(), &"juno-1").unwrap(); + let payload_ser = serde_json::to_string(&payload).unwrap(); + + let sign_doc = get_sign_doc(signer_addr.as_str(), &payload_ser, &"juno-1").unwrap(); // Hash and sign the payload - let msg_hash = Sha256::digest(&to_binary(&payload).unwrap()); + let msg_hash = Sha256::digest(&to_binary(&sign_doc).unwrap()); let msg = Message::from_slice(&msg_hash).unwrap(); let sig = secp.sign_ecdsa(&msg, &secret_key); diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index bcd68b112..600ebbc29 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -16,12 +16,12 @@ use sha2::{Digest, Sha256}; const UNCOMPRESSED_HEX_PK_LEN: usize = 130; const COMPRESSED_HEX_PK_LEN: usize = 66; -pub fn verify<'a>( - deps: DepsMut<'a>, +pub fn verify( + deps: DepsMut, env: Env, - info: &'a mut MessageInfo, + info: &mut MessageInfo, wrapped_msg: WrappedMessage, -) -> Result<(Binary, &'a mut MessageInfo), ContractError> { +) -> Result { let payload = wrapped_msg.payload; let signer_addr = pk_to_addr( @@ -30,8 +30,7 @@ pub fn verify<'a>( &payload.bech32_prefix, )?; - let payload_ser = - serde_json::to_string(&payload).map_err(|e| ContractError::SerdeError(e.into()))?; + let payload_ser = serde_json::to_string(&payload)?; // Convert message to signDoc format let sign_doc = get_sign_doc(&signer_addr.as_str(), &payload_ser, &payload.chain_id)?; @@ -88,7 +87,7 @@ pub fn verify<'a>( info.sender = signer_addr; // Return info with updater sender and msg to be deserialized by caller - return Ok((payload.msg, info)); + return Ok(payload.msg); } pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractError> { @@ -155,5 +154,5 @@ pub(crate) fn get_sign_doc( "sequence": "0" }); - serde_json::to_string(&doc).map_err(|e| ContractError::SerdeError(e.into())) + Ok(serde_json::to_string(&doc)?) } From de28c168c3627c55f250c38870d6eae6846f778e Mon Sep 17 00:00:00 2001 From: bluenote Date: Mon, 20 Mar 2023 12:51:36 -0700 Subject: [PATCH 13/16] clean up tests --- Cargo.lock | 25 ++ Cargo.toml | 2 + .../cw-verifier-middleware/Cargo.toml | 1 + .../cw-verifier-middleware/src/error.rs | 15 +- .../cw-verifier-middleware/src/lib.rs | 6 +- .../cw-verifier-middleware/src/testing/mod.rs | 2 +- .../src/testing/tests.rs | 257 +++++++++++------- .../cw-verifier-middleware/src/utils.rs | 40 +++ .../cw-verifier-middleware/src/verify.rs | 68 ++--- packages/dao-testing/src/helpers.rs | 2 +- test-contracts/verifier-test/Cargo.toml | 30 ++ test-contracts/verifier-test/src/contract.rs | 51 ++++ test-contracts/verifier-test/src/error.rs | 15 + test-contracts/verifier-test/src/lib.rs | 5 + test-contracts/verifier-test/src/msg.rs | 21 ++ test-contracts/verifier-test/src/tests.rs | 68 +++++ 16 files changed, 470 insertions(+), 138 deletions(-) create mode 100644 contracts/external/cw-verifier-middleware/src/utils.rs create mode 100644 test-contracts/verifier-test/Cargo.toml create mode 100644 test-contracts/verifier-test/src/contract.rs create mode 100644 test-contracts/verifier-test/src/error.rs create mode 100644 test-contracts/verifier-test/src/lib.rs create mode 100644 test-contracts/verifier-test/src/msg.rs create mode 100644 test-contracts/verifier-test/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index bf7935852..02afbe74e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip32" version = "0.4.0" @@ -3831,6 +3840,22 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +[[package]] +name = "verifier-test" +version = "0.1.0" +dependencies = [ + "bincode", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw-verifier-middleware", + "cw2 0.16.0", + "dao-testing", + "thiserror", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index f58eddbd5..c2b1b0ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ bip32 = "0.4.0" hex = "0.4.3" ripemd = "0.1.3" bech32 = "0.9.1" +bincode = "1.3" # One commit ahead of version 0.3.0. Allows initialization with an # optional owner. @@ -102,6 +103,7 @@ dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "*" } dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "*" } dao-voting-native-staked = { path = "./contracts/voting/dao-voting-native-staked", version = "*" } + # v1 dependencies. used for state migrations. cw-core-v1 = { package = "cw-core", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" } cw-proposal-single-v1 = { package = "cw-proposal-single", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" } diff --git a/contracts/external/cw-verifier-middleware/Cargo.toml b/contracts/external/cw-verifier-middleware/Cargo.toml index 4c6f3ac09..14e0185dd 100644 --- a/contracts/external/cw-verifier-middleware/Cargo.toml +++ b/contracts/external/cw-verifier-middleware/Cargo.toml @@ -19,6 +19,7 @@ ripemd = { workspace = true } bech32 = { workspace = true } serde_json = { workspace = true} + [dev-dependencies] secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index 1711d3988..9e4eb34e2 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -1,4 +1,5 @@ use bech32::Error as Bech32Error; + use cosmwasm_std::OverflowError; use cosmwasm_std::{StdError, VerificationError}; use hex::FromHexError; @@ -6,7 +7,7 @@ use secp256k1::Error as Secp256k1Error; use serde_json::Error as SerdeError; use thiserror::Error; -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), @@ -27,7 +28,7 @@ pub enum ContractError { OverflowError(#[from] OverflowError), #[error("{0}")] - SerdeError(String), + SerdeError(#[from] SerdeError), #[error("Invalid nonce")] InvalidNonce, @@ -42,8 +43,8 @@ pub enum ContractError { InvalidPublicKeyLength { length: usize }, } -impl From for ContractError { - fn from(error: SerdeError) -> Self { - ContractError::SerdeError(error.to_string()) - } -} +// impl From for ContractError { +// fn from(error: SerdeError) -> Self { +// ContractError::SerdeError(error.to_string()) +// } +// } diff --git a/contracts/external/cw-verifier-middleware/src/lib.rs b/contracts/external/cw-verifier-middleware/src/lib.rs index dbed528fc..56cb0db19 100644 --- a/contracts/external/cw-verifier-middleware/src/lib.rs +++ b/contracts/external/cw-verifier-middleware/src/lib.rs @@ -1,10 +1,10 @@ #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] -pub mod verify; pub mod error; pub mod msg; pub mod state; - +pub mod utils; +pub mod verify; #[cfg(test)] -mod testing; \ No newline at end of file +mod testing; diff --git a/contracts/external/cw-verifier-middleware/src/testing/mod.rs b/contracts/external/cw-verifier-middleware/src/testing/mod.rs index 4ddd54de5..9f1e9f269 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/mod.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/mod.rs @@ -1,2 +1,2 @@ +mod adversarial_tests; mod tests; -mod adversarial_tests; \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index dfd01368f..77e1866a9 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -1,3 +1,4 @@ +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, to_binary, Addr, BlockInfo, DepsMut, HexBinary, Uint128, VerificationError, @@ -10,6 +11,7 @@ use crate::{ error::ContractError, msg::{Payload, WrappedMessage}, state::NONCES, + utils::get_wrapped_msg, verify::{get_sign_doc, pk_to_addr, verify}, }; @@ -19,11 +21,16 @@ pub const COMPRESSED_PK: &str = pub const UNCOMPRESSED_PK: &str = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79"; pub const JUNO_PREFIX: &str = "juno"; +#[cw_serde] +pub enum TestExecuteMsg { + Test, +} + #[test] fn test_pk_to_addr_uncompressed() { let deps = mock_dependencies(); let generated_address = - pk_to_addr(deps.as_ref(), UNCOMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); + pk_to_addr(&deps.api, UNCOMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); } @@ -31,8 +38,7 @@ fn test_pk_to_addr_uncompressed() { #[test] fn test_pk_to_addr_compressed() { let deps = mock_dependencies(); - let generated_address = - pk_to_addr(deps.as_ref(), COMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); + let generated_address = pk_to_addr(&deps.api, COMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); } @@ -40,7 +46,7 @@ fn test_pk_to_addr_compressed() { fn test_pk_to_addr_invalid_hex_length() { let invalid_length_pk = "".to_string(); let deps = mock_dependencies(); - let err: ContractError = pk_to_addr(deps.as_ref(), invalid_length_pk, JUNO_PREFIX).unwrap_err(); + let err: ContractError = pk_to_addr(&deps.api, invalid_length_pk, JUNO_PREFIX).unwrap_err(); assert!(matches!(err, ContractError::InvalidPublicKeyLength { .. })); } @@ -50,7 +56,7 @@ fn test_pk_to_addr_not_hex_pk() { let non_hex_pk = "03zzzzcd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28".to_string(); let deps = mock_dependencies(); - let err: ContractError = pk_to_addr(deps.as_ref(), non_hex_pk, JUNO_PREFIX).unwrap_err(); + let err: ContractError = pk_to_addr(&deps.api, non_hex_pk, JUNO_PREFIX).unwrap_err(); assert!(matches!(err, ContractError::FromHexError { .. })); } @@ -59,18 +65,59 @@ fn test_pk_to_addr_not_hex_pk() { fn test_pk_to_addr_bech32_invalid_human_readable_part() { let deps = mock_dependencies(); let err: ContractError = - pk_to_addr(deps.as_ref(), UNCOMPRESSED_PK.to_string(), "jUnO").unwrap_err(); + pk_to_addr(&deps.api, UNCOMPRESSED_PK.to_string(), "jUnO").unwrap_err(); assert!(matches!(err, ContractError::Bech32Error { .. })); } #[test] fn test_verify_success() { - fn test_verify_success() { - // This test generates a payload in which the signature is base64 encoded, and the public key is hex encoded. - // The test then calls verify to validate that the signature is correctly verified. + // This test generates a payload in which the signature is base64 encoded, and the public key is hex encoded. + // The test then calls verify to validate that the signature is correctly verified. + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), + }; + + let mut deps = mock_dependencies(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload.clone()); + + // Verify + let env = mock_env(); + let info = mock_info("creator", &[]); + verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap(); + + // Verify nonce was incremented correctly + let nonce = NONCES + .load( + &deps.storage, + ( + &wrapped_msg.public_key.to_hex(), + &Addr::unchecked(payload.contract_address), + &payload.contract_version, + ), + ) + .unwrap(); + assert_eq!(nonce, Uint128::from(1u128)) +} - let payload = Payload { +// The type that verify deserializes to does not match the serialized message type. +#[test] +fn test_verify_wrong_message_type() { + let payload = Payload { nonce: Uint128::from(0u128), msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, @@ -80,27 +127,21 @@ fn test_verify_success() { chain_id: "juno-1".to_string(), }; - let mut deps = mock_dependencies(); - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload.clone()); - - // Verify - let env = mock_env(); - let mut info = mock_info("creator", &[]); - verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()).unwrap(); - - // Verify nonce was incremented correctly - let nonce = NONCES - .load( - &deps.storage, - ( - &wrapped_msg.public_key.to_hex(), - &Addr::unchecked(payload.contract_address), - &payload.contract_version, - ), - ) - .unwrap(); - assert_eq!(nonce, Uint128::from(1u128)) - } + let mut deps = mock_dependencies(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload.clone()); + + // Verify + let env = mock_env(); + let info = mock_info("creator", &[]); + let res = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); + + assert!(matches!(res, Err(ContractError::Std(_)))); } #[test] @@ -120,15 +161,21 @@ fn test_verify_invalid_pk() { // Generate wrapped message let mut deps = mock_dependencies(); - let mut wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); + let mut wrapped_msg = get_wrapped_msg(&deps.api, payload); // Set public key to invalid wrapped_msg.public_key = Vec::from("incorrect_public_key").into(); // Verify with incorrect public key let env = mock_env(); - let mut info = mock_info("creator", &[]); - let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); + let info = mock_info("creator", &[]); + let result = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); // Ensure that there was a pub key parsing error assert!(matches!( @@ -141,7 +188,7 @@ fn test_verify_invalid_pk() { fn test_verify_wrong_pk() { let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("test").unwrap(), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "juno".to_string(), @@ -172,8 +219,14 @@ fn test_verify_wrong_pk() { // Verify with incorrect public key let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); - let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); + let info = mock_info("creator", &[]); + let result = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); // Ensure that there was a signature verification error assert!(matches!(result, Err(ContractError::SignatureInvalid))); @@ -183,20 +236,27 @@ fn test_verify_wrong_pk() { fn test_verify_incorrect_nonce() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); // get a default wrapped message and verify it let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload); + verify::( + &deps.api, + &mut deps.storage, + &env, + info.clone(), + wrapped_msg.clone(), + ) + .unwrap(); // skip a nonce iteration let invalid_nonce_payload = Payload { @@ -208,8 +268,15 @@ fn test_verify_incorrect_nonce() { contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(deps.as_mut(), invalid_nonce_payload); - let err = verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap_err(); + let wrapped_msg = get_wrapped_msg(&deps.api, invalid_nonce_payload); + let err = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); // verify the invalid nonce error assert!(matches!(err, ContractError::InvalidNonce)); @@ -219,7 +286,7 @@ fn test_verify_incorrect_nonce() { fn test_verify_expired_message() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); // get an expired message let payload = Payload { @@ -231,10 +298,16 @@ fn test_verify_expired_message() { contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); - - let err: ContractError = - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload); + + let err: ContractError = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); assert!(matches!(err, ContractError::MessageExpired)); } @@ -243,7 +316,7 @@ fn test_verify_expired_message() { fn test_verify_wrong_payload() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); // Generate a keypair let secp = Secp256k1::new(); @@ -283,8 +356,14 @@ fn test_verify_wrong_payload() { public_key: hex_encoded.clone(), }; - let err: ContractError = - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); + let err: ContractError = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); assert!(matches!(err, ContractError::SignatureInvalid { .. })); } @@ -293,7 +372,7 @@ fn test_verify_wrong_payload() { fn test_verify_malformed_signature() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); let payload = Payload { nonce: Uint128::from(0u128), @@ -305,12 +384,18 @@ fn test_verify_malformed_signature() { chain_id: "juno-1".to_string(), }; - let mut wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); + let mut wrapped_msg = get_wrapped_msg(&deps.api, payload); let malformed_sig = Vec::from("malformed signature"); wrapped_msg.signature = malformed_sig.into(); - let err: ContractError = - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); + let err: ContractError = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); assert!(matches!(err, ContractError::VerificationError { .. })); } @@ -319,7 +404,7 @@ fn test_verify_malformed_signature() { fn test_verify_correct_address() { let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("test").unwrap(), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "juno".to_string(), @@ -328,16 +413,23 @@ fn test_verify_correct_address() { }; let mut deps = mock_dependencies(); - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); + let wrapped_msg = get_wrapped_msg(&deps.api, payload); let mut env = mock_env(); env.block.height = 1; - let mut info = mock_info("creator", &[]); - verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()).unwrap(); + let info = mock_info("creator", &[]); + let (_, verified_info) = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap(); - let addr = pk_to_addr(deps.as_ref(), wrapped_msg.public_key.to_hex(), JUNO_PREFIX).unwrap(); + let addr = pk_to_addr(&deps.api, wrapped_msg.public_key.to_hex(), JUNO_PREFIX).unwrap(); - assert_eq!(info.sender, addr); + assert_eq!(verified_info.sender, addr); } // Generate a validly signed message but without creating a sign doc first. @@ -374,47 +466,22 @@ fn test_verify_no_sign_doc() { // Verify should fail let env = mock_env(); - let mut info = mock_info("creator", &[]); - let res = verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()); + let info = mock_info("creator", &[]); + let res = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); assert!(matches!(res, Err(ContractError::SignatureInvalid { .. }))); } -// signs a given payload and returns the wrapped message -fn get_wrapped_msg(deps: DepsMut, payload: Payload) -> WrappedMessage { - // Generate a keypair - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); - - // Generate signdoc - let signer_addr = pk_to_addr( - deps.as_ref(), - public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes - &payload.bech32_prefix, - ) - .unwrap(); - - let payload_ser = serde_json::to_string(&payload).unwrap(); - - let sign_doc = get_sign_doc(signer_addr.as_str(), &payload_ser, &"juno-1").unwrap(); - - // Hash and sign the payload - let msg_hash = Sha256::digest(&to_binary(&sign_doc).unwrap()); - let msg = Message::from_slice(&msg_hash).unwrap(); - let sig = secp.sign_ecdsa(&msg, &secret_key); - - // Wrap the message - let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); - WrappedMessage { - payload, - signature: sig.serialize_compact().into(), - public_key: hex_encoded.clone(), - } -} - /* Moar tests to write: wrong version load a keypair corresponding to pre-known address and validate that address in info was set correctly test integrating with another contract wrong contract address +deserialize message into wrong type */ diff --git a/contracts/external/cw-verifier-middleware/src/utils.rs b/contracts/external/cw-verifier-middleware/src/utils.rs new file mode 100644 index 000000000..828f21973 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/utils.rs @@ -0,0 +1,40 @@ +use cosmwasm_std::{to_binary, Api, HexBinary}; +use secp256k1::{hashes::hex::ToHex, rand::rngs::OsRng, Message, Secp256k1}; +use sha2::{Digest, Sha256}; + +use crate::{ + msg::{Payload, WrappedMessage}, + verify::{get_sign_doc, pk_to_addr}, +}; + +// signs a given payload and returns the wrapped message +pub fn get_wrapped_msg(api: &dyn Api, payload: Payload) -> WrappedMessage { + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + + // Generate signdoc + let signer_addr = pk_to_addr( + api, + public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes + &payload.bech32_prefix, + ) + .unwrap(); + + let payload_ser = serde_json::to_string(&payload).unwrap(); + + let sign_doc = get_sign_doc(signer_addr.as_str(), &payload_ser, &"juno-1").unwrap(); + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&sign_doc).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + // Wrap the message + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); + WrappedMessage { + payload, + signature: sig.serialize_compact().into(), + public_key: hex_encoded.clone(), + } +} diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 600ebbc29..16d5c3c2b 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -4,28 +4,31 @@ use crate::{ state::{CONTRACT_ADDRESS, NONCES}, }; use bech32::{ToBase32, Variant}; -use cosmwasm_schema::schemars::_serde_json::json; +use cosmwasm_schema::{schemars::_serde_json::json, serde::de::DeserializeOwned}; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, StdError, StdResult, Uint128, + from_slice, to_binary, Addr, Api, DepsMut, Env, MessageInfo, StdError, Storage, Uint128, }; use ripemd::Ripemd160; -use secp256k1::hashes::serde_macros; use sha2::{Digest, Sha256}; const UNCOMPRESSED_HEX_PK_LEN: usize = 130; const COMPRESSED_HEX_PK_LEN: usize = 66; -pub fn verify( - deps: DepsMut, - env: Env, - info: &mut MessageInfo, +pub fn verify( + api: &dyn Api, + storage: &mut dyn Storage, + env: &Env, + info: MessageInfo, wrapped_msg: WrappedMessage, -) -> Result { +) -> Result<(T, MessageInfo), ContractError> +where + T: DeserializeOwned, +{ let payload = wrapped_msg.payload; let signer_addr = pk_to_addr( - deps.as_ref(), + api, wrapped_msg.public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes &payload.bech32_prefix, )?; @@ -42,7 +45,7 @@ pub fn verify( let msg_hash = Sha256::digest(&msg_ser); // Verify the signature - let sig_valid = deps.api.secp256k1_verify( + let sig_valid = api.secp256k1_verify( msg_hash.as_slice(), &wrapped_msg.signature, wrapped_msg.public_key.as_slice(), @@ -52,7 +55,14 @@ pub fn verify( return Err(ContractError::SignatureInvalid {}); } - let validated_contract_addr = deps.api.addr_validate(&payload.contract_address)?; + // Validate that the message has not expired + if let Some(expiration) = payload.expiration { + if expiration.is_expired(&env.block) { + return Err(ContractError::MessageExpired {}); + } + } + + let validated_contract_addr = api.addr_validate(&payload.contract_address)?; let pk = wrapped_msg.public_key.to_hex(); let nonce_key = ( pk.as_str(), @@ -62,7 +72,7 @@ pub fn verify( // Validate that the message has the correct nonce let nonce = NONCES - .may_load(deps.storage, nonce_key)? + .may_load(storage, nonce_key)? .unwrap_or(Uint128::from(0u128)); if payload.nonce != nonce { @@ -70,33 +80,33 @@ pub fn verify( } // Increment nonce - NONCES.update(deps.storage, nonce_key, |nonce: Option| { + NONCES.update(storage, nonce_key, |nonce: Option| { nonce .unwrap_or(Uint128::from(0u128)) .checked_add(Uint128::from(1u128)) .map_err(|e| StdError::from(e)) })?; - // Validate that the message has not expired - if let Some(expiration) = payload.expiration { - if expiration.is_expired(&env.block) { - return Err(ContractError::MessageExpired {}); - } - } - // Set the message sender to the address corresponding to the provided public key. - info.sender = signer_addr; + // Construct a new MessageInfo with the signer as the sender + let verified_info = MessageInfo { + sender: signer_addr, + funds: info.funds, + }; + + // Deserialize message into expected type + let verified_msg = from_slice::(&payload.msg.to_vec())?; - // Return info with updater sender and msg to be deserialized by caller - return Ok(payload.msg); + // Return info with sender and deserialized msg + return Ok((verified_msg, verified_info)); } -pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractError> { +pub fn initialize_contract_addr(deps: DepsMut, env: &Env) -> Result<(), ContractError> { CONTRACT_ADDRESS.save(deps.storage, &env.contract.address.to_string())?; Ok(()) } // Takes an compressed or uncompressed hex-encoded EC public key and a bech32 prefix and derives the bech32 address. -pub(crate) fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result { +pub fn pk_to_addr(api: &dyn Api, hex_pk: String, prefix: &str) -> Result { // Decode PK from hex let raw_pk = hex::decode(&hex_pk)?; @@ -124,16 +134,12 @@ pub(crate) fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result Result { +pub fn get_sign_doc(signer: &str, message: &str, chain_id: &str) -> Result { let doc = json!({ "account_number": "0", "chain_id": chain_id, diff --git a/packages/dao-testing/src/helpers.rs b/packages/dao-testing/src/helpers.rs index 8984588a4..d773270e1 100644 --- a/packages/dao-testing/src/helpers.rs +++ b/packages/dao-testing/src/helpers.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_binary, Addr, Binary, Empty, Uint128}; +use cosmwasm_std::{to_binary, Addr, Api, Binary, DepsMut, Empty, HexBinary, Uint128}; use cw20::Cw20Coin; use cw_multi_test::{App, Contract, ContractWrapper, Executor}; use cw_utils::Duration; diff --git a/test-contracts/verifier-test/Cargo.toml b/test-contracts/verifier-test/Cargo.toml new file mode 100644 index 000000000..bf9ae3285 --- /dev/null +++ b/test-contracts/verifier-test/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "verifier-test" +version = "0.1.0" +authors = ["bluenote"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-storage = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +cw-verifier-middleware = {workspace = true} +bincode = "1.3" + +[dev-dependencies] +cw-multi-test = { workspace = true } +dao-testing = { workspace = true} diff --git a/test-contracts/verifier-test/src/contract.rs b/test-contracts/verifier-test/src/contract.rs new file mode 100644 index 000000000..6ed7fb14c --- /dev/null +++ b/test-contracts/verifier-test/src/contract.rs @@ -0,0 +1,51 @@ +use crate::{ + error::ContractError, + msg::{ExecuteMsg, InnerExecuteMsg, InstantiateMsg, QueryMsg}, +}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw2::set_contract_version; +use cw_verifier_middleware::verify::verify; + +const CONTRACT_NAME: &str = "crates.io:cw-verifier-test"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::new().add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let (verified_msg, verified_info) = + verify::(deps.api, deps.storage, &env, info, msg.wrapped_msg)?; + match verified_msg { + InnerExecuteMsg::Execute => execute_execute(deps, env, verified_info)?, + }; + Ok(Response::default()) +} + +pub fn execute_execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::default().add_attribute("action", "execute_execute")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { + Ok(Binary::default()) +} diff --git a/test-contracts/verifier-test/src/error.rs b/test-contracts/verifier-test/src/error.rs new file mode 100644 index 000000000..74726833f --- /dev/null +++ b/test-contracts/verifier-test/src/error.rs @@ -0,0 +1,15 @@ +use cosmwasm_std::StdError; +use cw_verifier_middleware::error::ContractError as VerifyError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + VerifyError(#[from] VerifyError), + + #[error("Unauthorized")] + Unauthorized {}, +} diff --git a/test-contracts/verifier-test/src/lib.rs b/test-contracts/verifier-test/src/lib.rs new file mode 100644 index 000000000..2ed82bd3f --- /dev/null +++ b/test-contracts/verifier-test/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod msg; +#[cfg(test)] +mod tests; diff --git a/test-contracts/verifier-test/src/msg.rs b/test-contracts/verifier-test/src/msg.rs new file mode 100644 index 000000000..ae55a9f7f --- /dev/null +++ b/test-contracts/verifier-test/src/msg.rs @@ -0,0 +1,21 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::CosmosMsg; +use cw_verifier_middleware::msg::WrappedMessage; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum InnerExecuteMsg { + Execute, +} + +#[cw_serde] + +pub struct ExecuteMsg { + pub wrapped_msg: WrappedMessage, +} + +#[cw_serde] + +pub struct QueryMsg {} diff --git a/test-contracts/verifier-test/src/tests.rs b/test-contracts/verifier-test/src/tests.rs new file mode 100644 index 000000000..6d044696a --- /dev/null +++ b/test-contracts/verifier-test/src/tests.rs @@ -0,0 +1,68 @@ +use cosmwasm_std::testing::{MockApi, MockStorage}; +use cosmwasm_std::{to_binary, Addr, Api, Empty, Storage, Uint128}; +use cw_multi_test::{AppBuilder, Executor, Router}; +use cw_multi_test::{Contract, ContractWrapper}; +use cw_verifier_middleware::msg::Payload; +use cw_verifier_middleware::utils::get_wrapped_msg; + +use crate::msg::{ExecuteMsg, InnerExecuteMsg}; + +fn test_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) +} + +fn no_init( + _: &mut Router, + _: &dyn Api, + _: &mut dyn Storage, +) { +} + +#[test] +fn test_verify() { + let api = MockApi::default(); + let storage = MockStorage::new(); + + let mut app = AppBuilder::new() + .with_api(api) + .with_storage(storage) + .build(no_init); + + let code_id = app.store_code(test_contract()); + let contract = app + .instantiate_contract( + code_id, + Addr::unchecked("admin"), + &crate::msg::InstantiateMsg {}, + &[], + "test contract", + None, + ) + .unwrap(); + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary(&InnerExecuteMsg::Execute {}).unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), + }; + + let wrapped_msg = get_wrapped_msg(&api, payload); + app.execute_contract( + Addr::unchecked("ADMIN"), + contract, + &ExecuteMsg { + wrapped_msg: wrapped_msg, + }, + &[], + ) + .unwrap(); +} From 57b26794eea73876316f34b8a770e9b78e6495aa Mon Sep 17 00:00:00 2001 From: bluenote Date: Mon, 20 Mar 2023 12:57:05 -0700 Subject: [PATCH 14/16] rebase --- Cargo.lock | 2 +- Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02afbe74e..9c8b36539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3849,7 +3849,7 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 0.16.0", + "cw-storage-plus 1.0.1 (git+https://github.com/DA0-DA0/cw-storage-plus.git)", "cw-verifier-middleware", "cw2 0.16.0", "dao-testing", diff --git a/Cargo.toml b/Cargo.toml index c2b1b0ad8..b7eef09ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ cw4 = "0.16" cw4-group = "0.16" cw721 = "0.16" cw721-base = "0.16" -serde = "1.0" serde_json = "1.0" proc-macro2 = "1.0" quote = "1.0" From 41c827ff54ab9022fe8936fdbb6c438e609ce6b4 Mon Sep 17 00:00:00 2001 From: bluenote Date: Mon, 20 Mar 2023 13:49:32 -0700 Subject: [PATCH 15/16] remove from trait --- contracts/external/cw-verifier-middleware/src/error.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index 9e4eb34e2..1a4d1cc1c 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -42,9 +42,3 @@ pub enum ContractError { #[error("Invalid uncompressed public key hex string length; expected 130 bytes, got {length}")] InvalidPublicKeyLength { length: usize }, } - -// impl From for ContractError { -// fn from(error: SerdeError) -> Self { -// ContractError::SerdeError(error.to_string()) -// } -// } From 0f90f0199592e5fa7c5a2de9cc9ced57aebe0772 Mon Sep 17 00:00:00 2001 From: bluenote Date: Fri, 31 Mar 2023 09:07:39 -0700 Subject: [PATCH 16/16] wip add macro --- Cargo.lock | 7 +++++++ Cargo.toml | 3 ++- .../cw-verifier-middleware/Cargo.toml | 6 ++---- .../cw-verifier-middleware/derive/Cargo.toml | 19 +++++++++++++++++ .../cw-verifier-middleware/derive/lib.rs | 21 +++++++++++++++++++ .../cw-verifier-middleware/src/utils.rs | 1 - .../cw-verifier-middleware/src/verify.rs | 14 +++++++++++++ 7 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 contracts/external/cw-verifier-middleware/derive/Cargo.toml create mode 100644 contracts/external/cw-verifier-middleware/derive/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9c8b36539..388b9092d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1809,6 +1809,13 @@ dependencies = [ "syn", ] +[[package]] +name = "derive" +version = "0.1.0" +dependencies = [ + "dao-macros", +] + [[package]] name = "digest" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index b7eef09ae..3df1d31b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,8 @@ members = [ "packages/*", "test-contracts/*", "ci/*", - "contracts/external/*" + "contracts/external/*", + "contracts/external/cw-verifier-middleware/derive" ] exclude = ["ci/configs/"] diff --git a/contracts/external/cw-verifier-middleware/Cargo.toml b/contracts/external/cw-verifier-middleware/Cargo.toml index 14e0185dd..a28873063 100644 --- a/contracts/external/cw-verifier-middleware/Cargo.toml +++ b/contracts/external/cw-verifier-middleware/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] cosmwasm-std = { workspace = true} thiserror = { workspace = true } -secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } sha2 = { workspace = true } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } @@ -18,8 +17,7 @@ bip32 = { workspace = true } ripemd = { workspace = true } bech32 = { workspace = true } serde_json = { workspace = true} - +secp256k1 = { workspace = true } [dev-dependencies] - -secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } \ No newline at end of file +secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } diff --git a/contracts/external/cw-verifier-middleware/derive/Cargo.toml b/contracts/external/cw-verifier-middleware/derive/Cargo.toml new file mode 100644 index 000000000..910b9b791 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "derive" +version = "0.1.0" +edition = "2021" + +[dependencies] +dao-macros = { workspace = true } +cosmwasm-std = { workspace = true} +serde = { workspace = true, features = ["derive"]} +serde_derive = { workspace = true } +serde_json = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[lib] +name = "derive" +path = "lib.rs" +proc-macro = true diff --git a/contracts/external/cw-verifier-middleware/derive/lib.rs b/contracts/external/cw-verifier-middleware/derive/lib.rs new file mode 100644 index 000000000..da4110aaa --- /dev/null +++ b/contracts/external/cw-verifier-middleware/derive/lib.rs @@ -0,0 +1,21 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; +use proc_macro::TokenStream; +use quote::quote; +use serde::de::DeserializeOwned; +use std::error::Error; + +#[proc_macro_attribute] +pub fn cw_verifier_execute(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote! { + enum Right { + VerifyAndExecuteSignedMessage { + msg: ::cw_verifier_middleware::WrappedMessage + }, + } + } + .into(), + ) +} diff --git a/contracts/external/cw-verifier-middleware/src/utils.rs b/contracts/external/cw-verifier-middleware/src/utils.rs index 828f21973..8f1655959 100644 --- a/contracts/external/cw-verifier-middleware/src/utils.rs +++ b/contracts/external/cw-verifier-middleware/src/utils.rs @@ -22,7 +22,6 @@ pub fn get_wrapped_msg(api: &dyn Api, payload: Payload) -> WrappedMessage { .unwrap(); let payload_ser = serde_json::to_string(&payload).unwrap(); - let sign_doc = get_sign_doc(signer_addr.as_str(), &payload_ser, &"juno-1").unwrap(); // Hash and sign the payload diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 16d5c3c2b..4acb9ea2a 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -162,3 +162,17 @@ pub fn get_sign_doc(signer: &str, message: &str, chain_id: &str) -> Result, +>( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: WrappedMessage, + execute: fn(DepsMut, Env, MessageInfo, T) -> Result, +) -> Result { + let (msg, info): (T, _) = verify(deps.api, deps.storage, env, info, msg)?; + execute(deps, env, info, msg) +}