From 5318191646d3015f5c22fd96a865217fb0594738 Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 27 Sep 2023 22:44:03 +0100 Subject: [PATCH 01/20] jwt verification via contract --- Cargo.toml | 4 +- account/Cargo.toml | 6 ++- account/src/auth.rs | 21 +++++++- account/src/auth/jwt.rs | 106 ++++++++++++++++++++++++++++++++++++++++ account/src/contract.rs | 10 +++- account/src/error.rs | 15 ++++++ account/src/execute.rs | 32 +++++++++++- 7 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 account/src/auth/jwt.rs diff --git a/Cargo.toml b/Cargo.toml index 6001e26..1e83301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,6 @@ serde_json = "1.0.87" schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" -base64 = "0.21.4" \ No newline at end of file +base64 = "0.21.4" +jsonwebtoken = "8.3.0" +phf = { version = "0.11.2", features = ["macros"]} \ No newline at end of file diff --git a/account/Cargo.toml b/account/Cargo.toml index a4683ad..7b49ed2 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -12,7 +12,7 @@ absacc = { git = "https://github.com/larry0x/abstract-account.git" } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } -cw-storage-plus = { workspace = true } +cw-storage-plus = { workspace = true }q sha2 = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } @@ -22,4 +22,6 @@ schemars = { workspace = true } hex = { workspace = true } ripemd = { workspace = true } bech32 = { workspace = true } -base64 = { workspace = true } \ No newline at end of file +base64 = { workspace = true } +jsonwebtoken = { workspace = true } +phf = { workspace = true } \ No newline at end of file diff --git a/account/src/auth.rs b/account/src/auth.rs index fa5f379..0643219 100644 --- a/account/src/auth.rs +++ b/account/src/auth.rs @@ -1,9 +1,10 @@ use crate::error::ContractError; -use cosmwasm_std::{Api, Binary}; +use cosmwasm_std::{Api, Binary, Env}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; mod eth_crypto; +mod jwt; mod sign_arb; pub mod util; @@ -24,6 +25,12 @@ pub enum AddAuthenticator { address: String, signature: Binary, }, + JWT { + id: u8, + aud: String, + sub: String, + token: Binary, + }, } #[derive(Serialize, Deserialize, Clone, JsonSchema, PartialEq, Debug)] @@ -31,12 +38,14 @@ pub enum Authenticator { Secp256K1 { pubkey: Binary }, Ed25519 { pubkey: Binary }, EthWallet { address: String }, + JWT { aud: String, sub: String }, } impl Authenticator { pub fn verify( &self, api: &dyn Api, + env: &Env, tx_bytes: &Binary, sig_bytes: &Binary, ) -> Result { @@ -72,6 +81,16 @@ impl Authenticator { Err(error) => Err(error), } } + Authenticator::JWT { aud, sub } => { + let tx_bytes_hash = util::sha256(tx_bytes); + return jwt::verify( + &env.block.time, + &tx_bytes_hash, + sig_bytes.as_slice(), + aud, + sub, + ); + } } } } diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs new file mode 100644 index 0000000..a4b934b --- /dev/null +++ b/account/src/auth/jwt.rs @@ -0,0 +1,106 @@ +use crate::error::ContractError::{InvalidJWTAud, InvalidTime}; +use crate::error::ContractResult; +use base64::{engine::general_purpose, Engine as _}; +use cosmwasm_std::Timestamp; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use phf::{phf_map, Map}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::str; + +static AUD_KEY_MAP: Map<&'static str, &'static str> = phf_map! { + "project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94" => "sQKkA829tzjU2VA-INHvdrewkbQzjpsMn0PNM7KJaBODbB4ItZM4x1NVSWBiy2DGHkaDDvADRbbq1BZsC1iXVtIYm0AoD7x4QC1w89kp2_s0wmvUOSPiQZlYrgJqRDXirXJZX3MNku2McXbwdyPajDaR4nBBQOoUOF21CHqLDqBHs2R6tHyL80R_8mgueiqQ-4wg6SSVcB_6ZOh59vRcjKr34upKPWGQzvMGCkeTO9whzbIWbA1j-8ykiS63EhjWBZU_sSolsf1ZGq8peVrADDLhOvHtZxCZLKwB46k2kb8GKAWlO4wRP6BDVjzpnea7BsvZ6JwULKg3HisH9gzaiQ;AQAB", +}; + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + aud: Box<[String]>, // Optional. Audience + exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) + iat: usize, // Optional. Issued at (as UTC timestamp) + iss: String, // Optional. Issuer + nbf: usize, // Optional. Not Before (as UTC timestamp) + sub: String, // Optional. Subject (whom token refers to) + + transaction_hash: String, +} + +pub fn verify( + current_time: &Timestamp, + tx_hash: &Vec, + sig_bytes: &[u8], + aud: &String, + sub: &String, +) -> ContractResult { + if !AUD_KEY_MAP.contains_key(aud.as_str()) { + return Err(InvalidJWTAud); + } + + let key = match AUD_KEY_MAP.get(aud.as_str()) { + None => return Err(InvalidJWTAud), + Some(k) => *k, + }; + + let token = str::from_utf8(sig_bytes)?; + + // currently only RS256 is supported + let mut options = Validation::new(Algorithm::RS256); + options.required_spec_claims = HashSet::from([ + "sub".to_string(), + "aud".to_string(), + "exp".to_string(), + "nbf".to_string(), + "iat".to_string(), + "iss".to_string(), + "transaction_hash".to_string(), + ]); + + // make sure the sub and aud ids are as expected + options.sub = Option::from(sub.clone()); + options.aud = Option::from(HashSet::from([aud.clone()])); + + // disable time checks because system time is not available, will pull directly from BlockInfo + options.validate_exp = false; + options.validate_nbf = false; + + let mut key_split = key.split(';'); + let modulus = key_split.next().ok_or(InvalidJWTAud)?; + let exponent = key_split.next().ok_or(InvalidJWTAud)?; + let decoding_key = DecodingKey::from_rsa_components(modulus, exponent)?; + let token = decode::(&token, &decoding_key, &options)?; + + // complete the time checks + let expiration = Timestamp::from_seconds(token.claims.exp as u64); + if expiration.lt(current_time) { + return Err(InvalidTime); + } + let not_before = Timestamp::from_seconds(token.claims.nbf as u64); + if not_before.gt(current_time) { + return Err(InvalidTime); + } + + // make sure the provided hash matches the one from the tx + let hash_bytes = general_purpose::STANDARD.decode(token.claims.transaction_hash)?; + Ok(tx_hash.eq(&hash_bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::engine::general_purpose; + + #[test] + fn test_validate_token() { + let encoded_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWI5OGFjMTExLTg1MTUtNGY0OS05MDU2LTdmM2E5NzJmNzU4MSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCJdLCJleHAiOjE2OTU3NzA2MjgsImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTZmNDI2ZDhhLTA5M2UtNDQ1NS1hZThkLTlkMDg5MTZhMGI2NSIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImV4cGlyZXNfYXQiOiIyMDIzLTEwLTI2VDIzOjE4OjQ4WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6InBhc3N3b3JkIiwiZGVsaXZlcnlfbWV0aG9kIjoia25vd2xlZGdlIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wOS0yNlQyMzoxODo0OFoifV19LCJpYXQiOjE2OTU3NzAzMjgsImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCIsIm5iZiI6MTY5NTc3MDMyOCwic3ViIjoidXNlci10ZXN0LTUxODUyNjQyLWE1OTQtNGE2Zi1iNzZmLTkyODUxNzI1YTQyOSIsInRyYW5zYWN0aW9uX2hhc2giOiIweDEyMzQ1Njc4OTAifQ.ToSTvPFAaFP-eIqwWSBp0z7iotclWoNkghlrecU34kAoxloEqLvooXI7Ws_-HKy1rhTWhPWOtfh4QoxObV-39pe44xPFCoN2Vv0MiutMKJSaeIC5eVHxvSz0b2jjw0WkiPj8dK8HdzscNajvMATQ9R97U_i3rluTMnvliTw0zGUUsrMfTaHcltATUJ6Ufthxvb9w2XTTsIsBx0Ttldbf0XE_ZQnFk2uNW9Skyq0-zlZZXBorEbIbbAVeA87T_4CPqp9Pdc2qXRj9XrFtXuTD1lnXk9d28tu8l4H-4CWb8DYXZrFqb9-knavNXRsKb2NJnAcTh5c_I9RvR9lVxJtkWg"; + let encoded_hash = "0x1234567890"; + let hash_bytes = general_purpose::STANDARD.decode(encoded_hash).unwrap(); + + let verification = verify( + &Timestamp::from_seconds(1695770329), + &hash_bytes, + encoded_token.as_bytes(), + &"project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94".to_string(), + &"user-test-51852642-a594-4a6f-b76f-92851725a429".to_string(), + ); + assert!(verification.unwrap()); + } +} diff --git a/account/src/contract.rs b/account/src/contract.rs index 0051b17..003c90d 100644 --- a/account/src/contract.rs +++ b/account/src/contract.rs @@ -25,14 +25,20 @@ pub fn instantiate( } #[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, cred_bytes, simulate, .. - } => execute::before_tx(deps.as_ref(), &tx_bytes, cred_bytes.as_ref(), simulate), + } => execute::before_tx( + deps.as_ref(), + &env, + &tx_bytes, + cred_bytes.as_ref(), + simulate, + ), AccountSudoMsg::AfterTx { .. } => execute::after_tx(), } } diff --git a/account/src/error.rs b/account/src/error.rs index f10fa10..6ead30e 100644 --- a/account/src/error.rs +++ b/account/src/error.rs @@ -15,6 +15,15 @@ pub enum ContractError { #[error(transparent)] Bech32(#[from] bech32::Error), + #[error(transparent)] + JsonWebToken(#[from] jsonwebtoken::errors::Error), + + #[error(transparent)] + UTF8Error(#[from] std::str::Utf8Error), + + #[error(transparent)] + Base64Decode(#[from] base64::DecodeError), + #[error("signature is invalid")] InvalidSignature, @@ -35,6 +44,12 @@ pub enum ContractError { #[error("cannot delete the last authenticator")] MinimumAuthenticatorCount, + + #[error("invalid time on signature")] + InvalidTime, + + #[error("invalid jwt aud")] + InvalidJWTAud, } pub type ContractResult = Result; diff --git a/account/src/execute.rs b/account/src/execute.rs index 78d6c06..4b44584 100644 --- a/account/src/execute.rs +++ b/account/src/execute.rs @@ -16,6 +16,7 @@ pub fn init( ) -> ContractResult { if !authenticator.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), signature, )? { @@ -31,6 +32,7 @@ pub fn init( pub fn before_tx( deps: Deps, + env: &Env, tx_bytes: &Binary, cred_bytes: Option<&Binary>, simulate: bool, @@ -66,9 +68,12 @@ pub fn before_tx( return Err(ContractError::ShortSignature); } } + Authenticator::JWT { .. } => { + // todo: figure out if there are minimum checks for JWTs + } } - return match authenticator.verify(deps.api, tx_bytes, sig_bytes)? { + return match authenticator.verify(deps.api, env, tx_bytes, sig_bytes)? { true => Ok(Response::new().add_attribute("method", "before_tx")), false => Err(ContractError::InvalidSignature), }; @@ -98,6 +103,7 @@ pub fn add_auth_method( if !auth.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), &signature, )? { @@ -118,6 +124,7 @@ pub fn add_auth_method( if !auth.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), &signature, )? { @@ -138,6 +145,7 @@ pub fn add_auth_method( if !auth.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), &signature, )? { @@ -149,6 +157,28 @@ pub fn add_auth_method( .add_attribute("authenticator_id", id.to_string())) } } + AddAuthenticator::JWT { + id, + aud, + sub, + token, + } => { + let auth = Authenticator::JWT { aud, sub }; + + if !auth.verify( + deps.api, + &env, + &Binary::from(env.contract.address.as_bytes()), + &token, + )? { + Err(ContractError::InvalidSignature) + } else { + AUTHENTICATORS.save(deps.storage, id, &auth)?; + Ok(Response::new() + .add_attribute("method", "execute") + .add_attribute("authenticator_id", id.to_string())) + } + } } } From 4749ece9f864a471439c09d492b1e97b4db9c75f Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 27 Sep 2023 22:46:07 +0100 Subject: [PATCH 02/20] typo --- account/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/Cargo.toml b/account/Cargo.toml index 7b49ed2..0861a85 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -12,7 +12,7 @@ absacc = { git = "https://github.com/larry0x/abstract-account.git" } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } -cw-storage-plus = { workspace = true }q +cw-storage-plus = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } From 75dc862eedf24ee5bdd9d834147c954ff0a39c20 Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 27 Sep 2023 22:53:19 +0100 Subject: [PATCH 03/20] lints --- account/src/auth.rs | 6 +++--- account/src/auth/jwt.rs | 14 +++++++------- account/src/execute.rs | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/account/src/auth.rs b/account/src/auth.rs index 0643219..ffd064d 100644 --- a/account/src/auth.rs +++ b/account/src/auth.rs @@ -25,7 +25,7 @@ pub enum AddAuthenticator { address: String, signature: Binary, }, - JWT { + Jwt { id: u8, aud: String, sub: String, @@ -38,7 +38,7 @@ pub enum Authenticator { Secp256K1 { pubkey: Binary }, Ed25519 { pubkey: Binary }, EthWallet { address: String }, - JWT { aud: String, sub: String }, + Jwt { aud: String, sub: String }, } impl Authenticator { @@ -81,7 +81,7 @@ impl Authenticator { Err(error) => Err(error), } } - Authenticator::JWT { aud, sub } => { + Authenticator::Jwt { aud, sub } => { let tx_bytes_hash = util::sha256(tx_bytes); return jwt::verify( &env.block.time, diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index a4b934b..b92ab8b 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -28,14 +28,14 @@ pub fn verify( current_time: &Timestamp, tx_hash: &Vec, sig_bytes: &[u8], - aud: &String, - sub: &String, + aud: &str, + sub: &str, ) -> ContractResult { - if !AUD_KEY_MAP.contains_key(aud.as_str()) { + if !AUD_KEY_MAP.contains_key(aud) { return Err(InvalidJWTAud); } - let key = match AUD_KEY_MAP.get(aud.as_str()) { + let key = match AUD_KEY_MAP.get(aud) { None => return Err(InvalidJWTAud), Some(k) => *k, }; @@ -55,8 +55,8 @@ pub fn verify( ]); // make sure the sub and aud ids are as expected - options.sub = Option::from(sub.clone()); - options.aud = Option::from(HashSet::from([aud.clone()])); + options.sub = Option::from(sub.to_string()); + options.aud = Option::from(HashSet::from([aud.to_string()])); // disable time checks because system time is not available, will pull directly from BlockInfo options.validate_exp = false; @@ -66,7 +66,7 @@ pub fn verify( let modulus = key_split.next().ok_or(InvalidJWTAud)?; let exponent = key_split.next().ok_or(InvalidJWTAud)?; let decoding_key = DecodingKey::from_rsa_components(modulus, exponent)?; - let token = decode::(&token, &decoding_key, &options)?; + let token = decode::(token, &decoding_key, &options)?; // complete the time checks let expiration = Timestamp::from_seconds(token.claims.exp as u64); diff --git a/account/src/execute.rs b/account/src/execute.rs index 4b44584..2dbc73e 100644 --- a/account/src/execute.rs +++ b/account/src/execute.rs @@ -68,7 +68,7 @@ pub fn before_tx( return Err(ContractError::ShortSignature); } } - Authenticator::JWT { .. } => { + Authenticator::Jwt { .. } => { // todo: figure out if there are minimum checks for JWTs } } @@ -157,13 +157,13 @@ pub fn add_auth_method( .add_attribute("authenticator_id", id.to_string())) } } - AddAuthenticator::JWT { + AddAuthenticator::Jwt { id, aud, sub, token, } => { - let auth = Authenticator::JWT { aud, sub }; + let auth = Authenticator::Jwt { aud, sub }; if !auth.verify( deps.api, From ed32b11172a0ad7a248ba03b10621cf16a8f4cb6 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 28 Sep 2023 22:48:26 +0100 Subject: [PATCH 04/20] disable pem, trying to avoid the clib issue --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1e83301..1d0400f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,5 @@ schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" base64 = "0.21.4" -jsonwebtoken = "8.3.0" +jsonwebtoken = {version = "8.3.0", default-features = false } phf = { version = "0.11.2", features = ["macros"]} \ No newline at end of file From 66aaa32d0d2ec6e035d9d4962583a4e93769e990 Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 1 Oct 2023 12:48:17 +0100 Subject: [PATCH 05/20] rsa direct jwt check --- Cargo.toml | 9 +++-- README.md | 1 - account/Cargo.toml | 4 +- account/src/auth/jwt.rs | 88 +++++++++++++++++++++++------------------ account/src/error.rs | 9 +++-- 5 files changed, 64 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d0400f..2619377 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ members = [ "account" ] +[build] +target = "wasm32-unknown-unknown" + [workspace.dependencies] cosmos-sdk-proto = { version = "0.19", default-features = false } cosmwasm-schema = "1.2" @@ -12,7 +15,7 @@ cw-storage-plus = "1" cw-utils = "1" hex = "0.4" prost = "0.11" -sha2 = "0.10" +sha2 = { version = "0.10.8", features = ["oid"]} thiserror = "1" tiny-keccak = { version = "2", features = ["keccak"] } serde = { version = "1.0.145", default-features = false, features = ["derive"] } @@ -21,5 +24,5 @@ schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" base64 = "0.21.4" -jsonwebtoken = {version = "8.3.0", default-features = false } -phf = { version = "0.11.2", features = ["macros"]} \ No newline at end of file +phf = { version = "0.11.2", features = ["macros"]} +rsa = "0.9.2" \ No newline at end of file diff --git a/README.md b/README.md index 54cb3ed..800d9ee 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,4 @@ docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ cosmwasm/workspace-optimizer:0.13.0 - ``` \ No newline at end of file diff --git a/account/Cargo.toml b/account/Cargo.toml index 0861a85..2681142 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -23,5 +23,5 @@ hex = { workspace = true } ripemd = { workspace = true } bech32 = { workspace = true } base64 = { workspace = true } -jsonwebtoken = { workspace = true } -phf = { workspace = true } \ No newline at end of file +phf = { workspace = true } +rsa = { workspace = true } \ No newline at end of file diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index b92ab8b..ca309c5 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -1,11 +1,13 @@ -use crate::error::ContractError::{InvalidJWTAud, InvalidTime}; +use crate::error::ContractError::{InvalidJWTAud, InvalidTime, InvalidToken}; use crate::error::ContractResult; -use base64::{engine::general_purpose, Engine as _}; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine as _; use cosmwasm_std::Timestamp; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use phf::{phf_map, Map}; +use rsa::traits::SignatureScheme; +use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey}; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use sha2::{Digest, Sha256}; use std::str; static AUD_KEY_MAP: Map<&'static str, &'static str> = phf_map! { @@ -40,66 +42,76 @@ pub fn verify( Some(k) => *k, }; - let token = str::from_utf8(sig_bytes)?; - - // currently only RS256 is supported - let mut options = Validation::new(Algorithm::RS256); - options.required_spec_claims = HashSet::from([ - "sub".to_string(), - "aud".to_string(), - "exp".to_string(), - "nbf".to_string(), - "iat".to_string(), - "iss".to_string(), - "transaction_hash".to_string(), - ]); - - // make sure the sub and aud ids are as expected - options.sub = Option::from(sub.to_string()); - options.aud = Option::from(HashSet::from([aud.to_string()])); - - // disable time checks because system time is not available, will pull directly from BlockInfo - options.validate_exp = false; - options.validate_nbf = false; + // prepare the components of the token for verification + let mut components = sig_bytes.split(|&b| b == b'.'); + let header_bytes = components.next().ok_or(InvalidToken)?; // ignore the header, it is not currently used + let payload_bytes = components.next().ok_or(InvalidToken)?; + let digest_bytes = [header_bytes, &[b'.'], payload_bytes].concat(); + let signature_bytes = components.next().ok_or(InvalidToken)?; + let signature = URL_SAFE_NO_PAD.decode(signature_bytes)?; + // retrieve and rebuild the pubkey let mut key_split = key.split(';'); let modulus = key_split.next().ok_or(InvalidJWTAud)?; + let mod_bytes = URL_SAFE_NO_PAD.decode(modulus)?; let exponent = key_split.next().ok_or(InvalidJWTAud)?; - let decoding_key = DecodingKey::from_rsa_components(modulus, exponent)?; - let token = decode::(token, &decoding_key, &options)?; + let exp_bytes = URL_SAFE_NO_PAD.decode(exponent)?; + let pubkey = RsaPublicKey::new( + BigUint::from_bytes_be(mod_bytes.as_slice()), + BigUint::from_bytes_be(exp_bytes.as_slice()), + )?; + + // hash the message body before verification + let mut hasher = Sha256::new(); + hasher.update(digest_bytes); + let digest = hasher.finalize().as_slice().to_vec(); + + // verify the signature + let scheme = Pkcs1v15Sign::new::(); + scheme.verify(&pubkey, digest.as_slice(), signature.as_slice())?; + + // at this point, we have verified that the token is legitimately signed. + // now we perform logic checks on the body + let payload = URL_SAFE_NO_PAD.decode(payload_bytes)?; + let claims: Claims = cosmwasm_std::from_slice(payload.as_slice())?; + if !claims.sub.eq_ignore_ascii_case(sub) { + // this token was not for the supplied sub + return Err(InvalidToken); + } + if !claims.aud.contains(&aud.to_string()) { + // this token was for a different aud + return Err(InvalidToken); + } // complete the time checks - let expiration = Timestamp::from_seconds(token.claims.exp as u64); + let expiration = Timestamp::from_seconds(claims.exp as u64); if expiration.lt(current_time) { return Err(InvalidTime); } - let not_before = Timestamp::from_seconds(token.claims.nbf as u64); + let not_before = Timestamp::from_seconds(claims.nbf as u64); if not_before.gt(current_time) { return Err(InvalidTime); } - // make sure the provided hash matches the one from the tx - let hash_bytes = general_purpose::STANDARD.decode(token.claims.transaction_hash)?; - Ok(tx_hash.eq(&hash_bytes)) + Ok(tx_hash.eq(claims.transaction_hash.as_bytes())) } #[cfg(test)] mod tests { use super::*; - use base64::engine::general_purpose; #[test] fn test_validate_token() { - let encoded_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWI5OGFjMTExLTg1MTUtNGY0OS05MDU2LTdmM2E5NzJmNzU4MSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCJdLCJleHAiOjE2OTU3NzA2MjgsImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTZmNDI2ZDhhLTA5M2UtNDQ1NS1hZThkLTlkMDg5MTZhMGI2NSIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImV4cGlyZXNfYXQiOiIyMDIzLTEwLTI2VDIzOjE4OjQ4WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6InBhc3N3b3JkIiwiZGVsaXZlcnlfbWV0aG9kIjoia25vd2xlZGdlIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wOS0yNlQyMzoxODo0OFoifV19LCJpYXQiOjE2OTU3NzAzMjgsImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCIsIm5iZiI6MTY5NTc3MDMyOCwic3ViIjoidXNlci10ZXN0LTUxODUyNjQyLWE1OTQtNGE2Zi1iNzZmLTkyODUxNzI1YTQyOSIsInRyYW5zYWN0aW9uX2hhc2giOiIweDEyMzQ1Njc4OTAifQ.ToSTvPFAaFP-eIqwWSBp0z7iotclWoNkghlrecU34kAoxloEqLvooXI7Ws_-HKy1rhTWhPWOtfh4QoxObV-39pe44xPFCoN2Vv0MiutMKJSaeIC5eVHxvSz0b2jjw0WkiPj8dK8HdzscNajvMATQ9R97U_i3rluTMnvliTw0zGUUsrMfTaHcltATUJ6Ufthxvb9w2XTTsIsBx0Ttldbf0XE_ZQnFk2uNW9Skyq0-zlZZXBorEbIbbAVeA87T_4CPqp9Pdc2qXRj9XrFtXuTD1lnXk9d28tu8l4H-4CWb8DYXZrFqb9-knavNXRsKb2NJnAcTh5c_I9RvR9lVxJtkWg"; - let encoded_hash = "0x1234567890"; - let hash_bytes = general_purpose::STANDARD.decode(encoded_hash).unwrap(); + let encoded_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWI5OGFjMTExLTg1MTUtNGY0OS05MDU2LTdmM2E5NzJmNzU4MSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCJdLCJleHAiOjE2OTU4NDgxNjksImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTk1MDUyMzJkLTczNjUtNDExZC1hYzBlLTc3MWM2YWY2MmU3NCIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA5LTI3VDIwOjUxOjA5WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA5LTI3VDIwOjUxOjA5WiIsImV4cGlyZXNfYXQiOiIyMDIzLTEwLTI3VDIwOjUxOjA5WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6Im90cCIsImRlbGl2ZXJ5X21ldGhvZCI6ImVtYWlsIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wOS0yN1QyMDo1MTowOVoiLCJlbWFpbF9mYWN0b3IiOnsiZW1haWxfaWQiOiJlbWFpbC10ZXN0LWY1ODU1MDU1LTc2OWMtNGVhMC04YzZhLTFmMTUyNDgzNWJmMiIsImVtYWlsX2FkZHJlc3MiOiJmaWxhbWVudCsxNjk1ODQ3ODUyMDY1QGJ1cm50LmNvbSJ9fV19LCJpYXQiOjE2OTU4NDc4NjksImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCIsIm5iZiI6MTY5NTg0Nzg2OSwic3ViIjoidXNlci10ZXN0LWY1MmRkZWQyLWM5M2EtNGVmZC05MTY5LTU3N2ZiZGY2Y2I4NiIsInRyYW5zYWN0aW9uX2hhc2giOiIweDEyMzQ1Njc4OTBfMyJ9.SkoLgjViIkP2kVcgz4fqA1CEWKbkN40behhs_ph-uCIAWNakCC_6FEfvcYgxBp1idk2IPRRZir-QAK_XV8ov_RHZuCqc8qd_bzlAwXV7cElHDf8oQxs44kA_P81QExoCABa3_ZzJ8KUNwzY0NbFxI3oJKDOYGxapi5aY5xsuGO3wUJX4PlsJ5xQhn254THgxpstqXEj56K1bcuDw_y-TQiNnP9R3vLZfGWj6BkZjB8dfgMp6FMRq_tBmo4l37pVHIA8v3-tlYSNoV7Z1P2uLsuxrM3_eh5zJ48vogwiOYAC2Ih90Pp2mF0leKOEXC4feTl_2oPIvGdXqbxgAmGKjpA"; + let encoded_hash = "0x1234567890_3"; + let hash_bytes = encoded_hash.as_bytes().to_vec(); let verification = verify( - &Timestamp::from_seconds(1695770329), + &Timestamp::from_seconds(1695847870), &hash_bytes, encoded_token.as_bytes(), &"project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94".to_string(), - &"user-test-51852642-a594-4a6f-b76f-92851725a429".to_string(), + &"user-test-f52dded2-c93a-4efd-9169-577fbdf6cb86".to_string(), ); assert!(verification.unwrap()); } diff --git a/account/src/error.rs b/account/src/error.rs index 6ead30e..3b575c0 100644 --- a/account/src/error.rs +++ b/account/src/error.rs @@ -15,15 +15,15 @@ pub enum ContractError { #[error(transparent)] Bech32(#[from] bech32::Error), - #[error(transparent)] - JsonWebToken(#[from] jsonwebtoken::errors::Error), - #[error(transparent)] UTF8Error(#[from] std::str::Utf8Error), #[error(transparent)] Base64Decode(#[from] base64::DecodeError), + #[error(transparent)] + Rsa(#[from] rsa::Error), + #[error("signature is invalid")] InvalidSignature, @@ -50,6 +50,9 @@ pub enum ContractError { #[error("invalid jwt aud")] InvalidJWTAud, + + #[error("invalid token")] + InvalidToken, } pub type ContractResult = Result; From ddfead34ea6c727ea7c75792d0a0b8f0319551d4 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 2 Oct 2023 23:11:13 +0300 Subject: [PATCH 06/20] disable random --- Cargo.toml | 4 ++-- README.md | 2 +- account/Cargo.toml | 3 ++- account/src/lib.rs | 11 +++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2619377..9cccf3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,5 +24,5 @@ schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" base64 = "0.21.4" -phf = { version = "0.11.2", features = ["macros"]} -rsa = "0.9.2" \ No newline at end of file +phf = { version = "0.11.2", features = ["macros"] } +rsa = { version = "0.9.2" } diff --git a/README.md b/README.md index 800d9ee..8ec074e 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,5 @@ docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/workspace-optimizer:0.13.0 + cosmwasm/workspace-optimizer:0.14.0 ``` \ No newline at end of file diff --git a/account/Cargo.toml b/account/Cargo.toml index 2681142..4595d26 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -24,4 +24,5 @@ ripemd = { workspace = true } bech32 = { workspace = true } base64 = { workspace = true } phf = { workspace = true } -rsa = { workspace = true } \ No newline at end of file +rsa = { workspace = true } +getrandom = { version = "0.2.10", features = ["custom"] } \ No newline at end of file diff --git a/account/src/lib.rs b/account/src/lib.rs index 0da95b0..4ddb9d0 100644 --- a/account/src/lib.rs +++ b/account/src/lib.rs @@ -11,3 +11,14 @@ pub mod state; pub const CONTRACT_NAME: &str = "account"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// the random function must be disabled in cosmwasm +use core::num::NonZeroU32; +use getrandom::Error; + +pub fn always_fail(_buf: &mut [u8]) -> Result<(), Error> { + let code = NonZeroU32::new(Error::CUSTOM_START).unwrap(); + Err(Error::from(code)) +} +use getrandom::register_custom_getrandom; +register_custom_getrandom!(always_fail); From c263ffd1ae5d04b2f2ae9da74d81ab68a3f0fbaf Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 3 Oct 2023 11:06:51 +0300 Subject: [PATCH 07/20] cleanup deps --- Cargo.toml | 1 + account/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9cccf3b..50fdc0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ bech32 = "0.9.1" base64 = "0.21.4" phf = { version = "0.11.2", features = ["macros"] } rsa = { version = "0.9.2" } +getrandom = { version = "0.2.10", features = ["custom"] } diff --git a/account/Cargo.toml b/account/Cargo.toml index 4595d26..66f49bf 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -25,4 +25,4 @@ bech32 = { workspace = true } base64 = { workspace = true } phf = { workspace = true } rsa = { workspace = true } -getrandom = { version = "0.2.10", features = ["custom"] } \ No newline at end of file +getrandom = { workspace = true} \ No newline at end of file From f95bb4aa50466f8947a664e66a688846e6dcac5c Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 27 Sep 2023 22:44:03 +0100 Subject: [PATCH 08/20] jwt verification via contract --- Cargo.toml | 4 +- account/Cargo.toml | 6 ++- account/src/auth.rs | 21 +++++++- account/src/auth/jwt.rs | 106 ++++++++++++++++++++++++++++++++++++++++ account/src/contract.rs | 10 +++- account/src/error.rs | 15 ++++++ account/src/execute.rs | 32 +++++++++++- 7 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 account/src/auth/jwt.rs diff --git a/Cargo.toml b/Cargo.toml index 6001e26..1e83301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,6 @@ serde_json = "1.0.87" schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" -base64 = "0.21.4" \ No newline at end of file +base64 = "0.21.4" +jsonwebtoken = "8.3.0" +phf = { version = "0.11.2", features = ["macros"]} \ No newline at end of file diff --git a/account/Cargo.toml b/account/Cargo.toml index a4683ad..7b49ed2 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -12,7 +12,7 @@ absacc = { git = "https://github.com/larry0x/abstract-account.git" } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } -cw-storage-plus = { workspace = true } +cw-storage-plus = { workspace = true }q sha2 = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } @@ -22,4 +22,6 @@ schemars = { workspace = true } hex = { workspace = true } ripemd = { workspace = true } bech32 = { workspace = true } -base64 = { workspace = true } \ No newline at end of file +base64 = { workspace = true } +jsonwebtoken = { workspace = true } +phf = { workspace = true } \ No newline at end of file diff --git a/account/src/auth.rs b/account/src/auth.rs index 954d318..6552c08 100644 --- a/account/src/auth.rs +++ b/account/src/auth.rs @@ -1,9 +1,10 @@ use crate::error::ContractError; -use cosmwasm_std::{Api, Binary}; +use cosmwasm_std::{Api, Binary, Env}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; mod eth_crypto; +mod jwt; mod sign_arb; pub mod util; @@ -24,6 +25,12 @@ pub enum AddAuthenticator { address: String, signature: Binary, }, + JWT { + id: u8, + aud: String, + sub: String, + token: Binary, + }, } #[derive(Serialize, Deserialize, Clone, JsonSchema, PartialEq, Debug)] @@ -31,12 +38,14 @@ pub enum Authenticator { Secp256K1 { pubkey: Binary }, Ed25519 { pubkey: Binary }, EthWallet { address: String }, + JWT { aud: String, sub: String }, } impl Authenticator { pub fn verify( &self, api: &dyn Api, + env: &Env, tx_bytes: &Binary, sig_bytes: &Binary, ) -> Result { @@ -74,6 +83,16 @@ impl Authenticator { Err(error) => Err(error), } } + Authenticator::JWT { aud, sub } => { + let tx_bytes_hash = util::sha256(tx_bytes); + return jwt::verify( + &env.block.time, + &tx_bytes_hash, + sig_bytes.as_slice(), + aud, + sub, + ); + } } } } diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs new file mode 100644 index 0000000..a4b934b --- /dev/null +++ b/account/src/auth/jwt.rs @@ -0,0 +1,106 @@ +use crate::error::ContractError::{InvalidJWTAud, InvalidTime}; +use crate::error::ContractResult; +use base64::{engine::general_purpose, Engine as _}; +use cosmwasm_std::Timestamp; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use phf::{phf_map, Map}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::str; + +static AUD_KEY_MAP: Map<&'static str, &'static str> = phf_map! { + "project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94" => "sQKkA829tzjU2VA-INHvdrewkbQzjpsMn0PNM7KJaBODbB4ItZM4x1NVSWBiy2DGHkaDDvADRbbq1BZsC1iXVtIYm0AoD7x4QC1w89kp2_s0wmvUOSPiQZlYrgJqRDXirXJZX3MNku2McXbwdyPajDaR4nBBQOoUOF21CHqLDqBHs2R6tHyL80R_8mgueiqQ-4wg6SSVcB_6ZOh59vRcjKr34upKPWGQzvMGCkeTO9whzbIWbA1j-8ykiS63EhjWBZU_sSolsf1ZGq8peVrADDLhOvHtZxCZLKwB46k2kb8GKAWlO4wRP6BDVjzpnea7BsvZ6JwULKg3HisH9gzaiQ;AQAB", +}; + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + aud: Box<[String]>, // Optional. Audience + exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) + iat: usize, // Optional. Issued at (as UTC timestamp) + iss: String, // Optional. Issuer + nbf: usize, // Optional. Not Before (as UTC timestamp) + sub: String, // Optional. Subject (whom token refers to) + + transaction_hash: String, +} + +pub fn verify( + current_time: &Timestamp, + tx_hash: &Vec, + sig_bytes: &[u8], + aud: &String, + sub: &String, +) -> ContractResult { + if !AUD_KEY_MAP.contains_key(aud.as_str()) { + return Err(InvalidJWTAud); + } + + let key = match AUD_KEY_MAP.get(aud.as_str()) { + None => return Err(InvalidJWTAud), + Some(k) => *k, + }; + + let token = str::from_utf8(sig_bytes)?; + + // currently only RS256 is supported + let mut options = Validation::new(Algorithm::RS256); + options.required_spec_claims = HashSet::from([ + "sub".to_string(), + "aud".to_string(), + "exp".to_string(), + "nbf".to_string(), + "iat".to_string(), + "iss".to_string(), + "transaction_hash".to_string(), + ]); + + // make sure the sub and aud ids are as expected + options.sub = Option::from(sub.clone()); + options.aud = Option::from(HashSet::from([aud.clone()])); + + // disable time checks because system time is not available, will pull directly from BlockInfo + options.validate_exp = false; + options.validate_nbf = false; + + let mut key_split = key.split(';'); + let modulus = key_split.next().ok_or(InvalidJWTAud)?; + let exponent = key_split.next().ok_or(InvalidJWTAud)?; + let decoding_key = DecodingKey::from_rsa_components(modulus, exponent)?; + let token = decode::(&token, &decoding_key, &options)?; + + // complete the time checks + let expiration = Timestamp::from_seconds(token.claims.exp as u64); + if expiration.lt(current_time) { + return Err(InvalidTime); + } + let not_before = Timestamp::from_seconds(token.claims.nbf as u64); + if not_before.gt(current_time) { + return Err(InvalidTime); + } + + // make sure the provided hash matches the one from the tx + let hash_bytes = general_purpose::STANDARD.decode(token.claims.transaction_hash)?; + Ok(tx_hash.eq(&hash_bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::engine::general_purpose; + + #[test] + fn test_validate_token() { + let encoded_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWI5OGFjMTExLTg1MTUtNGY0OS05MDU2LTdmM2E5NzJmNzU4MSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCJdLCJleHAiOjE2OTU3NzA2MjgsImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTZmNDI2ZDhhLTA5M2UtNDQ1NS1hZThkLTlkMDg5MTZhMGI2NSIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImV4cGlyZXNfYXQiOiIyMDIzLTEwLTI2VDIzOjE4OjQ4WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6InBhc3N3b3JkIiwiZGVsaXZlcnlfbWV0aG9kIjoia25vd2xlZGdlIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wOS0yNlQyMzoxODo0OFoifV19LCJpYXQiOjE2OTU3NzAzMjgsImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCIsIm5iZiI6MTY5NTc3MDMyOCwic3ViIjoidXNlci10ZXN0LTUxODUyNjQyLWE1OTQtNGE2Zi1iNzZmLTkyODUxNzI1YTQyOSIsInRyYW5zYWN0aW9uX2hhc2giOiIweDEyMzQ1Njc4OTAifQ.ToSTvPFAaFP-eIqwWSBp0z7iotclWoNkghlrecU34kAoxloEqLvooXI7Ws_-HKy1rhTWhPWOtfh4QoxObV-39pe44xPFCoN2Vv0MiutMKJSaeIC5eVHxvSz0b2jjw0WkiPj8dK8HdzscNajvMATQ9R97U_i3rluTMnvliTw0zGUUsrMfTaHcltATUJ6Ufthxvb9w2XTTsIsBx0Ttldbf0XE_ZQnFk2uNW9Skyq0-zlZZXBorEbIbbAVeA87T_4CPqp9Pdc2qXRj9XrFtXuTD1lnXk9d28tu8l4H-4CWb8DYXZrFqb9-knavNXRsKb2NJnAcTh5c_I9RvR9lVxJtkWg"; + let encoded_hash = "0x1234567890"; + let hash_bytes = general_purpose::STANDARD.decode(encoded_hash).unwrap(); + + let verification = verify( + &Timestamp::from_seconds(1695770329), + &hash_bytes, + encoded_token.as_bytes(), + &"project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94".to_string(), + &"user-test-51852642-a594-4a6f-b76f-92851725a429".to_string(), + ); + assert!(verification.unwrap()); + } +} diff --git a/account/src/contract.rs b/account/src/contract.rs index 0051b17..003c90d 100644 --- a/account/src/contract.rs +++ b/account/src/contract.rs @@ -25,14 +25,20 @@ pub fn instantiate( } #[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, cred_bytes, simulate, .. - } => execute::before_tx(deps.as_ref(), &tx_bytes, cred_bytes.as_ref(), simulate), + } => execute::before_tx( + deps.as_ref(), + &env, + &tx_bytes, + cred_bytes.as_ref(), + simulate, + ), AccountSudoMsg::AfterTx { .. } => execute::after_tx(), } } diff --git a/account/src/error.rs b/account/src/error.rs index f10fa10..6ead30e 100644 --- a/account/src/error.rs +++ b/account/src/error.rs @@ -15,6 +15,15 @@ pub enum ContractError { #[error(transparent)] Bech32(#[from] bech32::Error), + #[error(transparent)] + JsonWebToken(#[from] jsonwebtoken::errors::Error), + + #[error(transparent)] + UTF8Error(#[from] std::str::Utf8Error), + + #[error(transparent)] + Base64Decode(#[from] base64::DecodeError), + #[error("signature is invalid")] InvalidSignature, @@ -35,6 +44,12 @@ pub enum ContractError { #[error("cannot delete the last authenticator")] MinimumAuthenticatorCount, + + #[error("invalid time on signature")] + InvalidTime, + + #[error("invalid jwt aud")] + InvalidJWTAud, } pub type ContractResult = Result; diff --git a/account/src/execute.rs b/account/src/execute.rs index 78d6c06..4b44584 100644 --- a/account/src/execute.rs +++ b/account/src/execute.rs @@ -16,6 +16,7 @@ pub fn init( ) -> ContractResult { if !authenticator.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), signature, )? { @@ -31,6 +32,7 @@ pub fn init( pub fn before_tx( deps: Deps, + env: &Env, tx_bytes: &Binary, cred_bytes: Option<&Binary>, simulate: bool, @@ -66,9 +68,12 @@ pub fn before_tx( return Err(ContractError::ShortSignature); } } + Authenticator::JWT { .. } => { + // todo: figure out if there are minimum checks for JWTs + } } - return match authenticator.verify(deps.api, tx_bytes, sig_bytes)? { + return match authenticator.verify(deps.api, env, tx_bytes, sig_bytes)? { true => Ok(Response::new().add_attribute("method", "before_tx")), false => Err(ContractError::InvalidSignature), }; @@ -98,6 +103,7 @@ pub fn add_auth_method( if !auth.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), &signature, )? { @@ -118,6 +124,7 @@ pub fn add_auth_method( if !auth.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), &signature, )? { @@ -138,6 +145,7 @@ pub fn add_auth_method( if !auth.verify( deps.api, + &env, &Binary::from(env.contract.address.as_bytes()), &signature, )? { @@ -149,6 +157,28 @@ pub fn add_auth_method( .add_attribute("authenticator_id", id.to_string())) } } + AddAuthenticator::JWT { + id, + aud, + sub, + token, + } => { + let auth = Authenticator::JWT { aud, sub }; + + if !auth.verify( + deps.api, + &env, + &Binary::from(env.contract.address.as_bytes()), + &token, + )? { + Err(ContractError::InvalidSignature) + } else { + AUTHENTICATORS.save(deps.storage, id, &auth)?; + Ok(Response::new() + .add_attribute("method", "execute") + .add_attribute("authenticator_id", id.to_string())) + } + } } } From 936a655b7db21d18ca99921599953d49f2942efa Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 27 Sep 2023 22:46:07 +0100 Subject: [PATCH 09/20] typo --- account/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/Cargo.toml b/account/Cargo.toml index 7b49ed2..0861a85 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -12,7 +12,7 @@ absacc = { git = "https://github.com/larry0x/abstract-account.git" } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } -cw-storage-plus = { workspace = true }q +cw-storage-plus = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } From 6dbcbecd36d3a9b5a04354a751239ec8af8896bd Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 27 Sep 2023 22:53:19 +0100 Subject: [PATCH 10/20] lints --- account/src/auth.rs | 6 +++--- account/src/auth/jwt.rs | 14 +++++++------- account/src/execute.rs | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/account/src/auth.rs b/account/src/auth.rs index 6552c08..402938c 100644 --- a/account/src/auth.rs +++ b/account/src/auth.rs @@ -25,7 +25,7 @@ pub enum AddAuthenticator { address: String, signature: Binary, }, - JWT { + Jwt { id: u8, aud: String, sub: String, @@ -38,7 +38,7 @@ pub enum Authenticator { Secp256K1 { pubkey: Binary }, Ed25519 { pubkey: Binary }, EthWallet { address: String }, - JWT { aud: String, sub: String }, + Jwt { aud: String, sub: String }, } impl Authenticator { @@ -83,7 +83,7 @@ impl Authenticator { Err(error) => Err(error), } } - Authenticator::JWT { aud, sub } => { + Authenticator::Jwt { aud, sub } => { let tx_bytes_hash = util::sha256(tx_bytes); return jwt::verify( &env.block.time, diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index a4b934b..b92ab8b 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -28,14 +28,14 @@ pub fn verify( current_time: &Timestamp, tx_hash: &Vec, sig_bytes: &[u8], - aud: &String, - sub: &String, + aud: &str, + sub: &str, ) -> ContractResult { - if !AUD_KEY_MAP.contains_key(aud.as_str()) { + if !AUD_KEY_MAP.contains_key(aud) { return Err(InvalidJWTAud); } - let key = match AUD_KEY_MAP.get(aud.as_str()) { + let key = match AUD_KEY_MAP.get(aud) { None => return Err(InvalidJWTAud), Some(k) => *k, }; @@ -55,8 +55,8 @@ pub fn verify( ]); // make sure the sub and aud ids are as expected - options.sub = Option::from(sub.clone()); - options.aud = Option::from(HashSet::from([aud.clone()])); + options.sub = Option::from(sub.to_string()); + options.aud = Option::from(HashSet::from([aud.to_string()])); // disable time checks because system time is not available, will pull directly from BlockInfo options.validate_exp = false; @@ -66,7 +66,7 @@ pub fn verify( let modulus = key_split.next().ok_or(InvalidJWTAud)?; let exponent = key_split.next().ok_or(InvalidJWTAud)?; let decoding_key = DecodingKey::from_rsa_components(modulus, exponent)?; - let token = decode::(&token, &decoding_key, &options)?; + let token = decode::(token, &decoding_key, &options)?; // complete the time checks let expiration = Timestamp::from_seconds(token.claims.exp as u64); diff --git a/account/src/execute.rs b/account/src/execute.rs index 4b44584..2dbc73e 100644 --- a/account/src/execute.rs +++ b/account/src/execute.rs @@ -68,7 +68,7 @@ pub fn before_tx( return Err(ContractError::ShortSignature); } } - Authenticator::JWT { .. } => { + Authenticator::Jwt { .. } => { // todo: figure out if there are minimum checks for JWTs } } @@ -157,13 +157,13 @@ pub fn add_auth_method( .add_attribute("authenticator_id", id.to_string())) } } - AddAuthenticator::JWT { + AddAuthenticator::Jwt { id, aud, sub, token, } => { - let auth = Authenticator::JWT { aud, sub }; + let auth = Authenticator::Jwt { aud, sub }; if !auth.verify( deps.api, From db41c8fa5b57932236a7c649bb5a3c72e536657c Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 28 Sep 2023 22:48:26 +0100 Subject: [PATCH 11/20] disable pem, trying to avoid the clib issue --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1e83301..1d0400f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,5 @@ schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" base64 = "0.21.4" -jsonwebtoken = "8.3.0" +jsonwebtoken = {version = "8.3.0", default-features = false } phf = { version = "0.11.2", features = ["macros"]} \ No newline at end of file From 8a5a853a23c2f376904c95e5eba073432920f72a Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 1 Oct 2023 12:48:17 +0100 Subject: [PATCH 12/20] rsa direct jwt check --- Cargo.toml | 9 +++-- README.md | 1 - account/Cargo.toml | 4 +- account/src/auth/jwt.rs | 88 +++++++++++++++++++++++------------------ account/src/error.rs | 9 +++-- 5 files changed, 64 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d0400f..2619377 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ members = [ "account" ] +[build] +target = "wasm32-unknown-unknown" + [workspace.dependencies] cosmos-sdk-proto = { version = "0.19", default-features = false } cosmwasm-schema = "1.2" @@ -12,7 +15,7 @@ cw-storage-plus = "1" cw-utils = "1" hex = "0.4" prost = "0.11" -sha2 = "0.10" +sha2 = { version = "0.10.8", features = ["oid"]} thiserror = "1" tiny-keccak = { version = "2", features = ["keccak"] } serde = { version = "1.0.145", default-features = false, features = ["derive"] } @@ -21,5 +24,5 @@ schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" base64 = "0.21.4" -jsonwebtoken = {version = "8.3.0", default-features = false } -phf = { version = "0.11.2", features = ["macros"]} \ No newline at end of file +phf = { version = "0.11.2", features = ["macros"]} +rsa = "0.9.2" \ No newline at end of file diff --git a/README.md b/README.md index 54cb3ed..800d9ee 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,4 @@ docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ cosmwasm/workspace-optimizer:0.13.0 - ``` \ No newline at end of file diff --git a/account/Cargo.toml b/account/Cargo.toml index 0861a85..2681142 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -23,5 +23,5 @@ hex = { workspace = true } ripemd = { workspace = true } bech32 = { workspace = true } base64 = { workspace = true } -jsonwebtoken = { workspace = true } -phf = { workspace = true } \ No newline at end of file +phf = { workspace = true } +rsa = { workspace = true } \ No newline at end of file diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index b92ab8b..ca309c5 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -1,11 +1,13 @@ -use crate::error::ContractError::{InvalidJWTAud, InvalidTime}; +use crate::error::ContractError::{InvalidJWTAud, InvalidTime, InvalidToken}; use crate::error::ContractResult; -use base64::{engine::general_purpose, Engine as _}; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine as _; use cosmwasm_std::Timestamp; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use phf::{phf_map, Map}; +use rsa::traits::SignatureScheme; +use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey}; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use sha2::{Digest, Sha256}; use std::str; static AUD_KEY_MAP: Map<&'static str, &'static str> = phf_map! { @@ -40,66 +42,76 @@ pub fn verify( Some(k) => *k, }; - let token = str::from_utf8(sig_bytes)?; - - // currently only RS256 is supported - let mut options = Validation::new(Algorithm::RS256); - options.required_spec_claims = HashSet::from([ - "sub".to_string(), - "aud".to_string(), - "exp".to_string(), - "nbf".to_string(), - "iat".to_string(), - "iss".to_string(), - "transaction_hash".to_string(), - ]); - - // make sure the sub and aud ids are as expected - options.sub = Option::from(sub.to_string()); - options.aud = Option::from(HashSet::from([aud.to_string()])); - - // disable time checks because system time is not available, will pull directly from BlockInfo - options.validate_exp = false; - options.validate_nbf = false; + // prepare the components of the token for verification + let mut components = sig_bytes.split(|&b| b == b'.'); + let header_bytes = components.next().ok_or(InvalidToken)?; // ignore the header, it is not currently used + let payload_bytes = components.next().ok_or(InvalidToken)?; + let digest_bytes = [header_bytes, &[b'.'], payload_bytes].concat(); + let signature_bytes = components.next().ok_or(InvalidToken)?; + let signature = URL_SAFE_NO_PAD.decode(signature_bytes)?; + // retrieve and rebuild the pubkey let mut key_split = key.split(';'); let modulus = key_split.next().ok_or(InvalidJWTAud)?; + let mod_bytes = URL_SAFE_NO_PAD.decode(modulus)?; let exponent = key_split.next().ok_or(InvalidJWTAud)?; - let decoding_key = DecodingKey::from_rsa_components(modulus, exponent)?; - let token = decode::(token, &decoding_key, &options)?; + let exp_bytes = URL_SAFE_NO_PAD.decode(exponent)?; + let pubkey = RsaPublicKey::new( + BigUint::from_bytes_be(mod_bytes.as_slice()), + BigUint::from_bytes_be(exp_bytes.as_slice()), + )?; + + // hash the message body before verification + let mut hasher = Sha256::new(); + hasher.update(digest_bytes); + let digest = hasher.finalize().as_slice().to_vec(); + + // verify the signature + let scheme = Pkcs1v15Sign::new::(); + scheme.verify(&pubkey, digest.as_slice(), signature.as_slice())?; + + // at this point, we have verified that the token is legitimately signed. + // now we perform logic checks on the body + let payload = URL_SAFE_NO_PAD.decode(payload_bytes)?; + let claims: Claims = cosmwasm_std::from_slice(payload.as_slice())?; + if !claims.sub.eq_ignore_ascii_case(sub) { + // this token was not for the supplied sub + return Err(InvalidToken); + } + if !claims.aud.contains(&aud.to_string()) { + // this token was for a different aud + return Err(InvalidToken); + } // complete the time checks - let expiration = Timestamp::from_seconds(token.claims.exp as u64); + let expiration = Timestamp::from_seconds(claims.exp as u64); if expiration.lt(current_time) { return Err(InvalidTime); } - let not_before = Timestamp::from_seconds(token.claims.nbf as u64); + let not_before = Timestamp::from_seconds(claims.nbf as u64); if not_before.gt(current_time) { return Err(InvalidTime); } - // make sure the provided hash matches the one from the tx - let hash_bytes = general_purpose::STANDARD.decode(token.claims.transaction_hash)?; - Ok(tx_hash.eq(&hash_bytes)) + Ok(tx_hash.eq(claims.transaction_hash.as_bytes())) } #[cfg(test)] mod tests { use super::*; - use base64::engine::general_purpose; #[test] fn test_validate_token() { - let encoded_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWI5OGFjMTExLTg1MTUtNGY0OS05MDU2LTdmM2E5NzJmNzU4MSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCJdLCJleHAiOjE2OTU3NzA2MjgsImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTZmNDI2ZDhhLTA5M2UtNDQ1NS1hZThkLTlkMDg5MTZhMGI2NSIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA5LTI2VDIzOjE4OjQ4WiIsImV4cGlyZXNfYXQiOiIyMDIzLTEwLTI2VDIzOjE4OjQ4WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6InBhc3N3b3JkIiwiZGVsaXZlcnlfbWV0aG9kIjoia25vd2xlZGdlIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wOS0yNlQyMzoxODo0OFoifV19LCJpYXQiOjE2OTU3NzAzMjgsImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCIsIm5iZiI6MTY5NTc3MDMyOCwic3ViIjoidXNlci10ZXN0LTUxODUyNjQyLWE1OTQtNGE2Zi1iNzZmLTkyODUxNzI1YTQyOSIsInRyYW5zYWN0aW9uX2hhc2giOiIweDEyMzQ1Njc4OTAifQ.ToSTvPFAaFP-eIqwWSBp0z7iotclWoNkghlrecU34kAoxloEqLvooXI7Ws_-HKy1rhTWhPWOtfh4QoxObV-39pe44xPFCoN2Vv0MiutMKJSaeIC5eVHxvSz0b2jjw0WkiPj8dK8HdzscNajvMATQ9R97U_i3rluTMnvliTw0zGUUsrMfTaHcltATUJ6Ufthxvb9w2XTTsIsBx0Ttldbf0XE_ZQnFk2uNW9Skyq0-zlZZXBorEbIbbAVeA87T_4CPqp9Pdc2qXRj9XrFtXuTD1lnXk9d28tu8l4H-4CWb8DYXZrFqb9-knavNXRsKb2NJnAcTh5c_I9RvR9lVxJtkWg"; - let encoded_hash = "0x1234567890"; - let hash_bytes = general_purpose::STANDARD.decode(encoded_hash).unwrap(); + let encoded_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWI5OGFjMTExLTg1MTUtNGY0OS05MDU2LTdmM2E5NzJmNzU4MSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCJdLCJleHAiOjE2OTU4NDgxNjksImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTk1MDUyMzJkLTczNjUtNDExZC1hYzBlLTc3MWM2YWY2MmU3NCIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA5LTI3VDIwOjUxOjA5WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA5LTI3VDIwOjUxOjA5WiIsImV4cGlyZXNfYXQiOiIyMDIzLTEwLTI3VDIwOjUxOjA5WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6Im90cCIsImRlbGl2ZXJ5X21ldGhvZCI6ImVtYWlsIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wOS0yN1QyMDo1MTowOVoiLCJlbWFpbF9mYWN0b3IiOnsiZW1haWxfaWQiOiJlbWFpbC10ZXN0LWY1ODU1MDU1LTc2OWMtNGVhMC04YzZhLTFmMTUyNDgzNWJmMiIsImVtYWlsX2FkZHJlc3MiOiJmaWxhbWVudCsxNjk1ODQ3ODUyMDY1QGJ1cm50LmNvbSJ9fV19LCJpYXQiOjE2OTU4NDc4NjksImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCIsIm5iZiI6MTY5NTg0Nzg2OSwic3ViIjoidXNlci10ZXN0LWY1MmRkZWQyLWM5M2EtNGVmZC05MTY5LTU3N2ZiZGY2Y2I4NiIsInRyYW5zYWN0aW9uX2hhc2giOiIweDEyMzQ1Njc4OTBfMyJ9.SkoLgjViIkP2kVcgz4fqA1CEWKbkN40behhs_ph-uCIAWNakCC_6FEfvcYgxBp1idk2IPRRZir-QAK_XV8ov_RHZuCqc8qd_bzlAwXV7cElHDf8oQxs44kA_P81QExoCABa3_ZzJ8KUNwzY0NbFxI3oJKDOYGxapi5aY5xsuGO3wUJX4PlsJ5xQhn254THgxpstqXEj56K1bcuDw_y-TQiNnP9R3vLZfGWj6BkZjB8dfgMp6FMRq_tBmo4l37pVHIA8v3-tlYSNoV7Z1P2uLsuxrM3_eh5zJ48vogwiOYAC2Ih90Pp2mF0leKOEXC4feTl_2oPIvGdXqbxgAmGKjpA"; + let encoded_hash = "0x1234567890_3"; + let hash_bytes = encoded_hash.as_bytes().to_vec(); let verification = verify( - &Timestamp::from_seconds(1695770329), + &Timestamp::from_seconds(1695847870), &hash_bytes, encoded_token.as_bytes(), &"project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94".to_string(), - &"user-test-51852642-a594-4a6f-b76f-92851725a429".to_string(), + &"user-test-f52dded2-c93a-4efd-9169-577fbdf6cb86".to_string(), ); assert!(verification.unwrap()); } diff --git a/account/src/error.rs b/account/src/error.rs index 6ead30e..3b575c0 100644 --- a/account/src/error.rs +++ b/account/src/error.rs @@ -15,15 +15,15 @@ pub enum ContractError { #[error(transparent)] Bech32(#[from] bech32::Error), - #[error(transparent)] - JsonWebToken(#[from] jsonwebtoken::errors::Error), - #[error(transparent)] UTF8Error(#[from] std::str::Utf8Error), #[error(transparent)] Base64Decode(#[from] base64::DecodeError), + #[error(transparent)] + Rsa(#[from] rsa::Error), + #[error("signature is invalid")] InvalidSignature, @@ -50,6 +50,9 @@ pub enum ContractError { #[error("invalid jwt aud")] InvalidJWTAud, + + #[error("invalid token")] + InvalidToken, } pub type ContractResult = Result; From 06c5338d814785b644fd27b00bf9e9c53f297a55 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 2 Oct 2023 23:11:13 +0300 Subject: [PATCH 13/20] disable random --- Cargo.toml | 4 ++-- README.md | 2 +- account/Cargo.toml | 3 ++- account/src/lib.rs | 11 +++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2619377..9cccf3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,5 +24,5 @@ schemars = "0.8.10" ripemd = "0.1.3" bech32 = "0.9.1" base64 = "0.21.4" -phf = { version = "0.11.2", features = ["macros"]} -rsa = "0.9.2" \ No newline at end of file +phf = { version = "0.11.2", features = ["macros"] } +rsa = { version = "0.9.2" } diff --git a/README.md b/README.md index 800d9ee..8ec074e 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,5 @@ docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/workspace-optimizer:0.13.0 + cosmwasm/workspace-optimizer:0.14.0 ``` \ No newline at end of file diff --git a/account/Cargo.toml b/account/Cargo.toml index 2681142..4595d26 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -24,4 +24,5 @@ ripemd = { workspace = true } bech32 = { workspace = true } base64 = { workspace = true } phf = { workspace = true } -rsa = { workspace = true } \ No newline at end of file +rsa = { workspace = true } +getrandom = { version = "0.2.10", features = ["custom"] } \ No newline at end of file diff --git a/account/src/lib.rs b/account/src/lib.rs index 0da95b0..4ddb9d0 100644 --- a/account/src/lib.rs +++ b/account/src/lib.rs @@ -11,3 +11,14 @@ pub mod state; pub const CONTRACT_NAME: &str = "account"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// the random function must be disabled in cosmwasm +use core::num::NonZeroU32; +use getrandom::Error; + +pub fn always_fail(_buf: &mut [u8]) -> Result<(), Error> { + let code = NonZeroU32::new(Error::CUSTOM_START).unwrap(); + Err(Error::from(code)) +} +use getrandom::register_custom_getrandom; +register_custom_getrandom!(always_fail); From 9c70757d759e4429b0632a5196a98c4d5931da3f Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 3 Oct 2023 11:06:51 +0300 Subject: [PATCH 14/20] cleanup deps --- Cargo.toml | 1 + account/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9cccf3b..50fdc0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ bech32 = "0.9.1" base64 = "0.21.4" phf = { version = "0.11.2", features = ["macros"] } rsa = { version = "0.9.2" } +getrandom = { version = "0.2.10", features = ["custom"] } diff --git a/account/Cargo.toml b/account/Cargo.toml index 4595d26..66f49bf 100644 --- a/account/Cargo.toml +++ b/account/Cargo.toml @@ -25,4 +25,4 @@ bech32 = { workspace = true } base64 = { workspace = true } phf = { workspace = true } rsa = { workspace = true } -getrandom = { version = "0.2.10", features = ["custom"] } \ No newline at end of file +getrandom = { workspace = true} \ No newline at end of file From 4dfadc631ab9981aa39ea0ef316bee7f047e76fe Mon Sep 17 00:00:00 2001 From: Ash Date: Sun, 8 Oct 2023 12:58:26 -0700 Subject: [PATCH 15/20] remove 'usize' in serde types, it causes F64Load issues when deploying the contract --- account/src/auth/jwt.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index ca309c5..bb1a407 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -17,10 +17,10 @@ static AUD_KEY_MAP: Map<&'static str, &'static str> = phf_map! { #[derive(Debug, Serialize, Deserialize)] struct Claims { aud: Box<[String]>, // Optional. Audience - exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) - iat: usize, // Optional. Issued at (as UTC timestamp) + exp: u64, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) + iat: u64, // Optional. Issued at (as UTC timestamp) iss: String, // Optional. Issuer - nbf: usize, // Optional. Not Before (as UTC timestamp) + nbf: u64, // Optional. Not Before (as UTC timestamp) sub: String, // Optional. Subject (whom token refers to) transaction_hash: String, From 6251614710c65f12faacfa91851b44b2e470a361 Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 13 Oct 2023 17:05:35 -0700 Subject: [PATCH 16/20] more info in errors --- account/src/auth/jwt.rs | 26 ++++++++++++++++++++++++-- account/src/error.rs | 3 +++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index bb1a407..2eb8738 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -1,4 +1,6 @@ -use crate::error::ContractError::{InvalidJWTAud, InvalidTime, InvalidToken}; +use crate::error::ContractError::{ + InvalidJWTAud, InvalidSignatureDetail, InvalidTime, InvalidToken, +}; use crate::error::ContractResult; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine as _; @@ -12,6 +14,7 @@ use std::str; static AUD_KEY_MAP: Map<&'static str, &'static str> = phf_map! { "project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94" => "sQKkA829tzjU2VA-INHvdrewkbQzjpsMn0PNM7KJaBODbB4ItZM4x1NVSWBiy2DGHkaDDvADRbbq1BZsC1iXVtIYm0AoD7x4QC1w89kp2_s0wmvUOSPiQZlYrgJqRDXirXJZX3MNku2McXbwdyPajDaR4nBBQOoUOF21CHqLDqBHs2R6tHyL80R_8mgueiqQ-4wg6SSVcB_6ZOh59vRcjKr34upKPWGQzvMGCkeTO9whzbIWbA1j-8ykiS63EhjWBZU_sSolsf1ZGq8peVrADDLhOvHtZxCZLKwB46k2kb8GKAWlO4wRP6BDVjzpnea7BsvZ6JwULKg3HisH9gzaiQ;AQAB", + "integration-test-project" => "olg7TF3aai-wR4HTDe5oR-WRhEsdW3u-O3IJHl0BiHkmR4MLskHG9HzivWoXsloUBnBMrFNxOH0x5cNMI07oi4PeRbHySiogRW9CXPjJaNlTi-pT_IgKFsyJNXsLyzrnajLkDbQU6pRsHmNeL0hAOUv48rtXv8VVWWN8okJehD2q9N7LHoFAOmIUEPg_VTHTt8K__O-9eMZKN4eMjh_4-sxRX6NXPSPT87XRlrK4GZ4pUdp86K0tOFLhwO4Uj0JkMNfI82eVZ1tAbDlqjd8jFnAb8fWm8wtdaTNbL_AAXmbDhswwJOyrw8fARZIhrXSdKBWa6e4k7sLwTIy-OO8saebnlARsjGst7ZCzmw5KCm2ctEVl3hYhHwyXu_A5rOblMrV3H0G7WqeKMCMVSJ11ssrlsmfVhNIwu1Qlt5GYmPTTJiCgGUGRxZkgDyOyjFNHglYpZamCGyJ9oyofsukEGoqMQ6WzjFi_hjVapzXi7Li-Q0OjEopIUUDDgeUrgjbGY0eiHI6sAz5hoaD0Qjc9e3Hk6-y7VcKCTCAanZOlJV0vJkHB98LBLh9qAoVUei_VaLFe2IcfVlrL_43aXlsHhr_SUQY5pHPlUMbQihE_57dpPRh31qDX_w6ye8dilniP8JmpKM2uIwnJ0x7hfJ45Qa0oLHmrGlzY9wi-RGP0YUk;AQAB", }; #[derive(Debug, Serialize, Deserialize)] @@ -93,12 +96,20 @@ pub fn verify( return Err(InvalidTime); } // make sure the provided hash matches the one from the tx - Ok(tx_hash.eq(claims.transaction_hash.as_bytes())) + if tx_hash.eq(claims.transaction_hash.as_bytes()) { + Ok(true) + } else { + Err(InvalidSignatureDetail { + expected: URL_SAFE_NO_PAD.encode(tx_hash), + received: claims.transaction_hash, + }) + } } #[cfg(test)] mod tests { use super::*; + use crate::auth::Authenticator; #[test] fn test_validate_token() { @@ -115,4 +126,15 @@ mod tests { ); assert!(verification.unwrap()); } + + #[test] + fn test_dump_authenticator() { + let authenticator = Authenticator::Jwt { + aud: "project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94".to_string(), + sub: "user-test-f52dded2-c93a-4efd-9169-577fbdf6cb86".to_string(), + }; + let serialized = serde_json::to_string(&authenticator).unwrap(); + + println!("authenticator: {}", serialized); + } } diff --git a/account/src/error.rs b/account/src/error.rs index 3b575c0..05fe775 100644 --- a/account/src/error.rs +++ b/account/src/error.rs @@ -27,6 +27,9 @@ pub enum ContractError { #[error("signature is invalid")] InvalidSignature, + #[error("signature is invalid. expected: {expected}, received {received}")] + InvalidSignatureDetail { expected: String, received: String }, + #[error("signature is empty")] EmptySignature, From 92448da26f2ae52bfe05ad11547228600c8e776a Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 16 Oct 2023 15:03:34 -0700 Subject: [PATCH 17/20] binary hash --- account/src/auth/jwt.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index 2eb8738..6d61da8 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -4,7 +4,7 @@ use crate::error::ContractError::{ use crate::error::ContractResult; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine as _; -use cosmwasm_std::Timestamp; +use cosmwasm_std::{Binary, Timestamp}; use phf::{phf_map, Map}; use rsa::traits::SignatureScheme; use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey}; @@ -26,7 +26,7 @@ struct Claims { nbf: u64, // Optional. Not Before (as UTC timestamp) sub: String, // Optional. Subject (whom token refers to) - transaction_hash: String, + transaction_hash: Binary, } pub fn verify( @@ -96,12 +96,12 @@ pub fn verify( return Err(InvalidTime); } // make sure the provided hash matches the one from the tx - if tx_hash.eq(claims.transaction_hash.as_bytes()) { + if tx_hash.eq(&claims.transaction_hash) { Ok(true) } else { Err(InvalidSignatureDetail { expected: URL_SAFE_NO_PAD.encode(tx_hash), - received: claims.transaction_hash, + received: URL_SAFE_NO_PAD.encode(claims.transaction_hash), }) } } From b219df52821daddbee9b0d08c0b33dbfa26740ac Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 17 Oct 2023 14:14:44 -0700 Subject: [PATCH 18/20] specific time errors --- account/src/auth/jwt.rs | 10 ++++++++-- account/src/error.rs | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index 6d61da8..0405c91 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -89,11 +89,17 @@ pub fn verify( // complete the time checks let expiration = Timestamp::from_seconds(claims.exp as u64); if expiration.lt(current_time) { - return Err(InvalidTime); + return Err(InvalidTime { + current: current_time.seconds(), + received: expiration.seconds(), + }); } let not_before = Timestamp::from_seconds(claims.nbf as u64); if not_before.gt(current_time) { - return Err(InvalidTime); + return Err(InvalidTime { + current: current_time.seconds(), + received: not_before.seconds(), + }); } // make sure the provided hash matches the one from the tx if tx_hash.eq(&claims.transaction_hash) { diff --git a/account/src/error.rs b/account/src/error.rs index 05fe775..0832421 100644 --- a/account/src/error.rs +++ b/account/src/error.rs @@ -48,8 +48,8 @@ pub enum ContractError { #[error("cannot delete the last authenticator")] MinimumAuthenticatorCount, - #[error("invalid time on signature")] - InvalidTime, + #[error("invalid time on signature. current: {current} received: {received}")] + InvalidTime { current: u64, received: u64 }, #[error("invalid jwt aud")] InvalidJWTAud, From fa77aed3576ea4bc1338f79cbe0b2bdfa01ebf9e Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 18 Oct 2023 10:48:49 -0700 Subject: [PATCH 19/20] fix tests --- account/src/execute.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/account/src/execute.rs b/account/src/execute.rs index 1c03b62..6939cb6 100644 --- a/account/src/execute.rs +++ b/account/src/execute.rs @@ -215,7 +215,7 @@ 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; + use cosmwasm_std::testing::{mock_dependencies, mock_env}; use cosmwasm_std::Binary; use crate::auth::Authenticator; @@ -226,6 +226,7 @@ mod tests { fn test_before_tx() { let authId = 0; let mut deps = mock_dependencies(); + let env = mock_env(); let pubkey = "Ayrlj6q3WWs91p45LVKwI8JyfMYNmWMrcDinLNEdWYE4"; let pubkey_bytes = general_purpose::STANDARD.decode(pubkey).unwrap(); @@ -248,6 +249,6 @@ mod tests { let sig_bytes = Binary::from(new_vec); let tx_bytes = Binary::from(general_purpose::STANDARD.decode("Cp0BCpoBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEnoKP3hpb24xbTZ2aDIwcHM3NW0ybjZxeHdwandmOGZzM2t4dzc1enN5M3YycnllaGQ5c3BtbnUwcTlyc2g0NnljeRIreGlvbjFlMmZ1d2UzdWhxOHpkOW5ra2s4NzZuYXdyd2R1bGd2NDYwdnpnNxoKCgV1eGlvbhIBMRJTCksKQwodL2Fic3RyYWN0YWNjb3VudC52MS5OaWxQdWJLZXkSIgog3pl1PDD1NqnoBnBk5J0wjYzvUFAkWKGTN2lgHc+PAUcSBAoCCAESBBDgpxIaFHhpb24tbG9jYWwtdGVzdG5ldC0xIAg=").unwrap()); - before_tx(deps.as_ref(), &tx_bytes, Some(&sig_bytes), false).unwrap(); + before_tx(deps.as_ref(), &env, &tx_bytes, Some(&sig_bytes), false).unwrap(); } } From 5ed8c90e98a4211aa41192d70979661db3cad540 Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 18 Oct 2023 10:55:11 -0700 Subject: [PATCH 20/20] tests covered in integration --- account/src/auth/jwt.rs | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/account/src/auth/jwt.rs b/account/src/auth/jwt.rs index 0405c91..6978677 100644 --- a/account/src/auth/jwt.rs +++ b/account/src/auth/jwt.rs @@ -111,36 +111,3 @@ pub fn verify( }) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::auth::Authenticator; - - #[test] - fn test_validate_token() { - let encoded_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3ay10ZXN0LWI5OGFjMTExLTg1MTUtNGY0OS05MDU2LTdmM2E5NzJmNzU4MSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCJdLCJleHAiOjE2OTU4NDgxNjksImh0dHBzOi8vc3R5dGNoLmNvbS9zZXNzaW9uIjp7ImlkIjoic2Vzc2lvbi10ZXN0LTk1MDUyMzJkLTczNjUtNDExZC1hYzBlLTc3MWM2YWY2MmU3NCIsInN0YXJ0ZWRfYXQiOiIyMDIzLTA5LTI3VDIwOjUxOjA5WiIsImxhc3RfYWNjZXNzZWRfYXQiOiIyMDIzLTA5LTI3VDIwOjUxOjA5WiIsImV4cGlyZXNfYXQiOiIyMDIzLTEwLTI3VDIwOjUxOjA5WiIsImF0dHJpYnV0ZXMiOnsidXNlcl9hZ2VudCI6IiIsImlwX2FkZHJlc3MiOiIifSwiYXV0aGVudGljYXRpb25fZmFjdG9ycyI6W3sidHlwZSI6Im90cCIsImRlbGl2ZXJ5X21ldGhvZCI6ImVtYWlsIiwibGFzdF9hdXRoZW50aWNhdGVkX2F0IjoiMjAyMy0wOS0yN1QyMDo1MTowOVoiLCJlbWFpbF9mYWN0b3IiOnsiZW1haWxfaWQiOiJlbWFpbC10ZXN0LWY1ODU1MDU1LTc2OWMtNGVhMC04YzZhLTFmMTUyNDgzNWJmMiIsImVtYWlsX2FkZHJlc3MiOiJmaWxhbWVudCsxNjk1ODQ3ODUyMDY1QGJ1cm50LmNvbSJ9fV19LCJpYXQiOjE2OTU4NDc4NjksImlzcyI6InN0eXRjaC5jb20vcHJvamVjdC10ZXN0LTE4NWU5YTlmLThiYWItNDJmMi1hOTI0LTk1M2E1OWU4ZmY5NCIsIm5iZiI6MTY5NTg0Nzg2OSwic3ViIjoidXNlci10ZXN0LWY1MmRkZWQyLWM5M2EtNGVmZC05MTY5LTU3N2ZiZGY2Y2I4NiIsInRyYW5zYWN0aW9uX2hhc2giOiIweDEyMzQ1Njc4OTBfMyJ9.SkoLgjViIkP2kVcgz4fqA1CEWKbkN40behhs_ph-uCIAWNakCC_6FEfvcYgxBp1idk2IPRRZir-QAK_XV8ov_RHZuCqc8qd_bzlAwXV7cElHDf8oQxs44kA_P81QExoCABa3_ZzJ8KUNwzY0NbFxI3oJKDOYGxapi5aY5xsuGO3wUJX4PlsJ5xQhn254THgxpstqXEj56K1bcuDw_y-TQiNnP9R3vLZfGWj6BkZjB8dfgMp6FMRq_tBmo4l37pVHIA8v3-tlYSNoV7Z1P2uLsuxrM3_eh5zJ48vogwiOYAC2Ih90Pp2mF0leKOEXC4feTl_2oPIvGdXqbxgAmGKjpA"; - let encoded_hash = "0x1234567890_3"; - let hash_bytes = encoded_hash.as_bytes().to_vec(); - - let verification = verify( - &Timestamp::from_seconds(1695847870), - &hash_bytes, - encoded_token.as_bytes(), - &"project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94".to_string(), - &"user-test-f52dded2-c93a-4efd-9169-577fbdf6cb86".to_string(), - ); - assert!(verification.unwrap()); - } - - #[test] - fn test_dump_authenticator() { - let authenticator = Authenticator::Jwt { - aud: "project-test-185e9a9f-8bab-42f2-a924-953a59e8ff94".to_string(), - sub: "user-test-f52dded2-c93a-4efd-9169-577fbdf6cb86".to_string(), - }; - let serialized = serde_json::to_string(&authenticator).unwrap(); - - println!("authenticator: {}", serialized); - } -}