From ca125a14c50ffebe201f1f13782d050c073ce24c Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 23 Aug 2022 17:45:23 +0200 Subject: [PATCH] WIP: better JOSE --- Cargo.lock | 15 +- crates/axum-utils/src/client_authorization.rs | 6 +- crates/config/Cargo.toml | 2 +- crates/handlers/Cargo.toml | 2 +- crates/handlers/src/oauth2/token.rs | 8 +- crates/jose/Cargo.toml | 3 +- crates/jose/src/jwt/header.rs | 143 +++++++++ crates/jose/src/{jwt.rs => jwt/mod.rs} | 203 +++++-------- crates/jose/src/jwt/raw.rs | 99 +++++++ crates/jose/src/jwt/signed.rs | 278 ++++++++++++++++++ .../jose/src/keystore/jwks/dynamic_store.rs | 9 +- crates/jose/src/keystore/jwks/static_store.rs | 11 +- crates/jose/src/keystore/shared_secret.rs | 20 +- crates/jose/src/keystore/static_keystore.rs | 20 +- crates/jose/src/keystore/traits.rs | 26 +- crates/jose/src/lib.rs | 2 +- 16 files changed, 690 insertions(+), 157 deletions(-) create mode 100644 crates/jose/src/jwt/header.rs rename crates/jose/src/{jwt.rs => jwt/mod.rs} (52%) create mode 100644 crates/jose/src/jwt/raw.rs create mode 100644 crates/jose/src/jwt/signed.rs diff --git a/Cargo.lock b/Cargo.lock index 4d1f06bc..a0945915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-signature" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57ade8a273b42906cb87145358c778667c1ca515bfe24f61ed8a4bfe756c8e3" +dependencies = [ + "async-trait", + "signature", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -2545,6 +2555,7 @@ name = "mas-jose" version = "0.1.0" dependencies = [ "anyhow", + "async-signature", "async-trait", "base64ct", "chrono", @@ -3725,8 +3736,7 @@ dependencies = [ [[package]] name = "rsa" version = "0.7.0-pre" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6168b9a0f38e487db90dc109ad6d8f37fc5590183b7bfe8d8687e0b86116d53f" +source = "git+https://github.com/RustCrypto/RSA.git#40242fbbb019cac4af216145bfa468a0d24d12ef" dependencies = [ "byteorder", "digest 0.10.3", @@ -3737,6 +3747,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", + "signature", "smallvec", "subtle", "zeroize", diff --git a/crates/axum-utils/src/client_authorization.rs b/crates/axum-utils/src/client_authorization.rs index 3ba745e4..f994816c 100644 --- a/crates/axum-utils/src/client_authorization.rs +++ b/crates/axum-utils/src/client_authorization.rs @@ -31,8 +31,8 @@ use mas_data_model::{Client, JwksOrJwksUri, StorageBackend}; use mas_http::HttpServiceExt; use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_jose::{ - DecodedJsonWebToken, DynamicJwksStore, Either, JsonWebKeySet, JsonWebTokenParts, JwtHeader, - SharedSecret, StaticJwksStore, VerifyingKeystore, + DecodedJsonWebToken, DynamicJwksStore, Either, JsonWebKeySet, JsonWebSignatureHeader, + JsonWebTokenParts, SharedSecret, StaticJwksStore, VerifyingKeystore, }; use mas_storage::{ oauth2::client::{lookup_client_by_client_id, ClientFetchError}, @@ -73,7 +73,7 @@ pub enum Credentials { ClientAssertionJwtBearer { client_id: String, jwt: JsonWebTokenParts, - header: Box, + header: Box, claims: HashMap, }, } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 5faaf56c..6e301d6a 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -25,7 +25,7 @@ sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "postgres"] } lettre = { version = "0.10.1", default-features = false, features = ["serde", "builder"] } rand = "0.8.5" -rsa = "0.7.0-pre" +rsa = { git = "https://github.com/RustCrypto/RSA.git" } p256 = { version = "0.11.1", features = ["ecdsa", "pem", "pkcs8"] } pkcs8 = { version = "0.9.0", features = ["pem"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] } diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index b981abb6..69a49812 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -40,7 +40,7 @@ serde_urlencoded = "0.7.1" argon2 = { version = "0.4.1", features = ["password-hash"] } # Crypto, hashing and signing stuff -rsa = "0.7.0-pre" +rsa = { git = "https://github.com/RustCrypto/RSA.git" } pkcs8 = { version = "0.9.0", features = ["pem"] } elliptic-curve = { version = "0.12.3", features = ["pem"] } sha2 = "0.10.2" diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index e42d9232..478f9a4f 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -26,7 +26,7 @@ use mas_data_model::{AuthorizationGrantStage, Client, TokenType}; use mas_iana::jose::JsonWebSignatureAlg; use mas_jose::{ claims::{self, ClaimError}, - DecodedJsonWebToken, SigningKeystore, StaticKeystore, + DecodedJsonWebToken, JwtSignatureError, SigningKeystore, StaticKeystore, }; use mas_router::UrlBuilder; use mas_storage::{ @@ -173,6 +173,12 @@ impl From for RouteError { } } +impl From for RouteError { + fn from(e: JwtSignatureError) -> Self { + Self::Internal(Box::new(e)) + } +} + #[tracing::instrument(skip_all, err)] pub(crate) async fn post( client_authorization: ClientAuthorization, diff --git a/crates/jose/Cargo.toml b/crates/jose/Cargo.toml index 295dc527..43221998 100644 --- a/crates/jose/Cargo.toml +++ b/crates/jose/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" license = "Apache-2.0" [dependencies] +async-signature = "0.2.0" anyhow = "1.0.62" async-trait = "0.1.57" base64ct = { version = "1.5.1", features = ["std"] } @@ -21,7 +22,7 @@ p256 = { version = "0.11.1", features = ["ecdsa", "pem", "pkcs8"] } pkcs1 = { version = "0.4.0", features = ["pem", "pkcs8"] } pkcs8 = { version = "0.9.0", features = ["pem", "std"] } rand = "0.8.5" -rsa = "0.7.0-pre" +rsa = { git = "https://github.com/RustCrypto/RSA.git" } schemars = "0.8.10" sec1 = "0.3.0" serde = { version = "1.0.144", features = ["derive"] } diff --git a/crates/jose/src/jwt/header.rs b/crates/jose/src/jwt/header.rs new file mode 100644 index 00000000..a64a44c4 --- /dev/null +++ b/crates/jose/src/jwt/header.rs @@ -0,0 +1,143 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use mas_iana::jose::JsonWebSignatureAlg; +use serde::{Deserialize, Serialize}; +use serde_with::{ + base64::{Base64, Standard, UrlSafe}, + formats::{Padded, Unpadded}, + serde_as, skip_serializing_none, +}; +use url::Url; + +use crate::jwk::JsonWebKey; + +#[serde_as] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct JsonWebSignatureHeader { + alg: JsonWebSignatureAlg, + + #[serde(default)] + jku: Option, + + #[serde(default)] + jwk: Option, + + #[serde(default)] + kid: Option, + + #[serde(default)] + x5u: Option, + + #[serde(default)] + #[serde_as(as = "Option>>")] + x5c: Option>>, + + #[serde(default)] + #[serde_as(as = "Option>")] + x5t: Option>, + + #[serde(default, rename = "x5t#S256")] + #[serde_as(as = "Option>")] + x5t_s256: Option>, + + #[serde(default)] + typ: Option, + + #[serde(default)] + cty: Option, + + #[serde(default)] + crit: Option>, +} + +impl JsonWebSignatureHeader { + #[must_use] + pub fn new(alg: JsonWebSignatureAlg) -> Self { + Self { + alg, + jku: None, + jwk: None, + kid: None, + x5u: None, + x5c: None, + x5t: None, + x5t_s256: None, + typ: None, + cty: None, + crit: None, + } + } + + #[must_use] + pub const fn alg(&self) -> JsonWebSignatureAlg { + self.alg + } + + #[must_use] + pub const fn jku(&self) -> Option<&Url> { + self.jku.as_ref() + } + + #[must_use] + pub fn with_jku(mut self, jku: Url) -> Self { + self.jku = Some(jku); + self + } + + #[must_use] + pub const fn jwk(&self) -> Option<&JsonWebKey> { + self.jwk.as_ref() + } + + #[must_use] + pub fn with_jwk(mut self, jwk: JsonWebKey) -> Self { + self.jwk = Some(jwk); + self + } + + #[must_use] + pub fn kid(&self) -> Option<&str> { + self.kid.as_deref() + } + + #[must_use] + pub fn with_kid(mut self, kid: impl Into) -> Self { + self.kid = Some(kid.into()); + self + } + + #[must_use] + pub fn typ(&self) -> Option<&str> { + self.typ.as_deref() + } + + #[must_use] + pub fn with_typ(mut self, typ: String) -> Self { + self.typ = Some(typ); + self + } + + #[must_use] + pub fn crit(&self) -> Option<&[String]> { + self.crit.as_deref() + } + + #[must_use] + pub fn with_crit(mut self, crit: Vec) -> Self { + self.crit = Some(crit); + self + } +} diff --git a/crates/jose/src/jwt.rs b/crates/jose/src/jwt/mod.rs similarity index 52% rename from crates/jose/src/jwt.rs rename to crates/jose/src/jwt/mod.rs index fa67ad16..3d2459cc 100644 --- a/crates/jose/src/jwt.rs +++ b/crates/jose/src/jwt/mod.rs @@ -15,116 +15,16 @@ use std::str::FromStr; use base64ct::{Base64UrlUnpadded, Encoding}; -use mas_iana::jose::{ - JsonWebEncryptionCompressionAlgorithm, JsonWebEncryptionEnc, JsonWebSignatureAlg, -}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_with::{ - base64::{Base64, Standard, UrlSafe}, - formats::{Padded, Unpadded}, - serde_as, skip_serializing_none, -}; -use url::Url; +use serde::{de::DeserializeOwned, Serialize}; +use thiserror::Error; -use crate::{jwk::JsonWebKey, SigningKeystore, VerifyingKeystore}; +use crate::{SigningKeystore, VerifyingKeystore}; -#[serde_as] -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct JwtHeader { - alg: JsonWebSignatureAlg, +mod header; +mod raw; +mod signed; - #[serde(default)] - enc: Option, - - #[serde(default)] - jku: Option, - - #[serde(default)] - jwk: Option, - - #[serde(default)] - kid: Option, - - #[serde(default)] - x5u: Option, - - #[serde(default)] - #[serde_as(as = "Option>>")] - x5c: Option>>, - - #[serde(default)] - #[serde_as(as = "Option>")] - x5t: Option>, - #[serde(default, rename = "x5t#S256")] - #[serde_as(as = "Option>")] - x5t_s256: Option>, - - #[serde(default)] - typ: Option, - - #[serde(default)] - cty: Option, - - #[serde(default)] - crit: Option>, - - #[serde(default)] - zip: Option, -} - -impl JwtHeader { - pub fn encode(&self) -> anyhow::Result { - let payload = serde_json::to_string(self)?; - let encoded = Base64UrlUnpadded::encode_string(payload.as_bytes()); - Ok(encoded) - } - - #[must_use] - pub fn new(alg: JsonWebSignatureAlg) -> Self { - Self { - alg, - enc: None, - jku: None, - jwk: None, - kid: None, - x5u: None, - x5c: None, - x5t: None, - x5t_s256: None, - typ: None, - cty: None, - crit: None, - zip: None, - } - } - - #[must_use] - pub fn alg(&self) -> JsonWebSignatureAlg { - self.alg - } - - #[must_use] - pub fn kid(&self) -> Option<&str> { - self.kid.as_deref() - } - - #[must_use] - pub fn with_kid(mut self, kid: impl Into) -> Self { - self.kid = Some(kid.into()); - self - } -} - -impl FromStr for JwtHeader { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let decoded = Base64UrlUnpadded::decode_vec(s)?; - let parsed = serde_json::from_slice(&decoded)?; - Ok(parsed) - } -} +pub use self::{header::JsonWebSignatureHeader, signed::Jwt}; #[derive(Debug, PartialEq, Eq)] pub struct JsonWebTokenParts { @@ -132,21 +32,62 @@ pub struct JsonWebTokenParts { signature: Vec, } +#[derive(Error, Debug)] +#[error("failed to decode JWT")] +pub enum JwtPartsDecodeError { + #[error("no dots found in the JWT")] + NoDots, + + #[error("could not decode signature")] + SignatureEncoding { + #[from] + inner: base64ct::Error, + }, +} + impl FromStr for JsonWebTokenParts { - type Err = anyhow::Error; + type Err = JwtPartsDecodeError; fn from_str(s: &str) -> Result { - let (payload, signature) = s - .rsplit_once('.') - .ok_or_else(|| anyhow::anyhow!("no dots found in JWT"))?; + let (payload, signature) = s.rsplit_once('.').ok_or(JwtPartsDecodeError::NoDots)?; let signature = Base64UrlUnpadded::decode_vec(signature)?; let payload = payload.to_owned(); Ok(Self { payload, signature }) } } +#[derive(Error, Debug)] +#[error("failed to serialize JWT")] +pub enum JwtSerializeError { + #[error("failed to serialize JWT header")] + Header { + #[source] + inner: serde_json::Error, + }, + + #[error("failed to serialize payload")] + Payload { + #[source] + inner: serde_json::Error, + }, +} + +#[derive(Error, Debug)] +#[error("failed to serialize JWT")] +pub enum JwtSignatureError { + Serialize { + #[from] + inner: JwtSerializeError, + }, + + Sign { + #[source] + inner: anyhow::Error, + }, +} + pub struct DecodedJsonWebToken { - header: JwtHeader, + header: JsonWebSignatureHeader, payload: T, } @@ -154,24 +95,33 @@ impl DecodedJsonWebToken where T: Serialize, { - fn serialize(&self) -> anyhow::Result { - let header = serde_json::to_vec(&self.header)?; + fn serialize(&self) -> Result { + let header = serde_json::to_vec(&self.header) + .map_err(|inner| JwtSerializeError::Header { inner })?; let header = Base64UrlUnpadded::encode_string(&header); - let payload = serde_json::to_vec(&self.payload)?; + + let payload = serde_json::to_vec(&self.payload) + .map_err(|inner| JwtSerializeError::Payload { inner })?; let payload = Base64UrlUnpadded::encode_string(&payload); Ok(format!("{}.{}", header, payload)) } - pub async fn sign(&self, store: &S) -> anyhow::Result { + pub async fn sign( + &self, + store: &S, + ) -> Result { let payload = self.serialize()?; - let signature = store.sign(&self.header, payload.as_bytes()).await?; + let signature = store + .sign(&self.header, payload.as_bytes()) + .await + .map_err(|inner| JwtSignatureError::Sign { inner })?; Ok(JsonWebTokenParts { payload, signature }) } } impl DecodedJsonWebToken { - pub fn new(header: JwtHeader, payload: T) -> Self { + pub fn new(header: JsonWebSignatureHeader, payload: T) -> Self { Self { header, payload } } @@ -179,11 +129,11 @@ impl DecodedJsonWebToken { &self.payload } - pub fn header(&self) -> &JwtHeader { + pub fn header(&self) -> &JsonWebSignatureHeader { &self.header } - pub fn split(self) -> (JwtHeader, T) { + pub fn split(self) -> (JsonWebSignatureHeader, T) { (self.header, self.payload) } } @@ -213,7 +163,11 @@ impl JsonWebTokenParts { Ok(decoded) } - pub fn verify(&self, header: &JwtHeader, store: &S) -> S::Future { + pub fn verify( + &self, + header: &JsonWebSignatureHeader, + store: &S, + ) -> S::Future { store.verify(header, self.payload.as_bytes(), &self.signature) } @@ -239,6 +193,8 @@ impl JsonWebTokenParts { #[cfg(test)] mod tests { + use mas_iana::jose::JsonWebSignatureAlg; + use super::*; use crate::SharedSecret; @@ -247,13 +203,12 @@ mod tests { let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; let jwt: JsonWebTokenParts = jwt.parse().unwrap(); let secret = "your-256-bit-secret"; - println!("{:?}", jwt); let store = SharedSecret::new(&secret); let jwt: DecodedJsonWebToken = jwt.decode_and_verify(&store).await.unwrap(); - assert_eq!(jwt.header.typ, Some("JWT".to_owned())); - assert_eq!(jwt.header.alg, JsonWebSignatureAlg::Hs256); + assert_eq!(jwt.header.typ(), Some("JWT")); + assert_eq!(jwt.header.alg(), JsonWebSignatureAlg::Hs256); assert_eq!( jwt.payload, serde_json::json!({ diff --git a/crates/jose/src/jwt/raw.rs b/crates/jose/src/jwt/raw.rs new file mode 100644 index 00000000..1c29a56e --- /dev/null +++ b/crates/jose/src/jwt/raw.rs @@ -0,0 +1,99 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{borrow::Cow, ops::Deref}; + +use thiserror::Error; + +pub struct RawJwt<'a> { + inner: Cow<'a, str>, + first_dot: usize, + second_dot: usize, +} + +impl RawJwt<'static> { + pub(super) fn new(inner: String, first_dot: usize, second_dot: usize) -> Self { + Self { + inner: inner.into(), + first_dot, + second_dot, + } + } +} + +impl<'a> std::fmt::Display for RawJwt<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +impl<'a> RawJwt<'a> { + pub fn header(&'a self) -> &'a str { + &self.inner[..self.first_dot] + } + + pub fn payload(&'a self) -> &'a str { + &self.inner[self.first_dot + 1..self.second_dot] + } + + pub fn signature(&'a self) -> &'a str { + &self.inner[self.second_dot + 1..] + } + + pub fn signed_part(&'a self) -> &'a str { + &self.inner[..self.second_dot] + } +} + +impl<'a> Deref for RawJwt<'a> { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug, Error)] +pub enum DecodeError { + #[error("no dots found in JWT")] + NoDots, + + #[error("only one dot found in JWT")] + OnlyOneDot, + + #[error("too many dots in JWT")] + TooManyDots, +} + +impl<'a> TryFrom<&'a str> for RawJwt<'a> { + type Error = DecodeError; + fn try_from(value: &'a str) -> Result { + let mut indices = value + .char_indices() + .filter_map(|(idx, c)| (c == '.').then(|| idx)); + + let first_dot = indices.next().ok_or(DecodeError::NoDots)?; + let second_dot = indices.next().ok_or(DecodeError::OnlyOneDot)?; + + if indices.next().is_some() { + return Err(DecodeError::TooManyDots); + } + + Ok(Self { + inner: value.into(), + first_dot, + second_dot, + }) + } +} diff --git a/crates/jose/src/jwt/signed.rs b/crates/jose/src/jwt/signed.rs new file mode 100644 index 00000000..3c159f18 --- /dev/null +++ b/crates/jose/src/jwt/signed.rs @@ -0,0 +1,278 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use base64ct::{Base64UrlUnpadded, Encoding}; +use serde::{de::DeserializeOwned, Serialize}; +use signature::{Signature, Signer, Verifier}; +use thiserror::Error; + +use super::{header::JsonWebSignatureHeader, raw::RawJwt}; + +pub struct Jwt<'a, T> { + raw: RawJwt<'a>, + header: JsonWebSignatureHeader, + payload: T, + signature: Vec, +} + +impl<'a, T> std::fmt::Display for Jwt<'a, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.raw) + } +} + +impl<'a, T> std::fmt::Debug for Jwt<'a, T> +where + T: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Jwt") + .field("raw", &"...") + .field("header", &self.header) + .field("payload", &self.payload) + .field("signature", &"...") + .finish() + } +} + +#[derive(Debug, Error)] +pub enum JwtDecodeError { + #[error(transparent)] + RawDecode { + #[from] + inner: super::raw::DecodeError, + }, + + #[error("failed to decode JWT header")] + DecodeHeader { + #[source] + inner: base64ct::Error, + }, + + #[error("failed to deserialize JWT header")] + DeserializeHeader { + #[source] + inner: serde_json::Error, + }, + + #[error("failed to decode JWT payload")] + DecodePayload { + #[source] + inner: base64ct::Error, + }, + + #[error("failed to deserialize JWT payload")] + DeserializePayload { + #[source] + inner: serde_json::Error, + }, + + #[error("failed to decode JWT signature")] + DecodeSignature { + #[source] + inner: base64ct::Error, + }, +} + +impl JwtDecodeError { + fn decode_header(inner: base64ct::Error) -> Self { + Self::DecodeHeader { inner } + } + + fn deserialize_header(inner: serde_json::Error) -> Self { + Self::DeserializeHeader { inner } + } + + fn decode_payload(inner: base64ct::Error) -> Self { + Self::DecodePayload { inner } + } + + fn deserialize_payload(inner: serde_json::Error) -> Self { + Self::DeserializePayload { inner } + } + + fn decode_signature(inner: base64ct::Error) -> Self { + Self::DecodeSignature { inner } + } +} + +impl<'a, T> TryFrom<&'a str> for Jwt<'a, T> +where + T: DeserializeOwned, +{ + type Error = JwtDecodeError; + fn try_from(value: &'a str) -> Result { + let raw = RawJwt::try_from(value)?; + + let header = + Base64UrlUnpadded::decode_vec(raw.header()).map_err(JwtDecodeError::decode_header)?; + let header = serde_json::from_slice(&header).map_err(JwtDecodeError::deserialize_header)?; + + let payload = + Base64UrlUnpadded::decode_vec(raw.payload()).map_err(JwtDecodeError::decode_payload)?; + let payload = + serde_json::from_slice(&payload).map_err(JwtDecodeError::deserialize_payload)?; + + let signature = Base64UrlUnpadded::decode_vec(raw.signature()) + .map_err(JwtDecodeError::decode_signature)?; + + Ok(Self { + raw, + header, + payload, + signature, + }) + } +} + +#[derive(Debug, Error)] +pub enum JwtVerificationError { + #[error("failed to parse signature")] + ParseSignature { + #[source] + inner: signature::Error, + }, + + #[error("signature verification failed")] + Verify { + #[source] + inner: signature::Error, + }, +} + +impl JwtVerificationError { + fn parse_signature(inner: signature::Error) -> Self { + Self::ParseSignature { inner } + } + + fn verify(inner: signature::Error) -> Self { + Self::Verify { inner } + } +} + +impl<'a, T> Jwt<'a, T> { + pub fn verify(&self, key: &K) -> Result<(), JwtVerificationError> + where + K: Verifier, + S: Signature, + { + let signature = + S::from_bytes(&self.signature).map_err(JwtVerificationError::parse_signature)?; + + key.verify(self.raw.signed_part().as_bytes(), &signature) + .map_err(JwtVerificationError::verify) + } + + pub fn as_str(&'a self) -> &'a str { + &self.raw + } +} + +#[derive(Debug, Error)] +pub enum JwtSignatureError { + #[error("failed to serialize header")] + EncodeHeader { + #[source] + inner: serde_json::Error, + }, + + #[error("failed to serialize payload")] + EncodePayload { + #[source] + inner: serde_json::Error, + }, +} + +impl JwtSignatureError { + fn encode_header(inner: serde_json::Error) -> Self { + Self::EncodeHeader { inner } + } + + fn encode_payload(inner: serde_json::Error) -> Self { + Self::EncodePayload { inner } + } +} + +impl Jwt<'static, T> { + pub fn sign( + header: JsonWebSignatureHeader, + payload: T, + key: &K, + ) -> Result + where + K: Signer, + S: Signature, + T: Serialize, + { + let header_ = serde_json::to_vec(&header).map_err(JwtSignatureError::encode_header)?; + let header_ = Base64UrlUnpadded::encode_string(&header_); + + let payload_ = serde_json::to_vec(&payload).map_err(JwtSignatureError::encode_payload)?; + let payload_ = Base64UrlUnpadded::encode_string(&payload_); + + let mut inner = format!("{}.{}", header_, payload_); + + let first_dot = header_.len(); + let second_dot = inner.len(); + + let signature = key.sign(inner.as_bytes()).as_bytes().to_vec(); + let signature_ = Base64UrlUnpadded::encode_string(&signature); + inner.reserve_exact(1 + signature_.len()); + inner.push('.'); + inner.push_str(&signature_); + + let raw = RawJwt::new(inner, first_dot, second_dot); + + Ok(Self { + raw, + header, + payload, + signature, + }) + } +} + +#[cfg(test)] +mod tests { + use mas_iana::jose::JsonWebSignatureAlg; + use rand::thread_rng; + + use super::*; + + #[test] + fn test_jwt_decode() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + let jwt: Jwt<'_, serde_json::Value> = Jwt::try_from(jwt).unwrap(); + assert_eq!(jwt.raw.header(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"); + assert_eq!( + jwt.raw.payload(), + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" + ); + assert_eq!( + jwt.raw.signature(), + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + ); + assert_eq!(jwt.raw.signed_part(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"); + } + + #[test] + fn test_jwt_sign_and_verify() { + let header = JsonWebSignatureHeader::new(JsonWebSignatureAlg::Es256); + let payload = serde_json::json!({"hello": "world"}); + + let key = ecdsa::SigningKey::::random(&mut thread_rng()); + let signed = Jwt::sign(header, payload, &key).unwrap(); + signed.verify(&key.verifying_key()).unwrap(); + } +} diff --git a/crates/jose/src/keystore/jwks/dynamic_store.rs b/crates/jose/src/keystore/jwks/dynamic_store.rs index c613be0f..83aa3588 100644 --- a/crates/jose/src/keystore/jwks/dynamic_store.rs +++ b/crates/jose/src/keystore/jwks/dynamic_store.rs @@ -24,7 +24,7 @@ use tower::{ }; use super::StaticJwksStore; -use crate::{JsonWebKeySet, JwtHeader, VerifyingKeystore}; +use crate::{JsonWebKeySet, JsonWebSignatureHeader, VerifyingKeystore}; #[derive(Debug, Error)] pub enum Error { @@ -121,7 +121,12 @@ impl VerifyingKeystore for DynamicJwksStore { type Error = Error; type Future = BoxFuture<'static, Result<(), Self::Error>>; - fn verify(&self, header: &JwtHeader, payload: &[u8], signature: &[u8]) -> Self::Future { + fn verify( + &self, + header: &JsonWebSignatureHeader, + payload: &[u8], + signature: &[u8], + ) -> Self::Future { let cache = self.cache.clone(); let exporter = self.exporter.clone(); let header = header.clone(); diff --git a/crates/jose/src/keystore/jwks/static_store.rs b/crates/jose/src/keystore/jwks/static_store.rs index 8f30df1b..7c51226b 100644 --- a/crates/jose/src/keystore/jwks/static_store.rs +++ b/crates/jose/src/keystore/jwks/static_store.rs @@ -21,7 +21,7 @@ use sha2::{Sha256, Sha384, Sha512}; use signature::{Signature, Verifier}; use thiserror::Error; -use crate::{JsonWebKey, JsonWebKeySet, JwtHeader, VerifyingKeystore}; +use crate::{JsonWebKey, JsonWebKeySet, JsonWebSignatureHeader, VerifyingKeystore}; #[derive(Debug, Error)] pub enum Error { @@ -154,7 +154,7 @@ impl StaticJwksStore { #[tracing::instrument(skip(self))] fn verify_sync( &self, - header: &JwtHeader, + header: &JsonWebSignatureHeader, payload: &[u8], signature: &[u8], ) -> Result<(), Error> { @@ -227,7 +227,12 @@ impl VerifyingKeystore for StaticJwksStore { type Error = Error; type Future = Ready>; - fn verify(&self, header: &JwtHeader, payload: &[u8], signature: &[u8]) -> Self::Future { + fn verify( + &self, + header: &JsonWebSignatureHeader, + payload: &[u8], + signature: &[u8], + ) -> Self::Future { std::future::ready(self.verify_sync(header, payload, signature)) } } diff --git a/crates/jose/src/keystore/shared_secret.rs b/crates/jose/src/keystore/shared_secret.rs index c24441b2..8b0023ef 100644 --- a/crates/jose/src/keystore/shared_secret.rs +++ b/crates/jose/src/keystore/shared_secret.rs @@ -23,7 +23,7 @@ use sha2::{Sha256, Sha384, Sha512}; use thiserror::Error; use super::{SigningKeystore, VerifyingKeystore}; -use crate::JwtHeader; +use crate::JsonWebSignatureHeader; #[derive(Debug, Error)] pub enum Error { @@ -50,7 +50,7 @@ impl<'a> SharedSecret<'a> { fn verify_sync( &self, - header: &JwtHeader, + header: &JsonWebSignatureHeader, payload: &[u8], signature: &[u8], ) -> Result<(), Error> { @@ -92,7 +92,10 @@ impl<'a> SigningKeystore for SharedSecret<'a> { algorithms } - async fn prepare_header(&self, alg: JsonWebSignatureAlg) -> anyhow::Result { + async fn prepare_header( + &self, + alg: JsonWebSignatureAlg, + ) -> anyhow::Result { if !matches!( alg, JsonWebSignatureAlg::Hs256 | JsonWebSignatureAlg::Hs384 | JsonWebSignatureAlg::Hs512, @@ -100,10 +103,10 @@ impl<'a> SigningKeystore for SharedSecret<'a> { bail!("unsupported algorithm") } - Ok(JwtHeader::new(alg)) + Ok(JsonWebSignatureHeader::new(alg)) } - async fn sign(&self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result> { + async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result> { // TODO: do the signing in a blocking task // TODO: should we bail out if the key is too small? let signature = match header.alg() { @@ -136,7 +139,12 @@ impl<'a> VerifyingKeystore for SharedSecret<'a> { type Error = Error; type Future = Ready>; - fn verify(&self, header: &JwtHeader, payload: &[u8], signature: &[u8]) -> Self::Future { + fn verify( + &self, + header: &JsonWebSignatureHeader, + payload: &[u8], + signature: &[u8], + ) -> Self::Future { std::future::ready(self.verify_sync(header, payload, signature)) } } diff --git a/crates/jose/src/keystore/static_keystore.rs b/crates/jose/src/keystore/static_keystore.rs index ab011cff..5699ea1e 100644 --- a/crates/jose/src/keystore/static_keystore.rs +++ b/crates/jose/src/keystore/static_keystore.rs @@ -34,7 +34,7 @@ use signature::{Signature, Signer, Verifier}; use tower::Service; use super::{SigningKeystore, VerifyingKeystore}; -use crate::{JsonWebKey, JsonWebKeySet, JwtHeader}; +use crate::{JsonWebKey, JsonWebKeySet, JsonWebSignatureHeader}; // Generate with // openssl genrsa 2048 @@ -132,7 +132,7 @@ impl StaticKeystore { fn verify_sync( &self, - header: &JwtHeader, + header: &JsonWebSignatureHeader, payload: &[u8], signature: &[u8], ) -> anyhow::Result<()> { @@ -245,8 +245,11 @@ impl SigningKeystore for StaticKeystore { algorithms } - async fn prepare_header(&self, alg: JsonWebSignatureAlg) -> anyhow::Result { - let header = JwtHeader::new(alg); + async fn prepare_header( + &self, + alg: JsonWebSignatureAlg, + ) -> anyhow::Result { + let header = JsonWebSignatureHeader::new(alg); let kid = match alg { JsonWebSignatureAlg::Rs256 @@ -267,7 +270,7 @@ impl SigningKeystore for StaticKeystore { Ok(header.with_kid(kid)) } - async fn sign(&self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result> { + async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result> { let kid = header .kid() .ok_or_else(|| anyhow::anyhow!("missing kid from the JWT header"))?; @@ -350,7 +353,12 @@ impl VerifyingKeystore for StaticKeystore { type Error = anyhow::Error; type Future = Ready>; - fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future { + fn verify( + &self, + header: &JsonWebSignatureHeader, + msg: &[u8], + signature: &[u8], + ) -> Self::Future { std::future::ready(self.verify_sync(header, msg, signature)) } } diff --git a/crates/jose/src/keystore/traits.rs b/crates/jose/src/keystore/traits.rs index 5e75a392..ae678753 100644 --- a/crates/jose/src/keystore/traits.rs +++ b/crates/jose/src/keystore/traits.rs @@ -22,22 +22,26 @@ use futures_util::{ use mas_iana::jose::JsonWebSignatureAlg; use thiserror::Error; -use crate::JwtHeader; +use crate::JsonWebSignatureHeader; #[async_trait] pub trait SigningKeystore { fn supported_algorithms(&self) -> HashSet; - async fn prepare_header(&self, alg: JsonWebSignatureAlg) -> anyhow::Result; + async fn prepare_header( + &self, + alg: JsonWebSignatureAlg, + ) -> anyhow::Result; - async fn sign(&self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result>; + async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result>; } pub trait VerifyingKeystore { type Error; type Future: Future>; - fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future; + fn verify(&self, header: &JsonWebSignatureHeader, msg: &[u8], signature: &[u8]) + -> Self::Future; } #[derive(Debug, Error)] @@ -61,7 +65,12 @@ where MapErr Self::Error>, >; - fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future { + fn verify( + &self, + header: &JsonWebSignatureHeader, + msg: &[u8], + signature: &[u8], + ) -> Self::Future { match self { Either::Left(left) => Either::Left( left.verify(header, msg, signature) @@ -83,7 +92,12 @@ where type Error = T::Error; type Future = T::Future; - fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future { + fn verify( + &self, + header: &JsonWebSignatureHeader, + msg: &[u8], + signature: &[u8], + ) -> Self::Future { self.as_ref().verify(header, msg, signature) } } diff --git a/crates/jose/src/lib.rs b/crates/jose/src/lib.rs index 41ac03b7..a2b671e4 100644 --- a/crates/jose/src/lib.rs +++ b/crates/jose/src/lib.rs @@ -26,7 +26,7 @@ pub use futures_util::future::Either; pub use self::{ jwk::{JsonWebKey, JsonWebKeySet}, - jwt::{DecodedJsonWebToken, JsonWebTokenParts, JwtHeader}, + jwt::{DecodedJsonWebToken, JsonWebSignatureHeader, JsonWebTokenParts, Jwt, JwtSignatureError}, keystore::{ DynamicJwksStore, SharedSecret, SigningKeystore, StaticJwksStore, StaticKeystore, VerifyingKeystore,