Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT Verification #6

Merged
merged 22 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,12 +15,15 @@ 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"] }
serde_json = "1.0.87"
schemars = "0.8.10"
ripemd = "0.1.3"
bech32 = "0.9.1"
base64 = "0.21.4"
base64 = "0.21.4"
phf = { version = "0.11.2", features = ["macros"] }
rsa = { version = "0.9.2" }
getrandom = { version = "0.2.10", features = ["custom"] }
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +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
```
5 changes: 4 additions & 1 deletion account/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ schemars = { workspace = true }
hex = { workspace = true }
ripemd = { workspace = true }
bech32 = { workspace = true }
base64 = { workspace = true }
base64 = { workspace = true }
phf = { workspace = true }
rsa = { workspace = true }
getrandom = { workspace = true}
21 changes: 20 additions & 1 deletion account/src/auth.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -24,19 +25,27 @@ pub enum AddAuthenticator {
address: String,
signature: Binary,
},
Jwt {
id: u8,
aud: String,
sub: String,
token: Binary,
},
}

#[derive(Serialize, Deserialize, Clone, JsonSchema, PartialEq, Debug)]
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<bool, ContractError> {
Expand Down Expand Up @@ -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,
);
}
}
}
}
113 changes: 113 additions & 0 deletions account/src/auth/jwt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 _;
use cosmwasm_std::{Binary, Timestamp};
use phf::{phf_map, Map};
use rsa::traits::SignatureScheme;
use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
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)]
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)

transaction_hash: Binary,
}

pub fn verify(
current_time: &Timestamp,
tx_hash: &Vec<u8>,
sig_bytes: &[u8],
aud: &str,
sub: &str,
) -> ContractResult<bool> {
if !AUD_KEY_MAP.contains_key(aud) {
return Err(InvalidJWTAud);
}

let key = match AUD_KEY_MAP.get(aud) {
None => return Err(InvalidJWTAud),
Some(k) => *k,
};

// 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 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 checks
let expiration = Timestamp::from_seconds(claims.exp as u64);
if expiration.lt(current_time) {
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 {
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) {
Ok(true)
} else {
Err(InvalidSignatureDetail {
expected: URL_SAFE_NO_PAD.encode(tx_hash),
received: URL_SAFE_NO_PAD.encode(claims.transaction_hash),
})
}
}
10 changes: 8 additions & 2 deletions account/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ pub fn instantiate(
}

#[entry_point]
pub fn sudo(deps: DepsMut, _env: Env, msg: AccountSudoMsg) -> ContractResult<Response> {
pub fn sudo(deps: DepsMut, env: Env, msg: AccountSudoMsg) -> ContractResult<Response> {
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(),
}
}
Expand Down
21 changes: 21 additions & 0 deletions account/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,21 @@ pub enum ContractError {
#[error(transparent)]
Bech32(#[from] bech32::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,

#[error("signature is invalid. expected: {expected}, received {received}")]
InvalidSignatureDetail { expected: String, received: String },

#[error("signature is empty")]
EmptySignature,

Expand All @@ -35,6 +47,15 @@ pub enum ContractError {

#[error("cannot delete the last authenticator")]
MinimumAuthenticatorCount,

#[error("invalid time on signature. current: {current} received: {received}")]
InvalidTime { current: u64, received: u64 },

#[error("invalid jwt aud")]
InvalidJWTAud,

#[error("invalid token")]
InvalidToken,
}

pub type ContractResult<T> = Result<T, ContractError>;
37 changes: 34 additions & 3 deletions account/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
) -> ContractResult<Response> {
if !authenticator.verify(
deps.api,
&env,
&Binary::from(env.contract.address.as_bytes()),
signature,
)? {
Expand All @@ -31,6 +32,7 @@

pub fn before_tx(
deps: Deps,
env: &Env,
tx_bytes: &Binary,
cred_bytes: Option<&Binary>,
simulate: bool,
Expand Down Expand Up @@ -66,9 +68,12 @@
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),
};
Expand Down Expand Up @@ -98,6 +103,7 @@

if !auth.verify(
deps.api,
&env,
&Binary::from(env.contract.address.as_bytes()),
&signature,
)? {
Expand All @@ -118,6 +124,7 @@

if !auth.verify(
deps.api,
&env,
&Binary::from(env.contract.address.as_bytes()),
&signature,
)? {
Expand All @@ -138,6 +145,7 @@

if !auth.verify(
deps.api,
&env,
&Binary::from(env.contract.address.as_bytes()),
&signature,
)? {
Expand All @@ -149,6 +157,28 @@
.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()))
}
}
}
}

Expand Down Expand Up @@ -185,7 +215,7 @@
#[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;
Expand All @@ -194,8 +224,9 @@

#[test]
fn test_before_tx() {
let authId = 0;

Check warning on line 227 in account/src/execute.rs

View workflow job for this annotation

GitHub Actions / Test Suite

variable `authId` should have a snake case name
let mut deps = mock_dependencies();
let env = mock_env();

let pubkey = "Ayrlj6q3WWs91p45LVKwI8JyfMYNmWMrcDinLNEdWYE4";
let pubkey_bytes = general_purpose::STANDARD.decode(pubkey).unwrap();
Expand All @@ -207,17 +238,17 @@
let sig_arr = general_purpose::STANDARD.decode(signature).unwrap();

// The index of the first authenticator is 0.
let credIndex = vec![0u8];

Check warning on line 241 in account/src/execute.rs

View workflow job for this annotation

GitHub Actions / Test Suite

variable `credIndex` should have a snake case name

let mut new_vec = Vec::new();
new_vec.extend_from_slice(&credIndex);
new_vec.extend_from_slice(&sig_arr);

AUTHENTICATORS.save(deps.as_mut().storage, authId, &auth);

Check warning on line 247 in account/src/execute.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused `Result` that must be used

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();
}
}
Loading
Loading