diff --git a/Cargo.lock b/Cargo.lock index a1c80d6e..b8866378 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2465,7 +2465,6 @@ dependencies = [ "axum-extra", "axum-macros", "chrono", - "data-encoding", "headers", "hyper", "indoc", @@ -2488,7 +2487,6 @@ dependencies = [ "serde_json", "serde_urlencoded", "serde_with", - "sha2 0.10.5", "sqlx", "thiserror", "tokio", diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 4d206867..581e8975 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -39,11 +39,7 @@ serde_urlencoded = "0.7.1" # Password hashing argon2 = { version = "0.4.1", features = ["password-hash"] } -# Crypto, hashing and signing stuff -sha2 = "0.10.5" - # Various data types and utilities -data-encoding = "2.3.2" chrono = { version = "0.4.22", features = ["serde"] } url = { version = "2.2.2", features = ["serde"] } mime = "0.3.16" diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index e03b5d8e..a91166b6 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -17,14 +17,13 @@ use std::collections::HashMap; use anyhow::Context; use axum::{extract::State, response::IntoResponse, Json}; use chrono::{DateTime, Duration, Utc}; -use data_encoding::BASE64URL_NOPAD; use headers::{CacheControl, HeaderMap, HeaderMapExt, Pragma}; use hyper::StatusCode; use mas_axum_utils::client_authorization::{ClientAuthorization, CredentialsVerificationError}; use mas_data_model::{AuthorizationGrantStage, Client, TokenType}; use mas_iana::jose::JsonWebSignatureAlg; use mas_jose::{ - claims::{self, ClaimError}, + claims::{self, hash_token, ClaimError}, constraints::Constrainable, jwt::{JsonWebSignatureHeader, Jwt, JwtSignatureError}, }; @@ -54,7 +53,6 @@ use oauth2_types::{ use rand::thread_rng; use serde::Serialize; use serde_with::{serde_as, skip_serializing_none}; -use sha2::{Digest, Sha256}; use sqlx::{PgPool, Postgres, Transaction}; use thiserror::Error; use tracing::debug; @@ -228,16 +226,6 @@ pub(crate) async fn post( Ok((headers, Json(reply))) } -fn hash(mut hasher: H, token: &str) -> anyhow::Result { - hasher.update(token); - let hash = hasher.finalize(); - // Left-most 128bit - let bits = hash - .get(..16) - .context("failed to get first 128 bits of hash")?; - Ok(BASE64URL_NOPAD.encode(bits)) -} - #[allow(clippy::too_many_lines)] async fn authorization_code_grant( grant: &AuthorizationCodeGrant, @@ -343,9 +331,6 @@ async fn authorization_code_grant( claims::AUTH_TIME.insert(&mut claims, last_authentication.created_at)?; } - claims::AT_HASH.insert(&mut claims, hash(Sha256::new(), &access_token_str)?)?; - claims::C_HASH.insert(&mut claims, hash(Sha256::new(), &grant.code)?)?; - let alg = client .id_token_signed_response_alg .unwrap_or(JsonWebSignatureAlg::Rs256); @@ -353,6 +338,9 @@ async fn authorization_code_grant( .signing_key_for_algorithm(alg) .context("no suitable key found")?; + claims::AT_HASH.insert(&mut claims, hash_token(alg, &access_token_str)?)?; + claims::C_HASH.insert(&mut claims, hash_token(alg, &grant.code)?)?; + let header = JsonWebSignatureHeader::new(alg) .with_kid(key.kid().context("key has no `kid` for some reason")?); let signer = key.params().signing_key_for_alg(alg)?; diff --git a/crates/jose/src/claims.rs b/crates/jose/src/claims.rs index b047708a..96835026 100644 --- a/crates/jose/src/claims.rs +++ b/crates/jose/src/claims.rs @@ -14,7 +14,11 @@ use std::{collections::HashMap, marker::PhantomData, ops::Deref}; +use anyhow::Context; +use base64ct::{Base64UrlUnpadded, Encoding}; +use mas_iana::jose::JsonWebSignatureAlg; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use sha2::{Digest, Sha256, Sha384, Sha512}; use thiserror::Error; #[derive(Debug, Error)] @@ -235,6 +239,81 @@ impl From<&TimeOptions> for TimeNotBefore { } } +/// Hash the given token with the given algorithm for an ID Token claim. +/// +/// According to the [OpenID Connect Core 1.0 specification]. +/// +/// # Errors +/// +/// Returns an error if the algorithm is not supported. +/// +/// [OpenID Connect Core 1.0 specification]: https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken +pub fn hash_token(alg: JsonWebSignatureAlg, token: &str) -> anyhow::Result { + let bits = match alg { + JsonWebSignatureAlg::Hs256 + | JsonWebSignatureAlg::Rs256 + | JsonWebSignatureAlg::Es256 + | JsonWebSignatureAlg::Ps256 + | JsonWebSignatureAlg::Es256K => { + let mut hasher = Sha256::new(); + hasher.update(token); + let hash = hasher.finalize(); + // Left-most half + hash.get(..16).map(ToOwned::to_owned) + } + JsonWebSignatureAlg::Hs384 + | JsonWebSignatureAlg::Rs384 + | JsonWebSignatureAlg::Es384 + | JsonWebSignatureAlg::Ps384 => { + let mut hasher = Sha384::new(); + hasher.update(token); + let hash = hasher.finalize(); + // Left-most half + hash.get(..24).map(ToOwned::to_owned) + } + JsonWebSignatureAlg::Hs512 + | JsonWebSignatureAlg::Rs512 + | JsonWebSignatureAlg::Es512 + | JsonWebSignatureAlg::Ps512 => { + let mut hasher = Sha512::new(); + hasher.update(token); + let hash = hasher.finalize(); + // Left-most half + hash.get(..32).map(ToOwned::to_owned) + } + JsonWebSignatureAlg::EdDsa | JsonWebSignatureAlg::None => { + return Err(anyhow::anyhow!("unsupported algorithm for hashing")) + } + } + .context("failed to get first half of hash")?; + + Ok(Base64UrlUnpadded::encode_string(&bits)) +} + +#[derive(Debug, Clone)] +pub struct TokenHash<'a> { + alg: JsonWebSignatureAlg, + token: &'a str, +} + +impl<'a> TokenHash<'a> { + /// Creates a new `TokenHash` validator for the given algorithm and token. + #[must_use] + pub fn new(alg: JsonWebSignatureAlg, token: &'a str) -> Self { + Self { alg, token } + } +} + +impl<'a> Validator for TokenHash<'a> { + fn validate(&self, value: &String) -> Result<(), anyhow::Error> { + if hash_token(self.alg, self.token)? == *value { + Ok(()) + } else { + Err(anyhow::anyhow!("hashes don't match")) + } + } +} + #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] #[serde(transparent)] pub struct Timestamp(#[serde(with = "chrono::serde::ts_seconds")] chrono::DateTime);