Skip to content

Commit

Permalink
Merge pull request #22 from burnt-labs/feat/jwt-query
Browse files Browse the repository at this point in the history
Feat/jwt query
  • Loading branch information
ash-burnt authored Mar 6, 2024
2 parents 7162a83 + c4a91c0 commit 91329a5
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 101 deletions.
10 changes: 2 additions & 8 deletions account/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

mod eth_crypto;
mod jwt;
pub mod jwt;
pub mod passkey;
mod secp256r1;
mod sign_arb;
Expand Down Expand Up @@ -113,13 +113,7 @@ impl Authenticator {
}
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,
);
jwt::verify(deps, &tx_bytes_hash, sig_bytes.as_slice(), aud, sub)
}
Authenticator::Secp256R1 { pubkey } => {
let tx_bytes_hash = util::sha256(tx_bytes);
Expand Down
108 changes: 26 additions & 82 deletions account/src/auth/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,110 +1,54 @@
use crate::error::ContractError::{
InvalidJWTAud, InvalidSignatureDetail, InvalidTime, InvalidToken,
};
use crate::error::ContractError::{InvalidSignatureDetail, InvalidToken};
use crate::error::ContractResult;
use crate::proto::{self, XionCustomQuery};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use cosmwasm_std::{Binary, Timestamp};
use phf::{phf_map, Map};
use rsa::traits::SignatureScheme;
use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Binary, Deps};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::str;

static AUD_KEY_MAP: Map<&'static str, &'static str> = phf_map! {
// GA - Testnet - Test project
"project-test-5ae234a7-6b74-46af-a7b7-969f3df38cc0" => "4ia1pODcj-BPNblyJ1ao1etK0VltRWQEmeoQtHaCWrOES-2BCFbcOBsDDxrXPzkTUK5j15fpMFbg36vDqXiYDNPHTp7WxUrOKOSyONk4gZUd626GZwKJBryMAhU7mBMByO56sLUHdDPajykYIlpHut75gDqipDI5QY9fh_piLh7OMy-MORaWdmkv1zFqLfjAr2GUKFmd7xiUAYTsjDClTTMn1rGskjBF8qPK9jDrPz9SEwN1n7N0JPsJVRqP6m5Yf_l9JWSKarSLbV9O0qMC7Nl0MpBKTw8HTVlwaBWF-5aGbg3dMQl8Cbn4vNUv-pPjrlvrpw2m_r0Gr5N9CBEKFQ;AQAB",
// GA - Testnet - Live project
"project-live-7e4a3221-79cd-4f34-ac1d-fedac4bde13e" => "qm5TbnKO8tCEVdwQK1Zit0_ig2nitUzA4V_m7oePByX1oSMismJOpbgEY2xjLVCMl_JdZOUIBQvaoFx169GS0-PrKEA8sXS-20Dp8rjiEG1hSaHapRfrDPjyN5TvPPp_xNAi8YBpZ5-msK0TZmG13Rcwn9xcu74AVW0PE19s0xWGAeukoaALfgk66RdwA7_C3KKeFkaEk9VpTtVJS7e-H815L2utXaqMC7uf-Qg93l0ifVBqaJj318BdV1dBj4cliMd1k7LlSD_qmcrqYUdggJB5FquVHjSj6-j5SMBne2IzWh4GLMneS_HGoTclRCHsOGi_3BhsjgkaZt6QCLr0_fafWUinJYrnEcIjojFlWuDvzPfoSV3bRefe_IQT4-Ht8fvwVcw5wEDhBiE2lfjHjMyRG-knlM910xnEJjJjxYWbyb_fLW-NVWULFH-L91DhxlXjDwO7hbbMlGlviTcsEa3ahwszNooQ63JJdp96iSA2JgWY6JPvWHG0mNrEU3AC6UMHLUtI2Hpg1ij6tiieFUMvFLvjj7dCozpDnZr2z6msCyTgUAmO3KQHaQ3Rvo2WwyuJPzOJLBnefLZIqZzAOXHAjI_bPTTOte1vPYkfLJxLKncdd-1OCwoLMyWAdCpD4gpIsam3jPhhQfAOio1XI1BXtDMxqIyXtCQD94ycwtU;AQAB",
// Exodvs - Test project
"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)]
struct Claims {
aud: Box<[String]>, // Optional. Audience
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: u64, // Optional. Not Before (as UTC timestamp)
sub: String, // Optional. Subject (whom token refers to)

// aud: Box<[String]>, // Optional. Audience
// 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: u64, // Optional. Not Before (as UTC timestamp)
// sub: String, // Optional. Subject (whom token refers to)
transaction_hash: Binary,
}

#[cw_serde]
struct QueryValidateJWTResponse {}

pub fn verify(
current_time: &Timestamp,
deps: Deps<XionCustomQuery>,
tx_hash: &Vec<u8>,
sig_bytes: &[u8],
aud: &str,
sub: &str,
) -> ContractResult<bool> {
if !AUD_KEY_MAP.contains_key(aud) {
return Err(InvalidJWTAud);
}
// let challenge = general_purpose::STANDARD.encode(tx_hash);

let key = match AUD_KEY_MAP.get(aud) {
None => return Err(InvalidJWTAud),
Some(k) => *k,
let query = proto::QueryValidateJWTRequest {
aud: aud.to_string(),
sub: sub.to_string(),
sig_bytes: String::from_utf8(sig_bytes.into()).unwrap(),
// tx_hash: challenge,
};

// prepare the components of the token for verification
deps.querier
.query::<QueryValidateJWTResponse>(&query.into())?;

// at this point we have validated the JWT. Any custom claims on it's body
// can follow
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
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 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::<Sha256>();
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 check
//
// timing in cosmos is unstable to say the least. therefore we have noticed
// that the perceived time in the chain can swing quite a bit, and is almost
// exclusively in the past. Therefore, NBF (not before) checks, which are
// primarily set at time of JWT creation, almost always fail. Knowing this,
// we have decided to only check expiration
let expiration = Timestamp::from_seconds(claims.exp);
if expiration.lt(current_time) {
return Err(InvalidTime {
current: current_time.seconds(),
received: expiration.seconds(),
});
}
// make sure the provided hash matches the one from the tx
if tx_hash.eq(&claims.transaction_hash) {
Ok(true)
Expand Down
22 changes: 11 additions & 11 deletions account/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::borrow::BorrowMut;

use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Env, Event, Order, Response};

use crate::auth::{passkey, AddAuthenticator, Authenticator};
use crate::auth::{jwt, passkey, AddAuthenticator, Authenticator};
use crate::proto::XionCustomQuery;
use crate::{
error::{ContractError, ContractResult},
Expand Down Expand Up @@ -169,17 +169,16 @@ pub fn add_auth_method(
sub: (*sub).clone(),
};

if !auth.verify(
jwt::verify(
deps.as_ref(),
&env,
&Binary::from(env.contract.address.as_bytes()),
&Binary::from(env.contract.address.as_bytes()).to_vec(),
token,
)? {
Err(ContractError::InvalidSignature)
} else {
save_authenticator(deps, *id, &auth)?;
Ok(())
}
aud,
sub,
)?;

save_authenticator(deps, *id, &auth)?;
Ok(())
}
AddAuthenticator::Secp256R1 {
id,
Expand Down Expand Up @@ -218,7 +217,7 @@ pub fn add_auth_method(
url: (*url).clone(),
passkey: passkey.clone(),
};
AUTHENTICATORS.save(deps.storage, *id, &auth)?;
save_authenticator(deps, *id, &auth)?;
// we replace the sent credential with the passkey for indexers and other
// observers to see
*(credential) = passkey;
Expand Down Expand Up @@ -353,6 +352,7 @@ mod tests {
))
}
XionCustomQuery::Authenticate(_) => todo!(),
XionCustomQuery::JWTValidate(_) => todo!(),
});

let query_msg = XionCustomQuery::Verify(proto::QueryWebAuthNVerifyRegisterRequest {
Expand Down
27 changes: 27 additions & 0 deletions account/src/proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,37 @@ pub struct QueryWebAuthNVerifyRegisterResponse {
#[derive(Clone, PartialEq, Eq, ::prost::Message, serde::Serialize, serde::Deserialize)]
pub struct QueryWebAuthNVerifyAuthenticateResponse {}

#[derive(
Clone,
PartialEq,
Eq,
::prost::Message,
serde::Serialize,
serde::Deserialize,
schemars::JsonSchema,
CosmwasmExt,
)]
#[proto_message(type_url = "/xion.jwk.v1.Query/ValidateJWT")]
#[proto_query(path = "/xion.jwk.v1.Query/ValidateJWT", response_type = QueryValidateJWTResponse)]
pub struct QueryValidateJWTRequest {
#[prost(string, tag = "1")]
pub aud: String,
#[prost(string, tag = "2")]
pub sub: String,
#[prost(string, tag = "3")]
pub sig_bytes: String,
// #[prost(string, tag = "4")]
// pub tx_hash: String,
}

#[derive(Clone, PartialEq, Eq, ::prost::Message, serde::Serialize, serde::Deserialize)]
pub struct QueryValidateJWTResponse {}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum XionCustomQuery {
Verify(QueryWebAuthNVerifyRegisterRequest),
Authenticate(QueryWebAuthNVerifyAuthenticateRequest),
JWTValidate(QueryValidateJWTRequest),
}
impl CustomQuery for XionCustomQuery {}

0 comments on commit 91329a5

Please sign in to comment.