diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa73f57..35196266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## 10.0.0 (unreleased) -- Introduce alternative crypto backends (AWS-LC, RustCrypto) +- BREAKING: now using traits for crypto backends, you have to choose between `aws_lc_rs` and `rust_crypto` +- Add `Clone` bound to `decode` +- Support decoding byte slices +- Support JWS ## 9.3.1 (2024-02-06) diff --git a/Cargo.toml b/Cargo.toml index 44b8ae28..00442397 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebtoken" -version = "9.3.1" +version = "10.0.0" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index 181885ae..4bfe424c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ See [JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token) for more inf Add the following to Cargo.toml: ```toml +# You will have to select either `aws_lc_rs` or `rust_crypto` as backend if you're not using your own jsonwebtoken = { version = "10", features = ["aws_lc_rs"] } # If you do not need pem decoding, you can disable the default feature `use_pem` that way: # jsonwebtoken = {version = "10", default-features = false, features = ["aws_lc_rs"] } @@ -110,6 +111,28 @@ RSA/EC, the key should always be the content of the private key in PEM or DER fo If your key is in PEM format, it is better performance wise to generate the `EncodingKey` 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. + +### JWK Thumbprints + +If you have a JWK object, you can generate a thumbprint like + +``` +let tp = my_jwk.thumbprint(&jsonwebtoken::DIGEST_SHA256); +``` + ### Decoding ```rust diff --git a/examples/custom_header.rs b/examples/custom_header.rs index 18f3cb81..0cb53afe 100644 --- a/examples/custom_header.rs +++ b/examples/custom_header.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use jsonwebtoken::errors::ErrorKind; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct Claims { sub: String, company: String, diff --git a/examples/custom_time.rs b/examples/custom_time.rs index fbf92b96..eba4e80d 100644 --- a/examples/custom_time.rs +++ b/examples/custom_time.rs @@ -5,7 +5,7 @@ use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; const SECRET: &str = "some-secret"; -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] struct Claims { sub: String, #[serde(with = "jwt_numeric_date")] diff --git a/examples/ed25519.rs b/examples/ed25519.rs index 8ee8b3fa..4322d84c 100644 --- a/examples/ed25519.rs +++ b/examples/ed25519.rs @@ -7,7 +7,7 @@ use jsonwebtoken::{ decode, encode, get_current_timestamp, Algorithm, DecodingKey, EncodingKey, Validation, }; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { sub: String, exp: u64, diff --git a/examples/validation.rs b/examples/validation.rs index ed2a9a8c..2ce7321a 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use jsonwebtoken::errors::ErrorKind; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct Claims { aud: String, sub: String, diff --git a/src/algorithms.rs b/src/algorithms.rs index 9d121beb..94eb3637 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -23,7 +23,6 @@ impl AlgorithmFamily { Algorithm::RS512, Algorithm::PS256, Algorithm::PS384, - Algorithm::PS384, Algorithm::PS512, ], Self::Ec => &[Algorithm::ES256, Algorithm::ES384], @@ -32,8 +31,6 @@ impl AlgorithmFamily { } } - - /// The algorithms supported for signing/verifying JWTs #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Default, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] diff --git a/src/decoding.rs b/src/decoding.rs index f3a9862d..2b712e65 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -97,7 +97,7 @@ impl DecodingKey { pub fn family(&self) -> AlgorithmFamily { self.family } - + /// If you're using HMAC, use this. pub fn from_secret(secret: &[u8]) -> Self { DecodingKey { @@ -259,7 +259,7 @@ impl DecodingKey { /// use serde::{Deserialize, Serialize}; /// use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; /// -/// #[derive(Debug, Serialize, Deserialize)] +/// #[derive(Debug, Clone, Serialize, Deserialize)] /// struct Claims { /// sub: String, /// company: String @@ -269,11 +269,12 @@ impl DecodingKey { /// // Claims is a struct that implements Deserialize /// let token_message = decode::(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256)); /// ``` -pub fn decode( - token: &str, +pub fn decode( + token: impl AsRef<[u8]>, key: &DecodingKey, validation: &Validation, ) -> Result> { + let token = token.as_ref(); let header = decode_header(token)?; if validation.validate_signature && !validation.algorithms.contains(&header.alg) { @@ -324,20 +325,20 @@ pub fn jwt_verifier_factory( /// let token = "a.jwt.token".to_string(); /// let header = decode_header(&token); /// ``` -pub fn decode_header(token: &str) -> Result
{ - let (_, message) = expect_two!(token.rsplitn(2, '.')); - let (_, header) = expect_two!(message.rsplitn(2, '.')); +pub fn decode_header(token: impl AsRef<[u8]>) -> Result
{ + let token = token.as_ref(); + let (_, message) = expect_two!(token.rsplitn(2, |b| *b == b'.')); + let (_, header) = expect_two!(message.rsplitn(2, |b| *b == b'.')); Header::from_encoded(header) } -/// Verify the signature of a JWT, and return a 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, +pub(crate) fn verify_signature_body( + message: &[u8], + signature: &[u8], + header: &Header, validation: &Validation, verifying_provider: Box, -) -> Result<(Header, &'a str)> { +) -> Result<()> { if validation.validate_signature && validation.algorithms.is_empty() { return Err(new_error(ErrorKind::MissingAlgorithm)); } @@ -350,19 +351,31 @@ 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)); } if validation.validate_signature - && verifying_provider.verify(message.as_bytes(), &b64_decode(signature)?).is_err() + && verifying_provider.verify(message, &b64_decode(signature)?).is_err() { return Err(new_error(ErrorKind::InvalidSignature)); } + Ok(()) +} + +/// Verify the signature of a JWT, and return a header object and raw payload. +/// +/// If the token or its signature is invalid, it will return an error. +fn verify_signature<'a>( + token: &'a [u8], + validation: &Validation, + verifying_provider: Box, +) -> Result<(Header, &'a [u8])> { + let (signature, message) = expect_two!(token.rsplitn(2, |b| *b == b'.')); + let (payload, header) = expect_two!(message.rsplitn(2, |b| *b == b'.')); + let header = Header::from_encoded(header)?; + verify_signature_body(message, signature, &header, validation, verifying_provider)?; + Ok((header, payload)) } diff --git a/src/encoding.rs b/src/encoding.rs index 08c2814c..fd886ba9 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,6 +1,9 @@ use std::fmt::{Debug, Formatter}; -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::{ + engine::general_purpose::{STANDARD, URL_SAFE}, + Engine, +}; use serde::ser::Serialize; use crate::algorithms::AlgorithmFamily; @@ -36,7 +39,7 @@ use crate::crypto::rust_crypto::{ #[derive(Clone)] pub struct EncodingKey { pub(crate) family: AlgorithmFamily, - content: Vec, + pub(crate) content: Vec, } impl EncodingKey { @@ -44,7 +47,7 @@ impl EncodingKey { pub fn family(&self) -> AlgorithmFamily { self.family } - + /// If you're using a HMAC secret that is not base64, use that. pub fn from_secret(secret: &[u8]) -> Self { EncodingKey { family: AlgorithmFamily::Hmac, content: secret.to_vec() } @@ -56,6 +59,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. diff --git a/src/header.rs b/src/header.rs index 0947d468..50a654b6 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,13 +2,110 @@ use std::collections::HashMap; 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: &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. +/// +/// 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, non_camel_case_types)] +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 => 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)] @@ -65,7 +162,27 @@ 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). + #[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, /// Any additional non-standard headers not defined in [RFC7515#4.1](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1). /// Once serialized, all keys will be converted to fields at the root level of the header payload /// Ex: Dict("custom" -> "header") will be converted to "{"typ": "JWT", ..., "custom": "header"}" @@ -87,6 +204,11 @@ impl Header { x5c: None, x5t: None, x5t_s256: None, + crit: None, + enc: None, + zip: None, + url: None, + nonce: None, extras: Default::default(), } } diff --git a/src/jwk.rs b/src/jwk.rs index 15501cf0..3b190c92 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -8,11 +8,25 @@ use std::{fmt, str::FromStr}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use crate::serialization::b64_encode; use crate::{ errors::{self, Error, ErrorKind}, - Algorithm, + Algorithm, EncodingKey, }; +#[cfg(feature = "aws_lc_rs")] +use aws_lc_rs::{digest, signature as aws_sig}; +#[cfg(feature = "aws_lc_rs")] +use aws_sig::KeyPair; +#[cfg(feature = "rust_crypto")] +use p256::{ecdsa::SigningKey as P256SigningKey, pkcs8::DecodePrivateKey}; +#[cfg(feature = "rust_crypto")] +use p384::ecdsa::SigningKey as P384SigningKey; +#[cfg(feature = "rust_crypto")] +use rsa::{pkcs1::DecodeRsaPrivateKey, traits::PublicKeyParts, RsaPrivateKey}; +#[cfg(feature = "rust_crypto")] +use sha2::{Digest, Sha256, Sha384, Sha512}; + /// The intended usage of the public `KeyType`. This enum is serialized `untagged` #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum PublicKeyUse { @@ -190,6 +204,10 @@ pub enum KeyAlgorithm { /// RSAES-OAEP-256 using SHA-2 #[serde(rename = "RSA-OAEP-256")] RSA_OAEP_256, + + /// Catch-All for when the key algorithm can not be determined or is not supported + #[serde(other)] + UNKNOWN_ALGORITHM, } impl FromStr for KeyAlgorithm { @@ -404,6 +422,14 @@ pub enum AlgorithmParameters { OctetKeyPair(OctetKeyPairParameters), } +/// The function to use to hash the intermediate thumbprint data. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ThumbprintHash { + SHA256, + SHA384, + SHA512, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] pub struct Jwk { #[serde(flatten)] @@ -413,6 +439,103 @@ pub struct Jwk { pub algorithm: AlgorithmParameters, } +#[cfg(feature = "aws_lc_rs")] +fn extract_rsa_public_key_components(key_content: &[u8]) -> errors::Result<(Vec, Vec)> { + let key_pair = aws_sig::RsaKeyPair::from_der(key_content) + .map_err(|e| ErrorKind::InvalidRsaKey(e.to_string()))?; + let public = key_pair.public_key(); + let components = aws_sig::RsaPublicKeyComponents::>::from(public); + Ok((components.n, components.e)) +} + +#[cfg(feature = "rust_crypto")] +fn extract_rsa_public_key_components(key_content: &[u8]) -> errors::Result<(Vec, Vec)> { + let private_key = RsaPrivateKey::from_pkcs1_der(key_content) + .map_err(|e| ErrorKind::InvalidRsaKey(e.to_string()))?; + let public_key = private_key.to_public_key(); + Ok((public_key.n().to_bytes_be(), public_key.e().to_bytes_be())) +} + +#[cfg(feature = "aws_lc_rs")] +fn extract_ec_public_key_coordinates( + key_content: &[u8], + alg: Algorithm, +) -> errors::Result<(EllipticCurve, Vec, Vec)> { + use aws_lc_rs::signature::{ + EcdsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING, + }; + + let (signing_alg, curve, pub_elem_bytes) = match alg { + Algorithm::ES256 => (&ECDSA_P256_SHA256_FIXED_SIGNING, EllipticCurve::P256, 32), + Algorithm::ES384 => (&ECDSA_P384_SHA384_FIXED_SIGNING, EllipticCurve::P384, 48), + _ => return Err(ErrorKind::InvalidEcdsaKey.into()), + }; + + let key_pair = EcdsaKeyPair::from_pkcs8(signing_alg, key_content) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + + let pub_bytes = key_pair.public_key().as_ref(); + if pub_bytes[0] != 4 { + return Err(ErrorKind::InvalidEcdsaKey.into()); + } + + let (x, y) = pub_bytes[1..].split_at(pub_elem_bytes); + Ok((curve, x.to_vec(), y.to_vec())) +} + +#[cfg(feature = "rust_crypto")] +fn extract_ec_public_key_coordinates( + key_content: &[u8], + alg: Algorithm, +) -> errors::Result<(EllipticCurve, Vec, Vec)> { + match alg { + Algorithm::ES256 => { + let signing_key = P256SigningKey::from_pkcs8_der(key_content) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + let public_key = signing_key.verifying_key(); + let encoded = public_key.to_encoded_point(false); + match encoded.coordinates() { + p256::elliptic_curve::sec1::Coordinates::Uncompressed { x, y } => { + Ok((EllipticCurve::P256, x.to_vec(), y.to_vec())) + } + _ => Err(ErrorKind::InvalidEcdsaKey.into()), + } + } + Algorithm::ES384 => { + let signing_key = P384SigningKey::from_pkcs8_der(key_content) + .map_err(|_| ErrorKind::InvalidEcdsaKey)?; + let public_key = signing_key.verifying_key(); + let encoded = public_key.to_encoded_point(false); + match encoded.coordinates() { + p384::elliptic_curve::sec1::Coordinates::Uncompressed { x, y } => { + Ok((EllipticCurve::P384, x.to_vec(), y.to_vec())) + } + _ => Err(ErrorKind::InvalidEcdsaKey.into()), + } + } + _ => Err(ErrorKind::InvalidEcdsaKey.into()), + } +} + +#[cfg(feature = "aws_lc_rs")] +fn compute_digest(data: &[u8], hash_function: ThumbprintHash) -> Vec { + let algorithm = match hash_function { + ThumbprintHash::SHA256 => &digest::SHA256, + ThumbprintHash::SHA384 => &digest::SHA384, + ThumbprintHash::SHA512 => &digest::SHA512, + }; + digest::digest(algorithm, data).as_ref().to_vec() +} + +#[cfg(feature = "rust_crypto")] +fn compute_digest(data: &[u8], hash_function: ThumbprintHash) -> Vec { + match hash_function { + ThumbprintHash::SHA256 => Sha256::digest(data).to_vec(), + ThumbprintHash::SHA384 => Sha384::digest(data).to_vec(), + ThumbprintHash::SHA512 => Sha512::digest(data).to_vec(), + } +} + impl Jwk { /// Find whether the Algorithm is implemented and supported pub fn is_supported(&self) -> bool { @@ -421,6 +544,104 @@ impl Jwk { _ => false, } } + pub fn from_encoding_key(key: &EncodingKey, alg: Algorithm) -> crate::errors::Result { + Ok(Self { + 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 { + key_type: OctetKeyType::Octet, + value: b64_encode(&key.content), + }) + } + crate::algorithms::AlgorithmFamily::Rsa => { + let (n, e) = extract_rsa_public_key_components(&key.content)?; + AlgorithmParameters::RSA(RSAKeyParameters { + key_type: RSAKeyType::RSA, + n: b64_encode(n), + e: b64_encode(e), + }) + } + crate::algorithms::AlgorithmFamily::Ec => { + let (curve, x, y) = extract_ec_public_key_coordinates(&key.content, alg)?; + AlgorithmParameters::EllipticCurve(EllipticCurveKeyParameters { + key_type: EllipticCurveKeyType::EC, + curve, + x: b64_encode(x), + y: b64_encode(y), + }) + } + crate::algorithms::AlgorithmFamily::Ed => { + unimplemented!(); + } + }, + }) + } + + /// Compute the thumbprint of the JWK. + /// + /// Per (RFC-7638)[https://datatracker.ietf.org/doc/html/rfc7638] + pub fn thumbprint(&self, hash_function: ThumbprintHash) -> 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, + ) + } + }, + }; + b64_encode(compute_digest(pre.as_bytes(), hash_function)) + } } /// A JWK set @@ -443,7 +664,10 @@ mod tests { use serde_json::json; use wasm_bindgen_test::wasm_bindgen_test; - use crate::jwk::{AlgorithmParameters, JwkSet, OctetKeyType}; + use crate::jwk::{ + AlgorithmParameters, Jwk, JwkSet, KeyAlgorithm, OctetKeyType, RSAKeyParameters, + ThumbprintHash, + }; use crate::serialization::b64_encode; use crate::Algorithm; @@ -477,4 +701,27 @@ mod tests { _ => panic!("Unexpected key algorithm"), } } + + #[test] + fn deserialize_unknown_key_algorithm() { + let key_alg_json = json!(""); + let key_alg_result: KeyAlgorithm = + serde_json::from_value(key_alg_json).expect("Could not deserialize json"); + assert_eq!(key_alg_result, KeyAlgorithm::UNKNOWN_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(ThumbprintHash::SHA256); + assert_eq!(tp.as_str(), "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"); + } } diff --git a/src/jws.rs b/src/jws.rs new file mode 100644 index 00000000..b34c7676 --- /dev/null +++ b/src/jws.rs @@ -0,0 +1,84 @@ +//! JSON Web Signatures data type. +use std::marker::PhantomData; + +use crate::crypto::sign; +use crate::errors::{new_error, ErrorKind, Result}; +use crate::serialization::{b64_encode_part, DecodedJwtPartClaims}; +use crate::validation::validate; +use crate::{DecodingKey, EncodingKey, Header, TokenData, Validation}; + +use crate::decoding::{jwt_verifier_factory, verify_signature_body}; +use serde::de::DeserializeOwned; +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, +} + +/// 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( + 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 = sign(message.as_bytes(), key, header.alg)?; + + Ok(Jws { + protected: encoded_header, + payload: encoded_claims, + signature, + _pd: Default::default(), + }) +} + +/// Validate a received JWS and decode into the header and claims. +pub fn decode( + jws: &Jws, + key: &DecodingKey, + validation: &Validation, +) -> Result> { + let header = Header::from_encoded(&jws.protected)?; + let message = [jws.protected.as_str(), jws.payload.as_str()].join("."); + + let verifying_provider = jwt_verifier_factory(&header.alg, key)?; + verify_signature_body( + message.as_bytes(), + jws.signature.as_bytes(), + &header, + validation, + verifying_provider, + )?; + + 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/lib.rs b/src/lib.rs index 8fd84222..dbede20b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ mod encoding; pub mod errors; mod header; pub mod jwk; +pub mod jws; #[cfg(feature = "use_pem")] mod pem; mod serialization; diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index fe2fbb00..e6dbe696 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -168,3 +168,26 @@ fn roundtrip_with_jwtio_example() { .unwrap(); assert_eq!(my_claims, token_data.claims); } + +#[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() + ); +} diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 9c679e88..de4b71d7 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -242,3 +242,21 @@ fn roundtrip_with_jwtio_example_jey() { assert_eq!(my_claims, cert_token_data.claims); } } + +#[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()); +}