diff --git a/Cargo.toml b/Cargo.toml index 1a50471..0950cf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,4 +24,4 @@ base64 = "0.21.4" phf = { version = "0.11.2", features = ["macros"] } rsa = { version = "0.9.2" } getrandom = { version = "0.2.10", features = ["custom"] } -p256 = {version = "0.13.2", features = ["ecdsa-core", "arithmetic", "serde"]} \ No newline at end of file +p256 = {version = "0.13.2", features = ["ecdsa-core", "arithmetic", "serde"]} diff --git a/account/Cargo.toml b/account/Cargo.toml index 6b095da..1612b58 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -30,4 +30,6 @@ p256 = { workspace = true } url = "2.4.1" coset = "0.3.5" futures = "0.3.29" -async-trait = "0.1.74" \ No newline at end of file +async-trait = "0.1.74" +prost = {version = "0.11.2", default-features = false, features = ["prost-derive"]} +osmosis-std-derive = "0.13.2" diff --git a/account/src/auth.rs b/account/src/auth.rs index 6b3103b..f31f0c5 100644 --- a/account/src/auth.rs +++ b/account/src/auth.rs @@ -1,5 +1,5 @@ -use crate::auth::secp256r1::verify; use crate::error::ContractError; +use crate::{auth::secp256r1::verify, proto::XionCustomQuery}; use cosmwasm_std::{Binary, Deps, Env}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -72,7 +72,7 @@ pub enum Authenticator { impl Authenticator { pub fn verify( &self, - deps: Deps, + deps: Deps, env: &Env, tx_bytes: &Binary, sig_bytes: &Binary, diff --git a/account/src/auth/passkey.rs b/account/src/auth/passkey.rs index e3bf67b..2a9cd40 100644 --- a/account/src/auth/passkey.rs +++ b/account/src/auth/passkey.rs @@ -1,9 +1,9 @@ use crate::error::ContractResult; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use crate::proto::{self, XionCustomQuery}; +use base64::engine::general_purpose::{self}; use base64::Engine; use cosmwasm_schema::cw_serde; -use cosmwasm_std::QueryRequest::Stargate; -use cosmwasm_std::{to_binary, Addr, Binary, Deps}; +use cosmwasm_std::{Addr, Binary, Deps}; #[cw_serde] struct QueryRegisterRequest { @@ -18,19 +18,23 @@ struct QueryRegisterResponse { credential: Binary, } -pub fn register(deps: Deps, addr: Addr, rp: String, data: Binary) -> ContractResult { - let query = QueryRegisterRequest { +#[cw_serde] +struct QueryAuthenticateResponse {} + +pub fn register( + deps: Deps, + addr: Addr, + rp: String, + data: Binary, +) -> ContractResult { + let query = proto::QueryWebAuthNVerifyRegisterRequest { addr: addr.clone().into(), - challenge: addr.to_string(), + challenge: Binary::from(addr.as_bytes()).to_base64(), rp, - data, + data: data.to_vec(), }; - let query_bz = to_binary(&query)?; - let query_response: QueryRegisterResponse = deps.querier.query(&Stargate { - path: "xion.v1.Query/WebAuthNVerifyRegister".to_string(), - data: query_bz, - })?; + let query_response = deps.querier.query::(&query.into())?; Ok(query_response.credential) } @@ -45,28 +49,26 @@ struct QueryVerifyRequest { } pub fn verify( - deps: Deps, + deps: Deps, addr: Addr, rp: String, signature: &Binary, tx_hash: Vec, credential: &Binary, ) -> ContractResult { - let challenge = URL_SAFE_NO_PAD.encode(tx_hash); + let challenge = + general_purpose::URL_SAFE_NO_PAD.encode(general_purpose::STANDARD.encode(tx_hash)); - let query = QueryVerifyRequest { + let query = proto::QueryWebAuthNVerifyAuthenticateRequest { addr: addr.into(), challenge, rp, - credential: credential.clone(), - data: signature.clone(), + credential: credential.clone().into(), + data: signature.clone().into(), }; - let query_bz = to_binary(&query)?; - deps.querier.query(&Stargate { - path: "xion.v1.Query/WebAuthNVerifyAuthenticate".to_string(), - data: query_bz, - })?; + deps.querier + .query::(&query.into())?; Ok(true) } diff --git a/account/src/auth/sign_arb.rs b/account/src/auth/sign_arb.rs index 186c77e..cafc77c 100644 --- a/account/src/auth/sign_arb.rs +++ b/account/src/auth/sign_arb.rs @@ -31,12 +31,14 @@ fn wrap_message(msg_bytes: &[u8], signer: Addr) -> Vec { mod tests { use crate::auth::sign_arb::wrap_message; use crate::auth::util; - use crate::auth::AddAuthenticator::Secp256K1; use crate::contract::instantiate; use crate::msg::InstantiateMsg; + use crate::proto::XionCustomQuery; use base64::{engine::general_purpose, Engine as _}; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{Addr, Api, Binary}; + use cosmwasm_std::testing::{ + mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, + }; + use cosmwasm_std::{Addr, Api, Binary, OwnedDeps}; #[test] fn test_derive_addr() { @@ -92,7 +94,12 @@ mod tests { #[test] fn test_init_sign_arb() { - let mut deps = mock_dependencies(); + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: core::marker::PhantomData::, + }; let mut env = mock_env(); let info = mock_info("sender", &[]); // This is the local faucet address to simplify reuse @@ -115,7 +122,7 @@ mod tests { let signature_bytes = general_purpose::STANDARD.decode(signature).unwrap(); let instantiate_msg = InstantiateMsg { - authenticator: Secp256K1 { + authenticator: crate::auth::AddAuthenticator::Secp256K1 { id: 0, pubkey: Binary::from(pubkey_bytes), signature: Binary::from(signature_bytes), diff --git a/account/src/contract.rs b/account/src/contract.rs index 1d1f226..23d4e68 100644 --- a/account/src/contract.rs +++ b/account/src/contract.rs @@ -7,6 +7,7 @@ use absacc::AccountSudoMsg; use crate::error::ContractError; use crate::execute::{add_auth_method, assert_self, remove_auth_method}; use crate::msg::{ExecuteMsg, MigrateMsg}; +use crate::proto::XionCustomQuery; use crate::{ error::ContractResult, execute, @@ -16,17 +17,21 @@ use crate::{ #[entry_point] pub fn instantiate( - deps: DepsMut, + deps: DepsMut, env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> ContractResult { cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - execute::init(deps, env, msg.authenticator) + execute::init(deps, env, &mut msg.authenticator.clone()) } #[entry_point] -pub fn sudo(deps: DepsMut, env: Env, msg: AccountSudoMsg) -> ContractResult { +pub fn sudo( + deps: DepsMut, + env: Env, + msg: AccountSudoMsg, +) -> ContractResult { match msg { AccountSudoMsg::BeforeTx { tx_bytes, @@ -46,17 +51,18 @@ pub fn sudo(deps: DepsMut, env: Env, msg: AccountSudoMsg) -> ContractResult, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> ContractResult { assert_self(&info.sender, &env.contract.address)?; - match msg { + let mut owned_msg = msg.clone(); + match &mut owned_msg { ExecuteMsg::AddAuthMethod { add_authenticator } => { add_auth_method(deps, env, add_authenticator) } - ExecuteMsg::RemoveAuthMethod { id } => remove_auth_method(deps, env, id), + ExecuteMsg::RemoveAuthMethod { id } => remove_auth_method(deps, env, *id), } } diff --git a/account/src/error.rs b/account/src/error.rs index 338bcd1..cbfc309 100644 --- a/account/src/error.rs +++ b/account/src/error.rs @@ -66,6 +66,9 @@ pub enum ContractError { #[error("invalid token")] InvalidToken, + #[error("url parse error: {url}")] + URLParse { url: String }, + #[error("cannot override existing authenticator at index {index}")] OverridingIndex { index: u8 }, } diff --git a/account/src/execute.rs b/account/src/execute.rs index 2789f79..77c5e30 100644 --- a/account/src/execute.rs +++ b/account/src/execute.rs @@ -1,17 +1,20 @@ +use std::borrow::BorrowMut; + use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Env, Event, Order, Response}; use crate::auth::{passkey, AddAuthenticator, Authenticator}; +use crate::proto::XionCustomQuery; use crate::{ error::{ContractError, ContractResult}, state::AUTHENTICATORS, }; pub fn init( - deps: DepsMut, + deps: DepsMut, env: Env, - add_authenticator: AddAuthenticator, + add_authenticator: &mut AddAuthenticator, ) -> ContractResult { - add_auth_method(deps, env.clone(), add_authenticator.clone())?; + add_auth_method(deps, env.clone(), add_authenticator)?; Ok( Response::new().add_event(Event::new("create_abstract_account").add_attributes(vec![ @@ -26,7 +29,7 @@ pub fn init( } pub fn before_tx( - deps: Deps, + deps: Deps, env: &Env, tx_bytes: &Binary, cred_bytes: Option<&Binary>, @@ -87,29 +90,29 @@ pub fn after_tx() -> ContractResult { } pub fn add_auth_method( - deps: DepsMut, + deps: DepsMut, env: Env, - add_authenticator: AddAuthenticator, + add_authenticator: &mut AddAuthenticator, ) -> ContractResult { - match add_authenticator.clone() { + match add_authenticator.borrow_mut() { AddAuthenticator::Secp256K1 { id, pubkey, signature, } => { let auth = Authenticator::Secp256K1 { - pubkey: pubkey.clone(), + pubkey: (*pubkey).clone(), }; if !auth.verify( deps.as_ref(), &env, &Binary::from(env.contract.address.as_bytes()), - &signature, + signature, )? { Err(ContractError::InvalidSignature) } else { - save_authenticator(deps, id, &auth)?; + save_authenticator(deps, *id, &auth)?; Ok(()) } } @@ -118,17 +121,19 @@ pub fn add_auth_method( pubkey, signature, } => { - let auth = Authenticator::Ed25519 { pubkey }; + let auth = Authenticator::Ed25519 { + pubkey: (*pubkey).clone(), + }; if !auth.verify( deps.as_ref(), &env, &Binary::from(env.contract.address.as_bytes()), - &signature, + signature, )? { Err(ContractError::InvalidSignature) } else { - save_authenticator(deps, id, &auth)?; + save_authenticator(deps, *id, &auth)?; Ok(()) } } @@ -137,17 +142,19 @@ pub fn add_auth_method( address, signature, } => { - let auth = Authenticator::EthWallet { address }; + let auth = Authenticator::EthWallet { + address: (*address).clone(), + }; if !auth.verify( deps.as_ref(), &env, &Binary::from(env.contract.address.as_bytes()), - &signature, + signature, )? { Err(ContractError::InvalidSignature) } else { - save_authenticator(deps, id, &auth)?; + save_authenticator(deps, *id, &auth)?; Ok(()) } } @@ -157,17 +164,20 @@ pub fn add_auth_method( sub, token, } => { - let auth = Authenticator::Jwt { aud, sub }; + let auth = Authenticator::Jwt { + aud: (*aud).clone(), + sub: (*sub).clone(), + }; if !auth.verify( deps.as_ref(), &env, &Binary::from(env.contract.address.as_bytes()), - &token, + token, )? { Err(ContractError::InvalidSignature) } else { - save_authenticator(deps, id, &auth)?; + save_authenticator(deps, *id, &auth)?; Ok(()) } } @@ -176,17 +186,19 @@ pub fn add_auth_method( pubkey, signature, } => { - let auth = Authenticator::Secp256R1 { pubkey }; + let auth = Authenticator::Secp256R1 { + pubkey: (*pubkey).clone(), + }; if !auth.verify( deps.as_ref(), &env, &Binary::from(env.contract.address.as_bytes()), - &signature, + signature, )? { Err(ContractError::InvalidSignature) } else { - AUTHENTICATORS.save(deps.storage, id, &auth)?; + AUTHENTICATORS.save(deps.storage, *id, &auth)?; Ok(()) } } @@ -198,13 +210,18 @@ pub fn add_auth_method( let passkey = passkey::register( deps.as_ref(), env.contract.address.clone(), - url.clone(), - credential, + (*url).clone(), + (*credential).clone(), )?; - let auth = Authenticator::Passkey { url, passkey }; - AUTHENTICATORS.save(deps.storage, id, &auth)?; - + let auth = Authenticator::Passkey { + url: (*url).clone(), + passkey: passkey.clone(), + }; + AUTHENTICATORS.save(deps.storage, *id, &auth)?; + // we replace the sent credential with the passkey for indexers and other + // observers to see + *(credential) = passkey; Ok(()) } }?; @@ -220,7 +237,7 @@ pub fn add_auth_method( } pub fn save_authenticator( - deps: DepsMut, + deps: DepsMut, id: u8, authenticator: &Authenticator, ) -> ContractResult<()> { @@ -232,7 +249,11 @@ pub fn save_authenticator( Ok(()) } -pub fn remove_auth_method(deps: DepsMut, env: Env, id: u8) -> ContractResult { +pub fn remove_auth_method( + deps: DepsMut, + env: Env, + id: u8, +) -> ContractResult { if AUTHENTICATORS .keys(deps.storage, None, None, Order::Ascending) .count() @@ -261,17 +282,24 @@ pub fn assert_self(sender: &Addr, contract: &Addr) -> ContractResult<()> { #[cfg(test)] mod tests { use base64::{engine::general_purpose, Engine as _}; - use cosmwasm_std::testing::{mock_dependencies, mock_env}; - use cosmwasm_std::Binary; + use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage}; + use cosmwasm_std::{Binary, OwnedDeps}; use crate::auth::Authenticator; use crate::execute::before_tx; + use crate::proto::{self, QueryWebAuthNVerifyRegisterResponse, XionCustomQuery}; use crate::state::AUTHENTICATORS; + use cosmwasm_std::QueryRequest::Custom; #[test] fn test_before_tx() { let auth_id = 0; - let mut deps = mock_dependencies(); + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: std::marker::PhantomData, + }; let env = mock_env(); let pubkey = "Ayrlj6q3WWs91p45LVKwI8JyfMYNmWMrcDinLNEdWYE4"; @@ -299,4 +327,49 @@ mod tests { before_tx(deps.as_ref(), &env, &tx_bytes, Some(&sig_bytes), false).unwrap(); } + + #[test] + pub fn test_custom_querier() { + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: core::marker::PhantomData::, + }; + + deps.querier = deps.querier.with_custom_handler(|query| match query { + XionCustomQuery::Verify(data) => { + assert_eq!(data.addr, "mock_address"); + assert_eq!(data.challenge, "mock_challenge"); + assert_eq!(data.rp, "mock_rp"); + assert_eq!(data.data, vec![0u8]); + + cosmwasm_std::SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + serde_json::to_vec(&QueryWebAuthNVerifyRegisterResponse { + credential: Binary::from("true".as_bytes()).into(), + }) + .unwrap() + .into(), + )) + } + XionCustomQuery::Authenticate(_) => todo!(), + }); + + let query_msg = XionCustomQuery::Verify(proto::QueryWebAuthNVerifyRegisterRequest { + addr: "mock_address".to_string(), + challenge: "mock_challenge".to_string(), + rp: "mock_rp".to_string(), + data: vec![0u8], + }); + let query_response = deps + .as_ref() + .querier + .query::(&Custom(query_msg)); + assert!(query_response.is_ok()); + + assert_eq!( + query_response.unwrap().credential, + Binary::from("true".as_bytes()) + ); + } } diff --git a/account/src/lib.rs b/account/src/lib.rs index 4ddb9d0..5f48840 100644 --- a/account/src/lib.rs +++ b/account/src/lib.rs @@ -6,6 +6,7 @@ pub mod contract; pub mod error; pub mod execute; pub mod msg; +pub mod proto; pub mod query; pub mod state; diff --git a/account/src/proto.rs b/account/src/proto.rs new file mode 100644 index 0000000..d3dfb46 --- /dev/null +++ b/account/src/proto.rs @@ -0,0 +1,70 @@ +use cosmwasm_std::CustomQuery; +use osmosis_std_derive::CosmwasmExt; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/xion.v1.Query/WebAuthNVerifyRegister")] +#[proto_query(path = "/xion.v1.Query/WebAuthNVerifyRegister", response_type = QueryWebAuthNVerifyRegisterResponse)] +pub struct QueryWebAuthNVerifyRegisterRequest { + #[prost(string, tag = "1")] + pub addr: String, + #[prost(string, tag = "2")] + pub challenge: String, + #[prost(string, tag = "3")] + pub rp: String, + #[prost(bytes, tag = "4")] + pub data: Vec, +} + +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/xion.v1.Query/WebAuthNVerifyAuthenticate")] +#[proto_query(path = "/xion.v1.Query/WebAuthNVerifyAuthenticate", response_type = QueryWebAuthNVerifyAuthenticateResponse)] +pub struct QueryWebAuthNVerifyAuthenticateRequest { + #[prost(string, tag = "1")] + pub addr: String, + #[prost(string, tag = "2")] + pub challenge: String, + #[prost(string, tag = "3")] + pub rp: String, + #[prost(bytes, tag = "4")] + pub credential: Vec, + #[prost(bytes, tag = "5")] + pub data: Vec, +} + +// We define the response as a prost message to be able to decode the protobuf data. +#[derive(Clone, PartialEq, Eq, ::prost::Message, serde::Serialize, serde::Deserialize)] +pub struct QueryWebAuthNVerifyRegisterResponse { + #[prost(bytes, tag = "1")] + pub credential: Vec, +} + +#[derive(Clone, PartialEq, Eq, ::prost::Message, serde::Serialize, serde::Deserialize)] +pub struct QueryWebAuthNVerifyAuthenticateResponse {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum XionCustomQuery { + Verify(QueryWebAuthNVerifyRegisterRequest), + Authenticate(QueryWebAuthNVerifyAuthenticateRequest), +} +impl CustomQuery for XionCustomQuery {}