diff --git a/Cargo.lock b/Cargo.lock index c6dadb2ca..6c65b5b92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4825,6 +4825,7 @@ dependencies = [ "serde", "serde_json", "sha3 0.10.8", + "signature-verifier-api", "thiserror", ] @@ -7248,6 +7249,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature-verifier-api" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "error-stack", + "thiserror", +] + [[package]] name = "similar" version = "2.2.1" diff --git a/Cargo.toml b/Cargo.toml index 24dd56917..c4a6eba8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ serde = { version = "1.0.145", default-features = false, features = ["derive"] } serde_json = "1.0.89" schemars = "0.8.10" sha3 = { version = "0.10.8", default-features = false, features = [] } +signature-verifier-api = { version = "^0.1.0", path = "packages/signature-verifier-api" } [profile.release] opt-level = 3 diff --git a/contracts/multisig/Cargo.toml b/contracts/multisig/Cargo.toml index 7f4e376e2..a5883ee30 100644 --- a/contracts/multisig/Cargo.toml +++ b/contracts/multisig/Cargo.toml @@ -56,6 +56,7 @@ schemars = "0.8.10" serde = { version = "1.0.145", default-features = false, features = ["derive"] } serde_json = "1.0.89" sha3 = { workspace = true } +signature-verifier-api = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/contracts/multisig/src/contract.rs b/contracts/multisig/src/contract.rs index 4cd0608ec..261274fb7 100644 --- a/contracts/multisig/src/contract.rs +++ b/contracts/multisig/src/contract.rs @@ -53,9 +53,9 @@ pub fn execute( } => { execute::require_authorized_caller(&deps, info.sender)?; - let _sig_verifier = sig_verifier + let sig_verifier = sig_verifier .map(|addr| deps.api.addr_validate(&addr)) - .transpose()?; // TODO: handle callback + .transpose()?; execute::start_signing_session( deps, env, @@ -63,6 +63,7 @@ pub fn execute( msg.try_into() .map_err(axelar_wasm_std::ContractError::from)?, chain_name, + sig_verifier, ) } ExecuteMsg::SubmitSignature { diff --git a/contracts/multisig/src/contract/execute.rs b/contracts/multisig/src/contract/execute.rs index 1dbc0cee2..13e1233dc 100644 --- a/contracts/multisig/src/contract/execute.rs +++ b/contracts/multisig/src/contract/execute.rs @@ -1,6 +1,7 @@ use connection_router::state::ChainName; use cosmwasm_std::WasmMsg; use sha3::{Digest, Keccak256}; +use signature_verifier_api::client::SignatureVerifier; use crate::signing::validate_session_signature; use crate::state::{load_session_signatures, save_pub_key, save_signature}; @@ -20,6 +21,7 @@ pub fn start_signing_session( worker_set_id: String, msg: MsgToSign, chain_name: ChainName, + sig_verifier: Option, ) -> Result { let config = CONFIG.load(deps.storage)?; let worker_set = get_worker_set(deps.storage, &worker_set_id)?; @@ -40,6 +42,7 @@ pub fn start_signing_session( chain_name.clone(), msg.clone(), expires_at, + sig_verifier, ); SIGNING_SESSIONS.save(deps.storage, session_id.into(), &signing_session)?; @@ -81,12 +84,21 @@ pub fn submit_signature( let signature: Signature = (pub_key.key_type(), signature).try_into()?; + let sig_verifier = session + .sig_verifier + .clone() + .map(|address| SignatureVerifier { + address, + querier: deps.querier, + }); + validate_session_signature( &session, &info.sender, &signature, pub_key, env.block.height, + sig_verifier, )?; let signature = save_signature(deps.storage, session_id, signature, &info.sender)?; diff --git a/contracts/multisig/src/msg.rs b/contracts/multisig/src/msg.rs index 149b6de07..93dffec6c 100644 --- a/contracts/multisig/src/msg.rs +++ b/contracts/multisig/src/msg.rs @@ -23,11 +23,12 @@ pub enum ExecuteMsg { worker_set_id: String, msg: HexBinary, chain_name: ChainName, - /* Address of a contract responsible for signature verification. - The multisig contract verifies each submitted signature by default. - But some chains need custom verification beyond this, so the verification can be optionally overridden. - If a callback address is provided, signature verification is handled by the contract at that address - instead of the multisig contract. TODO: define interface for callback */ + /// Address of a contract responsible for signature verification. + /// The multisig contract verifies each submitted signature by default. + /// But some chains need custom verification beyond this, so the verification can be optionally overridden. + /// If a callback address is provided, signature verification is handled by the contract at that address + /// instead of the multisig contract. Signature verifier contracts must implement interface defined in + /// [signature_verifier_api::msg] sig_verifier: Option, }, SubmitSignature { diff --git a/contracts/multisig/src/signing.rs b/contracts/multisig/src/signing.rs index 65b83d608..3b8018de7 100644 --- a/contracts/multisig/src/signing.rs +++ b/contracts/multisig/src/signing.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use connection_router::state::ChainName; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint256, Uint64}; +use signature_verifier_api::client::SignatureVerifier; use crate::{ key::{PublicKey, Signature}, @@ -19,6 +20,7 @@ pub struct SigningSession { pub msg: MsgToSign, pub state: MultisigState, pub expires_at: u64, + pub sig_verifier: Option, } impl SigningSession { @@ -28,6 +30,7 @@ impl SigningSession { chain_name: ChainName, msg: MsgToSign, expires_at: u64, + sig_verifier: Option, ) -> Self { Self { id: session_id, @@ -36,6 +39,7 @@ impl SigningSession { msg, state: MultisigState::Pending, expires_at, + sig_verifier, } } @@ -61,6 +65,7 @@ pub fn validate_session_signature( signature: &Signature, pub_key: &PublicKey, block_height: u64, + sig_verifier: Option, ) -> Result<(), ContractError> { if session.expires_at < block_height { return Err(ContractError::SigningSessionClosed { @@ -68,7 +73,24 @@ pub fn validate_session_signature( }); } - if !signature.verify(&session.msg, pub_key)? { + let valid = sig_verifier.map_or_else( + || signature.verify(&session.msg, pub_key), + |verifier| { + verifier + .verify_signature( + signature.as_ref().into(), + session.msg.as_ref().into(), + pub_key.as_ref().into(), + signer.to_string(), + session.id, + ) + .map_err(|err| ContractError::SignatureVerificationFailed { + reason: err.to_string(), + }) + }, + )?; + + if !valid { return Err(ContractError::InvalidSignature { session_id: session.id, signer: signer.into(), @@ -93,7 +115,10 @@ fn signers_weight(signatures: &HashMap, worker_set: &WorkerSe #[cfg(test)] mod tests { - use cosmwasm_std::{testing::MockStorage, Addr, HexBinary}; + use cosmwasm_std::{ + testing::{MockQuerier, MockStorage}, + to_binary, Addr, HexBinary, QuerierWrapper, + }; use crate::{ key::KeyType, @@ -128,6 +153,7 @@ mod tests { "mock-chain".parse().unwrap(), message.clone(), expires_at, + None, ); let signatures: HashMap = signers @@ -166,6 +192,7 @@ mod tests { "mock-chain".parse().unwrap(), message.clone(), expires_at, + None, ); let signatures: HashMap = signers @@ -217,7 +244,50 @@ mod tests { let signature = config.signatures.values().next().unwrap(); let pub_key = &worker_set.signers.get(&signer.to_string()).unwrap().pub_key; - assert!(validate_session_signature(&session, &signer, signature, pub_key, 0).is_ok()); + assert!( + validate_session_signature(&session, &signer, signature, pub_key, 0, None).is_ok() + ); + } + } + + #[test] + fn validation_through_signature_verifier_contract() { + for config in [ecdsa_setup(), ed25519_setup()] { + let session = config.session; + let worker_set = config.worker_set; + let signer = Addr::unchecked(config.signatures.keys().next().unwrap()); + let signature = config.signatures.values().next().unwrap(); + let pub_key = &worker_set.signers.get(&signer.to_string()).unwrap().pub_key; + + for verification in [true, false] { + let mut querier = MockQuerier::default(); + querier.update_wasm(move |_| Ok(to_binary(&verification).into()).into()); + let sig_verifier = Some(SignatureVerifier { + address: Addr::unchecked("verifier".to_string()), + querier: QuerierWrapper::new(&querier), + }); + + let result = validate_session_signature( + &session, + &signer, + signature, + pub_key, + 0, + sig_verifier, + ); + + if verification { + assert!(result.is_ok()); + } else { + assert_eq!( + result.unwrap_err(), + ContractError::InvalidSignature { + session_id: session.id, + signer: signer.clone().into(), + } + ); + } + } } } @@ -236,7 +306,8 @@ mod tests { &signer, signature, pub_key, - block_height + block_height, + None ) .is_ok()); } @@ -252,8 +323,14 @@ mod tests { let block_height = 12346; let pub_key = &worker_set.signers.get(&signer.to_string()).unwrap().pub_key; - let result = - validate_session_signature(&session, &signer, signature, pub_key, block_height); + let result = validate_session_signature( + &session, + &signer, + signature, + pub_key, + block_height, + None, + ); assert_eq!( result.unwrap_err(), @@ -281,7 +358,8 @@ mod tests { .try_into() .unwrap(); - let result = validate_session_signature(&session, &signer, &invalid_sig, pub_key, 0); + let result = + validate_session_signature(&session, &signer, &invalid_sig, pub_key, 0, None); assert_eq!( result.unwrap_err(), diff --git a/doc/src/contracts/multisig.md b/doc/src/contracts/multisig.md index 6fe40ec76..0c29c776e 100644 --- a/doc/src/contracts/multisig.md +++ b/doc/src/contracts/multisig.md @@ -50,12 +50,48 @@ Prover-->>-Relayer: returns data and proof ``` +## Custom signature verification + +If the multisig contract doesn't natively support the required signature verification, the `sig_verifier` parameter in `ExecuteMsg::StartSigningSession` can be set by the prover to specify a custom signature verification contract. The custom contract must implement the following interface defined in `packges/signature-verifier-api`: + +```Rust +pub enum QueryMsg { + #[returns(bool)] + VerifySignature { + signature: HexBinary, + message: HexBinary, + public_key: HexBinary, + signer_address: String, + session_id: Uint64, + }, +} +``` + +In case a custom verification contract is specified, when a signature is submitted, the multisig contract will call the `VerifySignature` query on the custom contract to verify the signature, which in turn should return `true` if the signature is valid or `false` otherwise. + +```mermaid +sequenceDiagram +actor Signers +box LightYellow Axelar +participant Multisig +participant SignatureVerifier +end + +Signers->>+Multisig: ExecuteMsg::SubmitSignature +Multisig->>SignatureVerifier: QueryMsg::VerifySignature +SignatureVerifier->>Multisig: returns true/false +deactivate Multisig +``` + ## Interface ```Rust pub enum ExecuteMsg { StartSigningSession { + worker_set_id: String, msg: HexBinary, + chain_name: ChainName, + sig_verifier: Option, }, SubmitSignature { session_id: Uint64, diff --git a/packages/signature-verifier-api/Cargo.toml b/packages/signature-verifier-api/Cargo.toml new file mode 100644 index 000000000..8dd99c411 --- /dev/null +++ b/packages/signature-verifier-api/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "signature-verifier-api" +version = "0.1.0" +rust-version = { workspace = true } +edition = "2021" + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +error-stack = { workspace = true } +thiserror = { workspace = true } diff --git a/packages/signature-verifier-api/src/client.rs b/packages/signature-verifier-api/src/client.rs new file mode 100644 index 000000000..3b549ee89 --- /dev/null +++ b/packages/signature-verifier-api/src/client.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::serde::de::DeserializeOwned; +use cosmwasm_std::{to_binary, Addr, HexBinary, QuerierWrapper, QueryRequest, Uint64, WasmQuery}; +use error_stack::{Result, ResultExt}; + +use crate::msg::QueryMsg; + +pub struct SignatureVerifier<'a> { + pub address: Addr, + pub querier: QuerierWrapper<'a>, +} + +impl SignatureVerifier<'_> { + fn query(&self, msg: &QueryMsg) -> Result { + self.querier + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: self.address.to_string(), + msg: to_binary(msg).expect("msg should always be serializable"), + })) + .change_context(Error::QuerySignatureVerifier) + } + + pub fn verify_signature( + &self, + signature: HexBinary, + message: HexBinary, + public_key: HexBinary, + signer_address: String, + session_id: Uint64, + ) -> Result { + self.query(&QueryMsg::VerifySignature { + signature, + message, + public_key, + signer_address, + session_id, + }) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("could not query the signature verifier contract")] + QuerySignatureVerifier, +} diff --git a/packages/signature-verifier-api/src/lib.rs b/packages/signature-verifier-api/src/lib.rs new file mode 100644 index 000000000..9952fef07 --- /dev/null +++ b/packages/signature-verifier-api/src/lib.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod msg; diff --git a/packages/signature-verifier-api/src/msg.rs b/packages/signature-verifier-api/src/msg.rs new file mode 100644 index 000000000..edc8cdc27 --- /dev/null +++ b/packages/signature-verifier-api/src/msg.rs @@ -0,0 +1,15 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{HexBinary, Uint64}; + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(bool)] + VerifySignature { + signature: HexBinary, + message: HexBinary, + public_key: HexBinary, + signer_address: String, + session_id: Uint64, + }, +}