diff --git a/crates/jose/Cargo.toml b/crates/jose/Cargo.toml index d2522dff..da672d29 100644 --- a/crates/jose/Cargo.toml +++ b/crates/jose/Cargo.toml @@ -22,7 +22,7 @@ schemars = "0.8.12" sec1 = "0.7.3" serde.workspace = true serde_json.workspace = true -serde_with = { version = "3.3.0", features = ["base64"] } +serde_with = "3.3.0" sha2 = { version = "0.10.7", features = ["oid"] } signature = "2.1.0" thiserror.workspace = true diff --git a/crates/jose/src/base64.rs b/crates/jose/src/base64.rs new file mode 100644 index 00000000..4bf02424 --- /dev/null +++ b/crates/jose/src/base64.rs @@ -0,0 +1,168 @@ +//! Transparent base64 encoding / decoding as part of (de)serialization. + +use std::{borrow::Cow, fmt, marker::PhantomData, str}; + +use base64ct::Encoding; +use serde::{ + de::{self, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +/// A wrapper around `Vec` that (de)serializes from / to a base64 string. +/// +/// The generic parameter `C` represents the base64 flavor. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Base64 { + bytes: Vec, + // Invariant PhantomData, Send + Sync + _phantom_conf: PhantomData C>, +} + +pub type Base64UrlNoPad = Base64; + +impl Base64 { + /// Create a `Base64` instance from raw bytes, to be base64-encoded in + /// serialization. + #[must_use] + pub fn new(bytes: Vec) -> Self { + Self { + bytes, + _phantom_conf: PhantomData, + } + } + + /// Get a reference to the raw bytes held by this `Base64` instance. + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + self.bytes.as_ref() + } + + /// Encode the bytes contained in this `Base64` instance to unpadded base64. + #[must_use] + pub fn encode(&self) -> String { + C::encode_string(self.as_bytes()) + } + + /// Get the raw bytes held by this `Base64` instance. + #[must_use] + pub fn into_inner(self) -> Vec { + self.bytes + } + + /// Create a `Base64` instance containing an empty `Vec`. + #[must_use] + pub fn empty() -> Self { + Self::new(Vec::new()) + } + + /// Parse some base64-encoded data to create a `Base64` instance. + pub fn parse(encoded: &str) -> Result { + C::decode_vec(encoded).map(Self::new) + } +} + +impl fmt::Debug for Base64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.encode().fmt(f) + } +} + +impl fmt::Display for Base64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.encode().fmt(f) + } +} + +impl<'de, C: Encoding> Deserialize<'de> for Base64 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let encoded = deserialize_cow_str(deserializer)?; + Self::parse(&encoded).map_err(de::Error::custom) + } +} + +impl Serialize for Base64 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.encode()) + } +} + +/// Deserialize a `Cow<'de, str>`. +/// +/// Different from serde's implementation of `Deserialize` for `Cow` since it +/// borrows from the input when possible. +pub fn deserialize_cow_str<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_string(CowStrVisitor) +} + +struct CowStrVisitor; + +impl<'de> Visitor<'de> for CowStrVisitor { + type Value = Cow<'de, str>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a string") + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: de::Error, + { + Ok(Cow::Borrowed(v)) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result + where + E: de::Error, + { + match str::from_utf8(v) { + Ok(s) => Ok(Cow::Borrowed(s)), + Err(_) => Err(de::Error::invalid_value(Unexpected::Bytes(v), &self)), + } + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(Cow::Owned(v.to_owned())) + } + + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Ok(Cow::Owned(v)) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: de::Error, + { + match str::from_utf8(v) { + Ok(s) => Ok(Cow::Owned(s.to_owned())), + Err(_) => Err(de::Error::invalid_value(Unexpected::Bytes(v), &self)), + } + } + + fn visit_byte_buf(self, v: Vec) -> Result + where + E: de::Error, + { + match String::from_utf8(v) { + Ok(s) => Ok(Cow::Owned(s)), + Err(e) => Err(de::Error::invalid_value( + Unexpected::Bytes(&e.into_bytes()), + &self, + )), + } + } +} diff --git a/crates/jose/src/jwk/mod.rs b/crates/jose/src/jwk/mod.rs index 4e001cf2..1423cad8 100644 --- a/crates/jose/src/jwk/mod.rs +++ b/crates/jose/src/jwk/mod.rs @@ -20,14 +20,13 @@ use mas_iana::jose::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_with::{ - base64::{Base64, Standard, UrlSafe}, - formats::{Padded, Unpadded}, - serde_as, skip_serializing_none, -}; +use serde_with::skip_serializing_none; use url::Url; -use crate::constraints::{Constrainable, Constraint, ConstraintSet}; +use crate::{ + base64::{Base64, Base64UrlNoPad}, + constraints::{Constrainable, Constraint, ConstraintSet}, +}; pub(crate) mod private_parameters; pub(crate) mod public_parameters; @@ -60,7 +59,6 @@ impl JwkEcCurve for k256::Secp256k1 { const CRV: JsonWebKeyEcEllipticCurve = JsonWebKeyEcEllipticCurve::Secp256K1; } -#[serde_as] #[skip_serializing_none] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct JsonWebKey

{ @@ -85,18 +83,15 @@ pub struct JsonWebKey

{ #[schemars(with = "Vec")] #[serde(default)] - #[serde_as(as = "Option>>")] - x5c: Option>>, + x5c: Option>, #[schemars(with = "Option")] #[serde(default)] - #[serde_as(as = "Option>")] - x5t: Option>, + x5t: Option, #[schemars(with = "Option")] #[serde(default, rename = "x5t#S256")] - #[serde_as(as = "Option>")] - x5t_s256: Option>, + x5t_s256: Option, } pub type PublicJsonWebKey = JsonWebKey; diff --git a/crates/jose/src/jwk/private_parameters.rs b/crates/jose/src/jwk/private_parameters.rs index 29345038..aaa4635c 100644 --- a/crates/jose/src/jwk/private_parameters.rs +++ b/crates/jose/src/jwk/private_parameters.rs @@ -17,16 +17,11 @@ use mas_iana::jose::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_with::{ - base64::{Base64, UrlSafe}, - formats::Unpadded, - serde_as, -}; use thiserror::Error; use super::{public_parameters::JsonWebKeyPublicParameters, ParametersInfo}; +use crate::base64::Base64UrlNoPad; -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kty")] pub enum JsonWebKeyPrivateParameters { @@ -114,13 +109,11 @@ impl TryFrom for JsonWebKeyPublicParameters { } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct OctPrivateParameters { /// Key Value #[schemars(with = "String")] - #[serde_as(as = "Base64")] - k: Vec, + k: Base64UrlNoPad, } impl ParametersInfo for OctPrivateParameters { @@ -137,48 +130,39 @@ impl ParametersInfo for OctPrivateParameters { } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct RsaPrivateParameters { /// Modulus #[schemars(with = "String")] - #[serde_as(as = "Base64")] - n: Vec, + n: Base64UrlNoPad, /// Exponent #[schemars(with = "String")] - #[serde_as(as = "Base64")] - e: Vec, + e: Base64UrlNoPad, /// Private Exponent #[schemars(with = "String")] - #[serde_as(as = "Base64")] - d: Vec, + d: Base64UrlNoPad, /// First Prime Factor #[schemars(with = "String")] - #[serde_as(as = "Base64")] - p: Vec, + p: Base64UrlNoPad, /// Second Prime Factor #[schemars(with = "String")] - #[serde_as(as = "Base64")] - q: Vec, + q: Base64UrlNoPad, /// First Factor CRT Exponent #[schemars(with = "String")] - #[serde_as(as = "Base64")] - dp: Vec, + dp: Base64UrlNoPad, /// Second Factor CRT Exponent #[schemars(with = "String")] - #[serde_as(as = "Base64")] - dq: Vec, + dq: Base64UrlNoPad, /// First CRT Coefficient #[schemars(with = "String")] - #[serde_as(as = "Base64")] - qi: Vec, + qi: Base64UrlNoPad, /// Other Primes Info #[serde(skip_serializing_if = "Option::is_none")] @@ -208,23 +192,19 @@ impl From for super::public_parameters::RsaPublicParameter } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] struct RsaOtherPrimeInfo { /// Prime Factor #[schemars(with = "String")] - #[serde_as(as = "Base64")] - r: Vec, + r: Base64UrlNoPad, /// Factor CRT Exponent #[schemars(with = "String")] - #[serde_as(as = "Base64")] - d: Vec, + d: Base64UrlNoPad, /// Factor CRT Coefficient #[schemars(with = "String")] - #[serde_as(as = "Base64")] - t: Vec, + t: Base64UrlNoPad, } mod rsa_impls { @@ -244,14 +224,14 @@ mod rsa_impls { #[allow(clippy::many_single_char_names)] fn try_from(value: &RsaPrivateParameters) -> Result { - let n = BigUint::from_bytes_be(&value.n); - let e = BigUint::from_bytes_be(&value.e); - let d = BigUint::from_bytes_be(&value.d); + let n = BigUint::from_bytes_be(value.n.as_bytes()); + let e = BigUint::from_bytes_be(value.e.as_bytes()); + let d = BigUint::from_bytes_be(value.d.as_bytes()); let primes = [&value.p, &value.q] .into_iter() .chain(value.oth.iter().flatten().map(|o| &o.r)) - .map(|i| BigUint::from_bytes_be(i)) + .map(|i| BigUint::from_bytes_be(i.as_bytes())) .collect(); let key = RsaPrivateKey::from_components(n, e, d, primes)?; @@ -263,22 +243,18 @@ mod rsa_impls { } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct EcPrivateParameters { pub(crate) crv: JsonWebKeyEcEllipticCurve, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - x: Vec, + x: Base64UrlNoPad, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - y: Vec, + y: Base64UrlNoPad, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - d: Vec, + d: Base64UrlNoPad, } impl ParametersInfo for EcPrivateParameters { @@ -310,6 +286,7 @@ mod ec_impls { }; use super::{super::JwkEcCurve, EcPrivateParameters}; + use crate::base64::Base64UrlNoPad; impl TryFrom for SecretKey where @@ -328,7 +305,7 @@ mod ec_impls { type Error = elliptic_curve::Error; fn try_from(value: &EcPrivateParameters) -> Result { - SecretKey::from_slice(&value.d) + SecretKey::from_slice(value.d.as_bytes()) } } @@ -357,22 +334,20 @@ mod ec_impls { let d = key.to_bytes(); EcPrivateParameters { crv: C::CRV, - x: x.to_vec(), - y: y.to_vec(), - d: d.to_vec(), + x: Base64UrlNoPad::new(x.to_vec()), + y: Base64UrlNoPad::new(y.to_vec()), + d: Base64UrlNoPad::new(d.to_vec()), } } } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct OkpPrivateParameters { crv: JsonWebKeyOkpEllipticCurve, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - x: Vec, + x: Base64UrlNoPad, } impl ParametersInfo for OkpPrivateParameters { diff --git a/crates/jose/src/jwk/public_parameters.rs b/crates/jose/src/jwk/public_parameters.rs index 89b1c256..3f35fb75 100644 --- a/crates/jose/src/jwk/public_parameters.rs +++ b/crates/jose/src/jwk/public_parameters.rs @@ -17,15 +17,10 @@ use mas_iana::jose::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_with::{ - base64::{Base64, UrlSafe}, - formats::Unpadded, - serde_as, -}; use super::ParametersInfo; +use crate::base64::Base64UrlNoPad; -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kty")] pub enum JsonWebKeyPublicParameters { @@ -83,16 +78,13 @@ impl ParametersInfo for JsonWebKeyPublicParameters { } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct RsaPublicParameters { #[schemars(with = "String")] - #[serde_as(as = "Base64")] - n: Vec, + n: Base64UrlNoPad, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - e: Vec, + e: Base64UrlNoPad, } impl ParametersInfo for RsaPublicParameters { @@ -113,27 +105,24 @@ impl ParametersInfo for RsaPublicParameters { } impl RsaPublicParameters { - pub const fn new(n: Vec, e: Vec) -> Self { + pub const fn new(n: Base64UrlNoPad, e: Base64UrlNoPad) -> Self { Self { n, e } } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct EcPublicParameters { pub(crate) crv: JsonWebKeyEcEllipticCurve, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - x: Vec, + x: Base64UrlNoPad, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - y: Vec, + y: Base64UrlNoPad, } impl EcPublicParameters { - pub const fn new(crv: JsonWebKeyEcEllipticCurve, x: Vec, y: Vec) -> Self { + pub const fn new(crv: JsonWebKeyEcEllipticCurve, x: Base64UrlNoPad, y: Base64UrlNoPad) -> Self { Self { crv, x, y } } } @@ -154,14 +143,12 @@ impl ParametersInfo for EcPublicParameters { } } -#[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct OkpPublicParameters { crv: JsonWebKeyOkpEllipticCurve, #[schemars(with = "String")] - #[serde_as(as = "Base64")] - x: Vec, + x: Base64UrlNoPad, } impl ParametersInfo for OkpPublicParameters { @@ -175,7 +162,7 @@ impl ParametersInfo for OkpPublicParameters { } impl OkpPublicParameters { - pub const fn new(crv: JsonWebKeyOkpEllipticCurve, x: Vec) -> Self { + pub const fn new(crv: JsonWebKeyOkpEllipticCurve, x: Base64UrlNoPad) -> Self { Self { crv, x } } } @@ -184,6 +171,7 @@ mod rsa_impls { use rsa::{traits::PublicKeyParts, BigUint, RsaPublicKey}; use super::{JsonWebKeyPublicParameters, RsaPublicParameters}; + use crate::base64::Base64UrlNoPad; impl From for JsonWebKeyPublicParameters { fn from(key: RsaPublicKey) -> Self { @@ -206,8 +194,8 @@ mod rsa_impls { impl From<&RsaPublicKey> for RsaPublicParameters { fn from(key: &RsaPublicKey) -> Self { Self { - n: key.n().to_bytes_be(), - e: key.e().to_bytes_be(), + n: Base64UrlNoPad::new(key.n().to_bytes_be()), + e: Base64UrlNoPad::new(key.e().to_bytes_be()), } } } @@ -222,8 +210,8 @@ mod rsa_impls { impl TryFrom<&RsaPublicParameters> for RsaPublicKey { type Error = rsa::errors::Error; fn try_from(value: &RsaPublicParameters) -> Result { - let n = BigUint::from_bytes_be(&value.n); - let e = BigUint::from_bytes_be(&value.e); + let n = BigUint::from_bytes_be(value.n.as_bytes()); + let e = BigUint::from_bytes_be(value.e.as_bytes()); let key = RsaPublicKey::new(n, e)?; Ok(key) } @@ -239,6 +227,7 @@ mod ec_impls { }; use super::{super::JwkEcCurve, EcPublicParameters, JsonWebKeyPublicParameters}; + use crate::base64::Base64UrlNoPad; impl TryFrom<&EcPublicParameters> for PublicKey where @@ -250,10 +239,12 @@ mod ec_impls { fn try_from(value: &EcPublicParameters) -> Result { let x = value .x + .as_bytes() .get(..C::FieldBytesSize::USIZE) .ok_or(elliptic_curve::Error)?; let y = value .y + .as_bytes() .get(..C::FieldBytesSize::USIZE) .ok_or(elliptic_curve::Error)?; @@ -311,8 +302,8 @@ mod ec_impls { }; EcPublicParameters { crv: C::CRV, - x: x.to_vec(), - y: y.to_vec(), + x: Base64UrlNoPad::new(x.to_vec()), + y: Base64UrlNoPad::new(y.to_vec()), } } } diff --git a/crates/jose/src/jwt/header.rs b/crates/jose/src/jwt/header.rs index b5a0b49e..af2ad424 100644 --- a/crates/jose/src/jwt/header.rs +++ b/crates/jose/src/jwt/header.rs @@ -14,16 +14,11 @@ 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 serde_with::skip_serializing_none; use url::Url; -use crate::jwk::PublicJsonWebKey; +use crate::{base64::Base64UrlNoPad, jwk::PublicJsonWebKey, Base64}; -#[serde_as] #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct JsonWebSignatureHeader { @@ -42,16 +37,13 @@ pub struct JsonWebSignatureHeader { x5u: Option, #[serde(default)] - #[serde_as(as = "Option>>")] - x5c: Option>>, + x5c: Option>, #[serde(default)] - #[serde_as(as = "Option>")] - x5t: Option>, + x5t: Option, #[serde(default, rename = "x5t#S256")] - #[serde_as(as = "Option>")] - x5t_s256: Option>, + x5t_s256: Option, #[serde(default)] typ: Option, diff --git a/crates/jose/src/lib.rs b/crates/jose/src/lib.rs index 26cf1005..aa7599fa 100644 --- a/crates/jose/src/lib.rs +++ b/crates/jose/src/lib.rs @@ -17,8 +17,11 @@ #![warn(clippy::pedantic)] #![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)] +mod base64; pub mod claims; pub mod constraints; pub mod jwa; pub mod jwk; pub mod jwt; + +pub use self::base64::Base64;