cw_auth/
auth.rs

1use crate::error::AuthError;
2use bech32::{self, u5, ToBase32};
3use cosmwasm_std::{Timestamp, Addr, MessageInfo, Env};
4#[cfg(target_arch = "wasm32")]
5use cosmwasm_std::{Api, ExternalApi};
6#[cfg(not(target_arch = "wasm32"))]
7use cosmwasm_crypto::{secp256k1_verify};
8use ripemd::{Digest, Ripemd160};
9use serde::de::DeserializeOwned;
10use serde::{Serialize, Deserialize};
11use sha2::Sha256;
12use std::str::from_utf8;
13use schemars::JsonSchema;
14
15#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
16/// some message along with signed authorization token
17pub struct MsgWithAuth<T> {
18    pub authorization: Authorization,
19    pub message: T,
20}
21
22#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
23/// an ADR-36 signed document along with a signature and pubkey of the signer
24pub struct Authorization {
25    pub document: String,
26    pub signature: String,
27    pub pubkey: String,
28}
29
30#[derive(Deserialize, Clone, Debug)]
31/// a message with extracted and validated auth token
32pub struct Authorized<S, T> {
33    pub auth_token: AuthToken<S>,
34    pub message: T,
35}
36
37#[derive(Deserialize, Clone, Debug)]
38/// auth token including addresses of signer and agent, expiration time, and any metadata
39pub struct AuthToken<T> {
40    pub user: Addr,
41    pub agent: Addr,
42    pub expires: u64,
43    pub meta: T,
44}
45
46#[derive(Deserialize)]
47struct SignDoc {
48    msgs: [SignMessage; 1],
49}
50
51#[derive(Deserialize)]
52struct SignMessage {
53    value: SignValue,
54}
55
56#[derive(Deserialize)]
57struct SignValue {
58    signer: Addr,
59    data: String,
60}
61
62/// authorize a message
63///
64/// takes a message with a signed and encoded auth token, and returns the message along
65/// with the validated and decoded auth token.
66pub fn authorize<M, A: DeserializeOwned>(
67    message: MsgWithAuth<M>,
68    info: &MessageInfo,
69    env: &Env,
70) -> Result<Authorized<A, M>, AuthError> {
71    Ok(Authorized {
72        message: message.message,
73        auth_token: validate(message.authorization, &info.sender, env.block.time)?,
74    })
75}
76
77/// validate a signed authorization token
78///
79/// this will ensure that the token is signed by the right address, provided by the right
80/// address, and unexpired, and will return the decoded token from within the document if
81/// it is valid.
82pub fn validate<A: DeserializeOwned>(
83    authorization: Authorization,
84    provider: &Addr,
85    block_time: Timestamp,
86) -> Result<AuthToken<A>, AuthError> {
87    let pubkey = base64::decode(authorization.pubkey)?;
88    let document = base64::decode(authorization.document)?;
89    let signature = base64::decode(authorization.signature)?;
90    validate_document_signature(&document, &signature, &pubkey)?;
91
92    let document = from_utf8(&document)?;
93    let document: SignDoc = serde_json_wasm::from_str(document)?;
94    let signer = &document.msgs[0].value.signer;
95    validate_signer_pubkey(signer, pubkey)?;
96
97    let auth_token = extract_token(&document)?;
98    validate_token_expires(&auth_token, block_time)?;
99    validate_token_user(&auth_token, signer)?;
100    validate_token_agent(&auth_token, provider)?;
101
102    Ok(auth_token)
103}
104
105/// grab the token from inside the SignDoc
106fn extract_token<A: DeserializeOwned>(
107    document: &SignDoc,
108) -> Result<AuthToken<A>, AuthError> {
109    let token = &document.msgs[0].value.data;
110    let token = base64::decode(token)?;
111    let token = from_utf8(&token)?.to_string();
112    Ok(serde_json_wasm::from_str(&token)?)
113}
114
115/// validate token has not yet expired
116fn validate_token_expires<A>(
117    token: &AuthToken<A>,
118    block_time: Timestamp,
119) -> Result<bool, AuthError> {
120    if token.expires > block_time.seconds() {
121        Ok(true)
122    } else {
123        Err(AuthError::TokenExpired)
124    }
125}
126
127/// check that the token is signed by the specified user
128fn validate_token_user<A>(token: &AuthToken<A>, signer: &Addr) -> Result<bool, AuthError> {
129    if token.user == *signer {
130        Ok(true)
131    } else {
132        Err(AuthError::TokenAddressMismatch)
133    }
134}
135
136/// check that the agent address in the token is the one we expect
137fn validate_token_agent<A>(token: &AuthToken<A>, agent: &Addr) -> Result<bool, AuthError> {
138    if token.agent == *agent {
139        Ok(true)
140    } else {
141        Err(AuthError::TokenAddressMismatch)
142    }
143}
144
145/// check that the signature is valid for this document
146fn validate_document_signature(
147    document: &[u8],
148    signature: &[u8],
149    pubkey: &[u8],
150) -> Result<bool, AuthError> {
151    let mut hasher = Sha256::new();
152    hasher.update(&document);
153    let document_hash: &[u8] = &hasher.finalize();
154
155    #[cfg(target_arch = "wasm32")]
156    let verification = {
157        let api = ExternalApi {};
158        api.secp256k1_verify(document_hash, signature, pubkey)
159    };
160
161    #[cfg(not(target_arch = "wasm32"))]
162    let verification = secp256k1_verify(document_hash, signature, pubkey);
163
164    if let Ok(true) = verification {
165        Ok(true)
166    } else {
167        Err(AuthError::InvalidSignature)
168    }
169}
170
171/// check that the stated signer corresponds to the given public key
172fn validate_signer_pubkey(address: &Addr, pubkey: Vec<u8>) -> Result<bool, AuthError> {
173    let (_hrp, data, _variant) = bech32::decode(address.as_str())?;
174    let hashed_pubkey = hash_pubkey(pubkey);
175    if data == hashed_pubkey {
176        Ok(true)
177    } else {
178        Err(AuthError::SignerPubkeyMismatch)
179    }
180}
181
182/// hash pubkey to get the data encoded in bech32 addresses
183fn hash_pubkey(pubkey: Vec<u8>) -> Vec<u5> {
184    let mut hasher = Sha256::new();
185    hasher.update(&pubkey);
186    let pubkey = hasher.finalize();
187    let mut hasher = Ripemd160::new();
188    hasher.update(pubkey);
189    hasher.finalize().to_vec().to_base32()
190}