From 6d3bc15e0002792a9d78c2b7bec24c2a716b4a41 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 23 Nov 2023 06:56:35 +0000 Subject: [PATCH 1/3] Bug: JWK must render all keys in alphabetical order --- src/jose.rs | 24 +++++++++++++++--------- src/key.rs | 25 ++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/jose.rs b/src/jose.rs index 7ee6ff8..b112ff0 100644 --- a/src/jose.rs +++ b/src/jose.rs @@ -193,11 +193,15 @@ impl DerivedKey where Builder: KeyDerivedBuilder, >::Value: Serialize, + Key: Clone, { - fn parameter(&self, key: &str) -> Option { + fn parameter(&self) -> Option { match self { DerivedKey::Omit => None, - DerivedKey::Derived(_) => Some(json!(format!("<{key}>"))), + DerivedKey::Derived(key) => Some( + serde_json::to_value(Builder::from(key.clone()).build()) + .expect("failed to serialize derived key"), + ), DerivedKey::Explicit(value) => serde_json::to_value(value).ok(), } } @@ -265,20 +269,22 @@ where #[cfg(feature = "fmt")] impl Signed where - Key: SerializeJWK, + Key: SerializeJWK + Clone, { fn parameters(&self) -> serde_json::Value { let mut data = json!({}); - if let Some(value) = self.key.parameter("jwk") { + data["alg"] = serde_json::to_value(self.algorithm).unwrap(); + + if let Some(value) = self.key.parameter() { data["jwk"] = value; } - if let Some(value) = self.thumbprint.parameter("x5t") { + if let Some(value) = self.thumbprint.parameter() { data["x5t"] = value; } - if let Some(value) = self.thumbprint_sha256.parameter("x5t#S256") { + if let Some(value) = self.thumbprint_sha256.parameter() { data["x5t#S256"] = value; } @@ -289,7 +295,7 @@ where #[cfg(feature = "fmt")] impl fmt::JWTFormat for Signed where - Key: SerializeJWK, + Key: SerializeJWK + Clone, { fn fmt(&self, f: &mut fmt::IndentWriter<'_, W>) -> std::fmt::Result { Base64JSON(&self.parameters()).fmt(f) @@ -692,7 +698,7 @@ where impl Header> where H: Serialize, - Key: SerializeJWK, + Key: SerializeJWK + Clone, { pub(crate) fn value(&self) -> serde_json::Value { let value = self.state.parameters(); @@ -717,7 +723,7 @@ where impl fmt::JWTFormat for Header> where H: Serialize, - Key: SerializeJWK, + Key: SerializeJWK + Clone, { fn fmt(&self, f: &mut fmt::IndentWriter<'_, W>) -> std::fmt::Result { let value = self.value(); diff --git a/src/key.rs b/src/key.rs index 64b849d..0533a78 100644 --- a/src/key.rs +++ b/src/key.rs @@ -110,7 +110,7 @@ where /// JSON Web Key in serialized form. /// /// This struct just contains the parameters of the JWK. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct JsonWebKey { #[serde(rename = "kty")] key_type: String, @@ -140,6 +140,29 @@ where } } +impl Serialize for JsonWebKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // JWKs must serialize keys in alphabetical order. + + let mut entries = self + .parameters + .iter() + .map(|(key, value)| (key.as_str(), value)) + .collect::>(); + let kty = serde_json::Value::String(self.key_type.clone()); + entries.insert("kty", &kty); + + let mut map = serializer.serialize_map(Some(entries.len()))?; + for (key, value) in entries { + map.serialize_entry(key, value)?; + } + map.end() + } +} + /// A JSON Web Key Thumbprint (RFC 7638) calculator. /// /// This type contains the raw parts to build a JWK and then digest From 813e65d1fb020b1490186120680077fdc6f47c45 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 23 Nov 2023 06:57:00 +0000 Subject: [PATCH 2/3] Feature: New Flat format which omits unprotected headers --- src/lib.rs | 2 +- src/token/formats.rs | 108 +++++++++++++++++++++++++++++++++++++++++-- src/token/mod.rs | 11 ++--- 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 350ab05..4d5dfe2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,4 +19,4 @@ pub use claims::{Claims, RegisteredClaims}; pub use fmt::JWTFormat; pub use jose::{Header, RegisteredHeader}; pub use token::Token; -pub use token::{Compact, Flat}; +pub use token::{Compact, Flat, FlatUnprotected}; diff --git a/src/token/formats.rs b/src/token/formats.rs index 6feebc8..86a1d27 100644 --- a/src/token/formats.rs +++ b/src/token/formats.rs @@ -1,6 +1,6 @@ use std::fmt::Write; -use serde::{Deserialize, Serialize}; +use serde::{ser, Deserialize, Serialize}; use super::{HasSignature, MaybeSigned}; use super::{Payload, Token}; @@ -20,13 +20,14 @@ impl Compact { } } -/// A token format that serializes the token as a single JSON object. +/// A token format that serializes the token as a single JSON object, +/// with the unprotected header as a top-level field. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Flat { +pub struct FlatUnprotected { unprotected: U, } -impl Flat { +impl FlatUnprotected { /// Creates a new `Flat` token format with the given unprotected header. pub fn new(unprotected: U) -> Self { Self { unprotected } @@ -43,6 +44,10 @@ impl Flat { } } +/// A token format that serializes the token as a single JSON object. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Flat; + /// Error returned when a token cannot be formatted as a string. /// /// This error can occur when serializing the header or payload, or @@ -103,7 +108,7 @@ struct FlatToken<'t, P, U> { signature: String, } -impl TokenFormat for Flat +impl TokenFormat for FlatUnprotected where U: Serialize, { @@ -134,3 +139,96 @@ where Ok(()) } } + +impl Serialize for Token> +where + S: HasSignature, + ::Header: Serialize, + ::HeaderState: Serialize, + U: Serialize, + P: Serialize, +{ + fn serialize(&self, serializer: Ser) -> Result + where + Ser: ser::Serializer, + { + let header = Base64JSON(self.state.header()) + .serialized_value() + .map_err(|err| ser::Error::custom(err))?; + let signature = Base64Data(self.state.signature()) + .serialized_value() + .map_err(|err| ser::Error::custom(err))?; + + let flat = FlatToken { + payload: &self.payload, + protected: header, + unprotected: &self.fmt.unprotected, + signature, + }; + + flat.serialize(serializer) + } +} + +#[derive(Debug, Serialize)] +struct FlatSimpleToken<'t, P> { + payload: &'t Payload

, + protected: String, + signature: String, +} + +impl TokenFormat for Flat { + fn render( + &self, + writer: &mut impl Write, + token: &Token, + ) -> Result<(), TokenFormattingError> + where + Self: Sized, + S: HasSignature, + ::Header: Serialize, + ::HeaderState: Serialize, + { + let header = Base64JSON(&token.state.header()).serialized_value()?; + let signature = Base64Data(token.state.signature()).serialized_value()?; + + let flat = FlatSimpleToken { + payload: &token.payload, + protected: header, + signature, + }; + + let data = serde_json::to_string(&flat)?; + write!(writer, "{}", data)?; + + Ok(()) + } +} + +impl Serialize for Token +where + S: HasSignature, + ::Header: Serialize, + ::HeaderState: Serialize, + P: Serialize, +{ + fn serialize(&self, serializer: Ser) -> Result + where + Ser: ser::Serializer, + { + let header = Base64JSON(self.state.header()) + .serialized_value() + .map_err(|err| ser::Error::custom(err))?; + let signature = Base64Data(self.state.signature()) + .serialized_value() + .map_err(|err| ser::Error::custom(err))?; + + let flat = FlatSimpleToken { + payload: &self.payload, + protected: header, + signature, + }; + + flat.serialize(serializer) + } +} diff --git a/src/token/mod.rs b/src/token/mod.rs index 3dcb86b..db1e441 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -26,7 +26,7 @@ use crate::{ mod formats; mod state; -pub use self::formats::{Compact, Flat, TokenFormat, TokenFormattingError}; +pub use self::formats::{Compact, Flat, FlatUnprotected, TokenFormat, TokenFormattingError}; pub use self::state::{HasSignature, MaybeSigned, Signed, Unsigned, Unverified, Verified}; /// A JWT Playload. Most payloads are JSON objects, which are serialized, and then converted @@ -223,10 +223,7 @@ impl Token, Compact> { } } -impl Token, Flat> -where - U: Serialize, -{ +impl Token, Flat> { /// Create a new token with the given header and payload, in the flat format. /// /// See also [`Token::new`] and [`Token::compact`] to create a token in a specific format. @@ -234,8 +231,8 @@ where /// The flat format is the format with a JSON object containing the header, payload, and /// signature, all in the same object. It can also include additional JSON data as "unprotected"\ /// headers, which are not signed and cannot be verified. - pub fn flat(header: H, unprotected: U, payload: P) -> Token, Flat> { - Token::new(header, payload, Flat::new(unprotected)) + pub fn flat(header: H, payload: P) -> Token, Flat> { + Token::new(header, payload, Flat) } } From e7183cfa03fd70c6132d4e5c38d276e4d6b42d61 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 23 Nov 2023 07:00:07 +0000 Subject: [PATCH 3/3] Lint: Clippy fixes --- src/token/formats.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/token/formats.rs b/src/token/formats.rs index 86a1d27..394522d 100644 --- a/src/token/formats.rs +++ b/src/token/formats.rs @@ -154,10 +154,10 @@ where { let header = Base64JSON(self.state.header()) .serialized_value() - .map_err(|err| ser::Error::custom(err))?; + .map_err(ser::Error::custom)?; let signature = Base64Data(self.state.signature()) .serialized_value() - .map_err(|err| ser::Error::custom(err))?; + .map_err(ser::Error::custom)?; let flat = FlatToken { payload: &self.payload, @@ -218,10 +218,10 @@ where { let header = Base64JSON(self.state.header()) .serialized_value() - .map_err(|err| ser::Error::custom(err))?; + .map_err(ser::Error::custom)?; let signature = Base64Data(self.state.signature()) .serialized_value() - .map_err(|err| ser::Error::custom(err))?; + .map_err(ser::Error::custom)?; let flat = FlatSimpleToken { payload: &self.payload,