From fd60bf20feb2d27142a489712f47ca717b9cd34c Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 10 Jan 2024 21:05:15 +0900 Subject: [PATCH 01/10] Changes to support ACME, including JWS --- src/decoding.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ src/encoding.rs | 28 +++++++++++++++++++++++++ src/header.rs | 12 +++++++++++ src/jws.rs | 24 +++++++++++++++++++++ src/lib.rs | 5 +++-- 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/jws.rs diff --git a/src/decoding.rs b/src/decoding.rs index 8d87f03d..ea4a8bc8 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -6,6 +6,7 @@ use crate::crypto::verify; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; use crate::jwk::{AlgorithmParameters, Jwk}; +use crate::jws::Jws; #[cfg(feature = "use_pem")] use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, DecodedJwtPartClaims}; @@ -286,3 +287,58 @@ pub fn decode_header(token: &str) -> Result
{ let (_, header) = expect_two!(message.rsplitn(2, '.')); Header::from_encoded(header) } + +/// Verify signature of a JWS, and return the header object +/// +/// If the token or its signature is invalid, it will return an error. +fn verify_jws_signature( + jws: &Jws, + key: &DecodingKey, + validation: &Validation, +) -> Result
{ + if validation.validate_signature && validation.algorithms.is_empty() { + return Err(new_error(ErrorKind::MissingAlgorithm)); + } + + if validation.validate_signature { + for alg in &validation.algorithms { + if key.family != alg.family() { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + } + } + + let header = Header::from_encoded(&jws.protected)?; + + if validation.validate_signature && !validation.algorithms.contains(&header.alg) { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + + let message = [jws.protected.as_str(), jws.payload.as_str()].join("."); + + if validation.validate_signature + && !verify(&jws.signature, message.as_bytes(), key, header.alg)? + { + return Err(new_error(ErrorKind::InvalidSignature)); + } + + Ok(header) +} + +/// Validate a received JWS and decode into the header and claims. +pub fn decode_jws( + jws: &Jws, + key: &DecodingKey, + validation: &Validation, +) -> Result> { + match verify_jws_signature(jws, key, validation) { + Err(e) => Err(e), + Ok(header) => { + let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(&jws.payload)?; + let claims = decoded_claims.deserialize()?; + validate(decoded_claims.deserialize()?, validation)?; + + Ok(TokenData { header, claims }) + } + } +} diff --git a/src/encoding.rs b/src/encoding.rs index 26f5c4c3..d57ec541 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -5,6 +5,7 @@ use crate::algorithms::AlgorithmFamily; use crate::crypto; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; +use crate::jws::Jws; #[cfg(feature = "use_pem")] use crate::pem::decoder::PemEncodedKey; use crate::serialization::b64_encode_part; @@ -129,3 +130,30 @@ pub fn encode(header: &Header, claims: &T, key: &EncodingKey) -> R Ok([message, signature].join(".")) } + +/// Encode the header and claims given and sign the payload using the algorithm from the header and the key. +/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. This produces a JWS instead of +/// a JWT -- usage is similar to `encode`, see that for more details. +pub fn encode_jws( + header: &Header, + claims: Option<&T>, + key: &EncodingKey, +) -> Result> { + if key.family != header.alg.family() { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + let encoded_header = b64_encode_part(header)?; + let encoded_claims = match claims { + Some(claims) => b64_encode_part(claims)?, + None => "".to_string(), + }; + let message = [encoded_header.as_str(), encoded_claims.as_str()].join("."); + let signature = crypto::sign(message.as_bytes(), key, header.alg)?; + + Ok(Jws { + protected: encoded_header, + payload: encoded_claims, + signature: signature, + _pd: Default::default(), + }) +} diff --git a/src/header.rs b/src/header.rs index 220f0fa4..6a414434 100644 --- a/src/header.rs +++ b/src/header.rs @@ -64,6 +64,16 @@ pub struct Header { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "x5t#S256")] pub x5t_s256: Option, + /// ACME: The URL to which this JWS object is directed + /// + /// Defined in [RFC8555#6.4](https://datatracker.ietf.org/doc/html/rfc8555#section-6.4). + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// ACME: Random data for preventing replay attacks. + /// + /// Defined in [RFC8555#6.5.2](https://datatracker.ietf.org/doc/html/rfc8555#section-6.5.2). + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, } impl Header { @@ -80,6 +90,8 @@ impl Header { x5c: None, x5t: None, x5t_s256: None, + url: None, + nonce: None, } } diff --git a/src/jws.rs b/src/jws.rs new file mode 100644 index 00000000..0d1328f2 --- /dev/null +++ b/src/jws.rs @@ -0,0 +1,24 @@ +//! JSON Web Signatures data type. +use std::marker::PhantomData; + +use serde::{Deserialize, Serialize}; + +/// This is a serde-compatible JSON Web Signature structure. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Jws { + /// The base64 encoded header data. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub protected: String, + /// The base64 encoded claims data. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub payload: String, + /// The signature on the other fields. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub signature: String, + /// Unused, for associating type metadata. + #[serde(skip)] + pub _pd: PhantomData, +} diff --git a/src/lib.rs b/src/lib.rs index 0c8664bf..c7195e66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,13 +12,14 @@ mod encoding; pub mod errors; mod header; pub mod jwk; +pub mod jws; #[cfg(feature = "use_pem")] mod pem; mod serialization; mod validation; pub use algorithms::Algorithm; -pub use decoding::{decode, decode_header, DecodingKey, TokenData}; -pub use encoding::{encode, EncodingKey}; +pub use decoding::{decode, decode_header, decode_jws, DecodingKey, TokenData}; +pub use encoding::{encode, encode_jws, EncodingKey}; pub use header::Header; pub use validation::{get_current_timestamp, Validation}; From b6ef5479f8230941ff6efc64714996bfe91ad1f3 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 10 Jan 2024 22:18:26 +0900 Subject: [PATCH 02/10] Add method to generate JWK from EncodingKey --- src/encoding.rs | 2 +- src/jwk.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index 26f5c4c3..8e9b34d7 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -14,7 +14,7 @@ use crate::serialization::b64_encode_part; #[derive(Clone)] pub struct EncodingKey { pub(crate) family: AlgorithmFamily, - content: Vec, + pub(crate) content: Vec, } impl EncodingKey { diff --git a/src/jwk.rs b/src/jwk.rs index 49c58003..8c6c8b42 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -5,8 +5,14 @@ //! tweaked to remove the private bits as it's not the goal for this crate currently. use crate::{ + crypto::ecdsa::alg_to_ec_signing, errors::{self, Error, ErrorKind}, - Algorithm, + serialization::b64_encode, + Algorithm, EncodingKey, +}; +use ring::{ + rand, + signature::{self, KeyPair}, }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::{fmt, str::FromStr}; @@ -416,6 +422,72 @@ impl Jwk { pub fn is_supported(&self) -> bool { self.common.key_algorithm.unwrap().to_algorithm().is_ok() } + + pub fn from_encoding_key( + key: &EncodingKey, + algorithm: Algorithm, + ) -> crate::errors::Result { + Ok(Self { + common: CommonParameters::default(), + algorithm: match key.family { + crate::algorithms::AlgorithmFamily::Hmac => { + AlgorithmParameters::OctetKey(OctetKeyParameters { + key_type: OctetKeyType::Octet, + value: b64_encode(&key.content), + }) + } + crate::algorithms::AlgorithmFamily::Rsa => { + let key_pair = signature::RsaKeyPair::from_der(&key.content) + .map_err(|e| ErrorKind::InvalidRsaKey(e.to_string()))?; + let public = key_pair.public(); + let components = + ring::signature::RsaPublicKeyComponents::>::from(public); + AlgorithmParameters::RSA(RSAKeyParameters { + key_type: RSAKeyType::RSA, + n: b64_encode(components.n), + e: b64_encode(components.e), + }) + } + crate::algorithms::AlgorithmFamily::Ec => { + let rng = rand::SystemRandom::new(); + let key_pair = signature::EcdsaKeyPair::from_pkcs8( + alg_to_ec_signing(algorithm), + &key.content, + &rng, + )?; + // Ring has this as `ring::ec::suite_b::curve::P384.elem_scalar_seed_len` but + // it's private and not exposed via any methods AFAICT. + let pub_elem_bytes; + let curve; + match algorithm { + Algorithm::ES256 => { + pub_elem_bytes = 32; + curve = EllipticCurve::P256; + } + Algorithm::ES384 => { + pub_elem_bytes = 48; + curve = EllipticCurve::P384; + } + _ => unreachable!(), + }; + let pub_bytes = key_pair.public_key().as_ref(); + if pub_bytes[0] != 4 { + panic!("Compressed coordinates in public key!"); + } + let (x, y) = pub_bytes[1..].split_at(pub_elem_bytes); + AlgorithmParameters::EllipticCurve(EllipticCurveKeyParameters { + key_type: EllipticCurveKeyType::EC, + curve: curve, + x: b64_encode(x), + y: b64_encode(y), + }) + } + crate::algorithms::AlgorithmFamily::Ed => { + unimplemented!(); + } + }, + }) + } } /// A JWK set From 7904bb1ad28dd7c84c48d73c8f0c2dc9681c81f1 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 10 Jan 2024 22:28:07 +0900 Subject: [PATCH 03/10] Automated testing --- src/jwk.rs | 27 ++++++++++++++++++++------- tests/ecdsa/mod.rs | 23 +++++++++++++++++++++++ tests/rsa/mod.rs | 18 ++++++++++++++++++ 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/jwk.rs b/src/jwk.rs index 8c6c8b42..56235a04 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -423,12 +423,25 @@ impl Jwk { self.common.key_algorithm.unwrap().to_algorithm().is_ok() } - pub fn from_encoding_key( - key: &EncodingKey, - algorithm: Algorithm, - ) -> crate::errors::Result { + pub fn from_encoding_key(key: &EncodingKey, alg: Algorithm) -> crate::errors::Result { Ok(Self { - common: CommonParameters::default(), + common: CommonParameters { + key_algorithm: Some(match alg { + Algorithm::HS256 => KeyAlgorithm::HS256, + Algorithm::HS384 => KeyAlgorithm::HS384, + Algorithm::HS512 => KeyAlgorithm::HS512, + Algorithm::ES256 => KeyAlgorithm::ES256, + Algorithm::ES384 => KeyAlgorithm::ES384, + Algorithm::RS256 => KeyAlgorithm::RS256, + Algorithm::RS384 => KeyAlgorithm::RS384, + Algorithm::RS512 => KeyAlgorithm::RS512, + Algorithm::PS256 => KeyAlgorithm::PS256, + Algorithm::PS384 => KeyAlgorithm::PS384, + Algorithm::PS512 => KeyAlgorithm::PS512, + Algorithm::EdDSA => KeyAlgorithm::EdDSA, + }), + ..Default::default() + }, algorithm: match key.family { crate::algorithms::AlgorithmFamily::Hmac => { AlgorithmParameters::OctetKey(OctetKeyParameters { @@ -451,7 +464,7 @@ impl Jwk { crate::algorithms::AlgorithmFamily::Ec => { let rng = rand::SystemRandom::new(); let key_pair = signature::EcdsaKeyPair::from_pkcs8( - alg_to_ec_signing(algorithm), + alg_to_ec_signing(alg), &key.content, &rng, )?; @@ -459,7 +472,7 @@ impl Jwk { // it's private and not exposed via any methods AFAICT. let pub_elem_bytes; let curve; - match algorithm { + match alg { Algorithm::ES256 => { pub_elem_bytes = 32; curve = EllipticCurve::P256; diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 8c06910f..66ff22c1 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -103,6 +103,29 @@ fn ec_x_y() { assert!(res.is_ok()); } +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn ec_jwk_from_key() { + use jsonwebtoken::jwk::Jwk; + use serde_json::json; + + let privkey = include_str!("private_ecdsa_key.pem"); + let encoding_key = EncodingKey::from_ec_pem(privkey.as_ref()).unwrap(); + let jwk = Jwk::from_encoding_key(&encoding_key, Algorithm::ES256).unwrap(); + assert_eq!( + jwk, + serde_json::from_value(json!({ + "kty": "EC", + "crv": "P-256", + "x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ", + "y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4", + "alg": "ES256", + })) + .unwrap() + ); +} + #[cfg(feature = "use_pem")] #[test] #[wasm_bindgen_test] diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 3297149f..f31969c3 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -169,6 +169,24 @@ fn rsa_modulus_exponent() { assert!(res.is_ok()); } +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn rsa_jwk_from_key() { + use jsonwebtoken::jwk::Jwk; + use serde_json::json; + + let privkey = include_str!("private_rsa_key_pkcs8.pem"); + let encoding_key = EncodingKey::from_rsa_pem(privkey.as_ref()).unwrap(); + let jwk = Jwk::from_encoding_key(&encoding_key, Algorithm::RS256).unwrap(); + assert_eq!(jwk, serde_json::from_value(json!({ + "kty": "RSA", + "n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ", + "e": "AQAB", + "alg": "RS256", + })).unwrap()); +} + #[cfg(feature = "use_pem")] #[test] #[wasm_bindgen_test] From b0072fdf9595c99b3d3493dbcf4edda17a576e85 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Fri, 12 Jan 2024 20:34:11 +0900 Subject: [PATCH 04/10] Crit, zip, enc, readme, refactor --- README.md | 14 ++++++ src/decoding.rs | 55 ++++++++++------------- src/header.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index ccd4e759..f5c5fffc 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,20 @@ let token = decode::(&token, &DecodingKey::from_rsa_components(jwk["n"], If your key is in PEM format, it is better performance wise to generate the `DecodingKey` once in a `lazy_static` or something similar and reuse it. +### Encoding and decoding JWS + +JWS is handled the same way as JWT, but using `encode_jws` and `decode_jws`: + +```rust +let encoded = encode_jws(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; +my_claims = decode_jws(&encoded, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?.claims; +``` + +`encode_jws` returns a `Jws` struct which can be placed in other structs or serialized/deserialized from JSON directly. + +The generic parameter in `Jws` indicates the claims type and prevents accidentally encoding or decoding the wrong claims type +when the Jws is nested in another struct. + ### Convert SEC1 private key to PKCS8 `jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top, this is a SEC1 type and can be converted to PKCS8 like so: diff --git a/src/decoding.rs b/src/decoding.rs index ea4a8bc8..9974b42f 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -202,14 +202,13 @@ impl DecodingKey { } } -/// Verify signature of a JWT, and return header object and raw payload -/// -/// If the token or its signature is invalid, it will return an error. -fn verify_signature<'a>( - token: &'a str, +fn verify_signature_body( + header: &Header, + message: &str, + signature: &str, key: &DecodingKey, validation: &Validation, -) -> Result<(Header, &'a str)> { +) -> Result<()> { if validation.validate_signature && validation.algorithms.is_empty() { return Err(new_error(ErrorKind::MissingAlgorithm)); } @@ -222,10 +221,6 @@ fn verify_signature<'a>( } } - let (signature, message) = expect_two!(token.rsplitn(2, '.')); - let (payload, header) = expect_two!(message.rsplitn(2, '.')); - let header = Header::from_encoded(header)?; - if validation.validate_signature && !validation.algorithms.contains(&header.alg) { return Err(new_error(ErrorKind::InvalidAlgorithm)); } @@ -234,6 +229,23 @@ fn verify_signature<'a>( return Err(new_error(ErrorKind::InvalidSignature)); } + return Ok(()); +} + +/// Verify signature of a JWT, and return header object and raw payload +/// +/// If the token or its signature is invalid, it will return an error. +fn verify_signature<'a>( + token: &'a str, + key: &DecodingKey, + validation: &Validation, +) -> Result<(Header, &'a str)> { + let (signature, message) = expect_two!(token.rsplitn(2, '.')); + let (payload, header) = expect_two!(message.rsplitn(2, '.')); + let header = Header::from_encoded(header)?; + + verify_signature_body(&header, message, signature, key, validation)?; + Ok((header, payload)) } @@ -296,31 +308,10 @@ fn verify_jws_signature( key: &DecodingKey, validation: &Validation, ) -> Result
{ - if validation.validate_signature && validation.algorithms.is_empty() { - return Err(new_error(ErrorKind::MissingAlgorithm)); - } - - if validation.validate_signature { - for alg in &validation.algorithms { - if key.family != alg.family() { - return Err(new_error(ErrorKind::InvalidAlgorithm)); - } - } - } - let header = Header::from_encoded(&jws.protected)?; - - if validation.validate_signature && !validation.algorithms.contains(&header.alg) { - return Err(new_error(ErrorKind::InvalidAlgorithm)); - } - let message = [jws.protected.as_str(), jws.payload.as_str()].join("."); - if validation.validate_signature - && !verify(&jws.signature, message.as_bytes(), key, header.alg)? - { - return Err(new_error(ErrorKind::InvalidSignature)); - } + verify_signature_body(&header, &message, &jws.signature, key, validation)?; Ok(header) } diff --git a/src/header.rs b/src/header.rs index 6a414434..4ec2dfd1 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,13 +1,110 @@ use std::result; use base64::{engine::general_purpose::STANDARD, Engine}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::algorithms::Algorithm; use crate::errors::Result; use crate::jwk::Jwk; use crate::serialization::b64_decode; +const ZIP_SERIAL_DEFLATE: &'static str = "DEF"; +const ENC_A128CBC_HS256: &'static str = "A128CBC-HS256"; +const ENC_A192CBC_HS384: &'static str = "A192CBC-HS384"; +const ENC_A256CBC_HS512: &'static str = "A256CBC-HS512"; +const ENC_A128GCM: &'static str = "A128GCM"; +const ENC_A192GCM: &'static str = "A192GCM"; +const ENC_A256GCM: &'static str = "A256GCM"; + +/// Encryption algorithm for encrypted payloads. +/// +/// Defined in [RFC7516#4.1.2](https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2). +/// +/// Values defined in [RFC7518#5.1](https://datatracker.ietf.org/doc/html/rfc7518#section-5.1). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[allow(clippy::upper_case_acronyms)] +pub enum Enc { + A128CBC_HS256, + A192CBC_HS384, + A256CBC_HS512, + A128GCM, + A192GCM, + A256GCM, + Other(String), +} + +impl Serialize for Enc { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + Enc::A128CBC_HS256 => ENC_A128CBC_HS256, + Enc::A192CBC_HS384 => ENC_A192CBC_HS384, + Enc::A256CBC_HS512 => ENC_A256CBC_HS512, + Enc::A128GCM => ENC_A128GCM, + Enc::A192GCM => ENC_A192GCM, + Enc::A256GCM => ENC_A256GCM, + Enc::Other(v) => v, + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Enc { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + ENC_A128CBC_HS256 => return Ok(Enc::A128CBC_HS256), + ENC_A192CBC_HS384 => return Ok(Enc::A192CBC_HS384), + ENC_A256CBC_HS512 => return Ok(Enc::A256CBC_HS512), + ENC_A128GCM => return Ok(Enc::A128GCM), + ENC_A192GCM => return Ok(Enc::A192GCM), + ENC_A256GCM => return Ok(Enc::A256GCM), + _ => (), + } + Ok(Enc::Other(s)) + } +} +/// Compression applied to plaintext. +/// +/// Defined in [RFC7516#4.1.3](https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.3). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Zip { + Deflate, + Other(String), +} + +impl Serialize for Zip { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + Zip::Deflate => ZIP_SERIAL_DEFLATE, + Zip::Other(v) => v, + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Zip { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + ZIP_SERIAL_DEFLATE => return Ok(Zip::Deflate), + _ => (), + } + Ok(Zip::Other(s)) + } +} + /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] @@ -64,6 +161,17 @@ pub struct Header { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "x5t#S256")] pub x5t_s256: Option, + /// Critical - indicates header fields that must be understood by the receiver. + /// + /// Defined in [RFC7515#4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). + #[serde(skip_serializing_if = "Option::is_none")] + pub crit: Option>, + /// See `Enc` for description. + #[serde(skip_serializing_if = "Option::is_none")] + pub enc: Option, + /// See `Zip` for description. + #[serde(skip_serializing_if = "Option::is_none")] + pub zip: Option, /// ACME: The URL to which this JWS object is directed /// /// Defined in [RFC8555#6.4](https://datatracker.ietf.org/doc/html/rfc8555#section-6.4). @@ -90,6 +198,9 @@ impl Header { x5c: None, x5t: None, x5t_s256: None, + crit: None, + enc: None, + zip: None, url: None, nonce: None, } From 5a3b07f380bd9310854dd3aa11190ac1d81ccb92 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 14 Jan 2024 19:58:17 +0900 Subject: [PATCH 05/10] Add support for JWK thumbprints --- README.md | 8 +++++++ src/jwk.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 4 ++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ccd4e759..043ef059 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,14 @@ let token = decode::(&token, &DecodingKey::from_rsa_components(jwk["n"], If your key is in PEM format, it is better performance wise to generate the `DecodingKey` once in a `lazy_static` or something similar and reuse it. +### JWK Thumbprints + +If you have a JWK object, you can generate a thumbprint like + +``` +let tp = my_jwk.thumbprint(&jsonwebtoken::DIGEST_SHA256); +``` + ### Convert SEC1 private key to PKCS8 `jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top, this is a SEC1 type and can be converted to PKCS8 like so: diff --git a/src/jwk.rs b/src/jwk.rs index 49c58003..eeeb1c00 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -6,9 +6,11 @@ use crate::{ errors::{self, Error, ErrorKind}, + serialization::b64_encode, Algorithm, }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::json; use std::{fmt, str::FromStr}; /// The intended usage of the public `KeyType`. This enum is serialized `untagged` @@ -416,6 +418,55 @@ impl Jwk { pub fn is_supported(&self) -> bool { self.common.key_algorithm.unwrap().to_algorithm().is_ok() } + + /// Compute the thumbprint of the JWK. + /// + /// Per (RFC-7638)[https://datatracker.ietf.org/doc/html/rfc7638] + pub fn thumbprint(&self, hash_function: &'static ring::digest::Algorithm) -> String { + let pre = match &self.algorithm { + AlgorithmParameters::EllipticCurve(a) => match a.curve { + EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 => { + format!( + r#"{{"crv":{},"kty":{},"x":"{}","y":"{}"}}"#, + serde_json::to_string(&a.curve).unwrap(), + serde_json::to_string(&a.key_type).unwrap(), + a.x, + a.y, + ) + } + EllipticCurve::Ed25519 => panic!("EllipticCurve can't contain this curve type"), + }, + AlgorithmParameters::RSA(a) => { + format!( + r#"{{"e":"{}","kty":{},"n":"{}"}}"#, + a.e, + serde_json::to_string(&a.key_type).unwrap(), + a.n, + ) + } + AlgorithmParameters::OctetKey(a) => { + format!( + r#"{{"k":"{}","kty":{}}}"#, + a.value, + serde_json::to_string(&a.key_type).unwrap() + ) + } + AlgorithmParameters::OctetKeyPair(a) => match a.curve { + EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 => { + panic!("OctetKeyPair can't contain this curve type") + } + EllipticCurve::Ed25519 => { + format!( + r#"{{crv:{},"kty":{},"x":"{}"}}"#, + serde_json::to_string(&a.curve).unwrap(), + serde_json::to_string(&a.key_type).unwrap(), + a.x, + ) + } + }, + }; + return b64_encode(ring::digest::digest(hash_function, &pre.as_bytes())); + } } /// A JWK set @@ -435,7 +486,7 @@ impl JwkSet { #[cfg(test)] mod tests { - use crate::jwk::{AlgorithmParameters, JwkSet, OctetKeyType}; + use crate::jwk::{AlgorithmParameters, Jwk, JwkSet, OctetKeyType, RSAKeyParameters}; use crate::serialization::b64_encode; use crate::Algorithm; use serde_json::json; @@ -471,4 +522,19 @@ mod tests { _ => panic!("Unexpected key algorithm"), } } + + #[test] + #[wasm_bindgen_test] + fn check_thumbprint() { + let tp = Jwk { + common: crate::jwk::CommonParameters { key_id: Some("2011-04-29".to_string()), ..Default::default() }, + algorithm: AlgorithmParameters::RSA(RSAKeyParameters { + key_type: crate::jwk::RSAKeyType::RSA, + n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw".to_string(), + e: "AQAB".to_string(), + }), + } + .thumbprint(&ring::digest::SHA256); + assert_eq!(tp.as_str(), "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"); + } } diff --git a/src/lib.rs b/src/lib.rs index 0c8664bf..d736b39a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,3 +22,7 @@ pub use decoding::{decode, decode_header, DecodingKey, TokenData}; pub use encoding::{encode, EncodingKey}; pub use header::Header; pub use validation::{get_current_timestamp, Validation}; + +pub use ring::digest::SHA256 as DIGEST_SHA256; +pub use ring::digest::SHA384 as DIGEST_SHA384; +pub use ring::digest::SHA512 as DIGEST_SHA512; From c64cbe21e00e60690c005637af511828c5ae20cb Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 15 Jan 2024 20:10:20 +0900 Subject: [PATCH 06/10] Add thumbprint hash enum, clippy fixes --- src/jwk.rs | 21 ++++++++++++++++----- src/lib.rs | 4 ---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/jwk.rs b/src/jwk.rs index eeeb1c00..0b8ce9df 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -10,7 +10,6 @@ use crate::{ Algorithm, }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::json; use std::{fmt, str::FromStr}; /// The intended usage of the public `KeyType`. This enum is serialized `untagged` @@ -404,6 +403,13 @@ pub enum AlgorithmParameters { OctetKeyPair(OctetKeyPairParameters), } +/// The function to use to hash the intermediate thumbprint data. +pub enum ThumbprintHash { + SHA256, + SHA384, + SHA512 +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] pub struct Jwk { #[serde(flatten)] @@ -422,7 +428,12 @@ impl Jwk { /// Compute the thumbprint of the JWK. /// /// Per (RFC-7638)[https://datatracker.ietf.org/doc/html/rfc7638] - pub fn thumbprint(&self, hash_function: &'static ring::digest::Algorithm) -> String { + pub fn thumbprint(&self, hash_function: ThumbprintHash) -> String { + let hash_function = match hash_function { + ThumbprintHash::SHA256 => &ring::digest::SHA256, + ThumbprintHash::SHA384 => &ring::digest::SHA384, + ThumbprintHash::SHA512 => &ring::digest::SHA512, + }; let pre = match &self.algorithm { AlgorithmParameters::EllipticCurve(a) => match a.curve { EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 => { @@ -465,7 +476,7 @@ impl Jwk { } }, }; - return b64_encode(ring::digest::digest(hash_function, &pre.as_bytes())); + return b64_encode(ring::digest::digest(hash_function, pre.as_bytes())); } } @@ -486,7 +497,7 @@ impl JwkSet { #[cfg(test)] mod tests { - use crate::jwk::{AlgorithmParameters, Jwk, JwkSet, OctetKeyType, RSAKeyParameters}; + use crate::jwk::{AlgorithmParameters, Jwk, JwkSet, OctetKeyType, RSAKeyParameters, ThumbprintHash}; use crate::serialization::b64_encode; use crate::Algorithm; use serde_json::json; @@ -534,7 +545,7 @@ mod tests { e: "AQAB".to_string(), }), } - .thumbprint(&ring::digest::SHA256); + .thumbprint(ThumbprintHash::SHA256); assert_eq!(tp.as_str(), "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"); } } diff --git a/src/lib.rs b/src/lib.rs index d736b39a..0c8664bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,3 @@ pub use decoding::{decode, decode_header, DecodingKey, TokenData}; pub use encoding::{encode, EncodingKey}; pub use header::Header; pub use validation::{get_current_timestamp, Validation}; - -pub use ring::digest::SHA256 as DIGEST_SHA256; -pub use ring::digest::SHA384 as DIGEST_SHA384; -pub use ring::digest::SHA512 as DIGEST_SHA512; From dbc9dbded4ce512ac21751ffd7ad77af7089ba39 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 15 Jan 2024 20:14:13 +0900 Subject: [PATCH 07/10] Clippy --- src/decoding.rs | 2 +- src/encoding.rs | 2 +- src/header.rs | 21 ++++++++++----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/decoding.rs b/src/decoding.rs index 9974b42f..6f87fe61 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -229,7 +229,7 @@ fn verify_signature_body( return Err(new_error(ErrorKind::InvalidSignature)); } - return Ok(()); + Ok(()) } /// Verify signature of a JWT, and return header object and raw payload diff --git a/src/encoding.rs b/src/encoding.rs index d57ec541..dda26c4c 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -153,7 +153,7 @@ pub fn encode_jws( Ok(Jws { protected: encoded_header, payload: encoded_claims, - signature: signature, + signature, _pd: Default::default(), }) } diff --git a/src/header.rs b/src/header.rs index 4ec2dfd1..16e7bde3 100644 --- a/src/header.rs +++ b/src/header.rs @@ -8,13 +8,13 @@ use crate::errors::Result; use crate::jwk::Jwk; use crate::serialization::b64_decode; -const ZIP_SERIAL_DEFLATE: &'static str = "DEF"; -const ENC_A128CBC_HS256: &'static str = "A128CBC-HS256"; -const ENC_A192CBC_HS384: &'static str = "A192CBC-HS384"; -const ENC_A256CBC_HS512: &'static str = "A256CBC-HS512"; -const ENC_A128GCM: &'static str = "A128GCM"; -const ENC_A192GCM: &'static str = "A192GCM"; -const ENC_A256GCM: &'static str = "A256GCM"; +const ZIP_SERIAL_DEFLATE: &str = "DEF"; +const ENC_A128CBC_HS256: &str = "A128CBC-HS256"; +const ENC_A192CBC_HS384: &str = "A192CBC-HS384"; +const ENC_A256CBC_HS512: &str = "A256CBC-HS512"; +const ENC_A128GCM: &str = "A128GCM"; +const ENC_A192GCM: &str = "A192GCM"; +const ENC_A256GCM: &str = "A256GCM"; /// Encryption algorithm for encrypted payloads. /// @@ -22,7 +22,7 @@ const ENC_A256GCM: &'static str = "A256GCM"; /// /// Values defined in [RFC7518#5.1](https://datatracker.ietf.org/doc/html/rfc7518#section-5.1). #[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[allow(clippy::upper_case_acronyms)] +#[allow(clippy::upper_case_acronyms, non_camel_case_types)] pub enum Enc { A128CBC_HS256, A192CBC_HS384, @@ -98,10 +98,9 @@ impl<'de> Deserialize<'de> for Zip { { let s = String::deserialize(deserializer)?; match s.as_str() { - ZIP_SERIAL_DEFLATE => return Ok(Zip::Deflate), - _ => (), + ZIP_SERIAL_DEFLATE => Ok(Zip::Deflate), + _ => Ok(Zip::Other(s)), } - Ok(Zip::Other(s)) } } From f2f070b77bd99e9fab6eb8a783ff088496934e1b Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 15 Jan 2024 20:15:24 +0900 Subject: [PATCH 08/10] Clippy --- src/jwk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jwk.rs b/src/jwk.rs index 56235a04..df1c8a8e 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -490,7 +490,7 @@ impl Jwk { let (x, y) = pub_bytes[1..].split_at(pub_elem_bytes); AlgorithmParameters::EllipticCurve(EllipticCurveKeyParameters { key_type: EllipticCurveKeyType::EC, - curve: curve, + curve, x: b64_encode(x), y: b64_encode(y), }) From a1cf7b7fb98be62b28b73904394eb6150f43a52a Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 15 Jan 2024 21:14:26 +0900 Subject: [PATCH 09/10] Format fixes --- src/jwk.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/jwk.rs b/src/jwk.rs index 0b8ce9df..9a656bb9 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -407,7 +407,7 @@ pub enum AlgorithmParameters { pub enum ThumbprintHash { SHA256, SHA384, - SHA512 + SHA512, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] @@ -497,7 +497,9 @@ impl JwkSet { #[cfg(test)] mod tests { - use crate::jwk::{AlgorithmParameters, Jwk, JwkSet, OctetKeyType, RSAKeyParameters, ThumbprintHash}; + use crate::jwk::{ + AlgorithmParameters, Jwk, JwkSet, OctetKeyType, RSAKeyParameters, ThumbprintHash, + }; use crate::serialization::b64_encode; use crate::Algorithm; use serde_json::json; From b8cf8b2cd4a43e1e35b4d939a9df27a9646259bc Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 17 Jan 2024 20:26:05 +0900 Subject: [PATCH 10/10] Method to load urlsafe base64 hmac encoding key --- src/encoding.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/encoding.rs b/src/encoding.rs index dda26c4c..3be53524 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,4 +1,7 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::{ + engine::general_purpose::{STANDARD, URL_SAFE}, + Engine, +}; use serde::ser::Serialize; use crate::algorithms::AlgorithmFamily; @@ -30,6 +33,12 @@ impl EncodingKey { Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out }) } + /// For loading websafe base64 HMAC secrets, ex: ACME EAB credentials. + pub fn from_urlsafe_base64_secret(secret: &str) -> Result { + let out = URL_SAFE.decode(secret)?; + Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out }) + } + /// If you are loading a RSA key from a .pem file. /// This errors if the key is not a valid RSA key. /// Only exists if the feature `use_pem` is enabled.