From c41224c37dd9a38056f317b012bc8db93dca113e Mon Sep 17 00:00:00 2001 From: Rob Sliwa Date: Sun, 14 Jan 2024 09:02:34 -0500 Subject: [PATCH] WebAssembly Build Compatibility This update introduces an important modification to improve the crate's compatibility with WebAssembly builds. Previously, the crate depended on the Ring crate, which posed challenges for compiling it for WebAssembly. To address this, following changes were made: 1. **Default Feature - "ring"**: The default feature continues to use the `jsonwebtoken` library, which is dependent on the Ring crate. 2. **New Optional Feature - "noring"**: Optional feature, "noring", which leverages the `jsonwebtoken-rustcrypto` library. This alternative library implements the necessary cryptographic functionalities without the dependency on the Ring crate, making it a suitable choice for wasm32-unknown-unknown targets. --- .github/workflows/ci.yml | 4 + .gitignore | 1 + Cargo.toml | 16 +- src/decoding.rs | 104 ++++++++-- src/encoding.rs | 112 +++++++++-- src/error.rs | 14 +- src/jwk.rs | 10 +- src/lib.rs | 2 + src/registries.rs | 406 +++++++++++++++++++++++++++++++++++++++ src/test_utils.rs | 11 +- src/validation.rs | 31 +++ 11 files changed, 663 insertions(+), 48 deletions(-) create mode 100644 src/registries.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31000a2..981393f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,3 +20,7 @@ jobs: run: cargo build --verbose - name: Run tests run: cargo test --verbose + - name: Build without Ring + run: cargo build --no-default-features --features "noring" + - name: Run tests without Ring + run: cargo test --no-default-features --features "noring" diff --git a/.gitignore b/.gitignore index 4fffb2f..547b8be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +.vscode/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f8939a9..4b6e606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sdjwt" -version = "0.7.0" +version = "0.7.1" authors = ["Rob Sliwa "] license = "MIT" readme = "README.md" @@ -17,8 +17,18 @@ thiserror = "1.0.51" rand = "0.8.5" base64 = "0.21.5" sha2 = "0.10.8" -jsonwebtoken = "9.2.0" chrono = "0.4.31" +rsa = "0.8" +getrandom = { version = "0.2.12", features = ["js"] } + +jsonwebtoken-rustcrypto = { git = "https://github.com/JadedBlueEyes/jsonwebtoken", rev = "a7758b0", optional = true } +serde_plain = { version = "1.0.2", optional = true } + +jsonwebtoken = { version = "9.2.0", optional = true } + +[features] +default = ["ring"] +ring = ["jsonwebtoken"] +noring = ["jsonwebtoken-rustcrypto", "serde_plain"] [dev-dependencies] -rsa = "0.5" diff --git a/src/decoding.rs b/src/decoding.rs index fd6930a..20bec02 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -1,5 +1,20 @@ use crate::{Algorithm, Error, Validation}; -use jsonwebtoken::DecodingKey; +#[cfg(feature = "noring")] +use base64::Engine; +#[cfg(feature = "ring")] +use jsonwebtoken::{ + decode as jwt_decode, Algorithm as JwtAlgorithm, DecodingKey, Validation as JwtValidation, +}; + +#[cfg(feature = "noring")] +use jsonwebtoken_rustcrypto::{ + decode as jwt_decode, Algorithm as JwtAlgorithm, DecodingKey, Validation as JwtValidation, +}; +#[cfg(feature = "noring")] +use rsa::PublicKeyParts; +#[cfg(feature = "noring")] +use rsa::{pkcs1::DecodeRsaPublicKey, RsaPublicKey}; + use serde_json::Value; #[derive(Clone)] @@ -20,42 +35,60 @@ impl KeyForDecoding { }) } + #[cfg(feature = "ring")] pub fn from_rsa_pem(key: &[u8]) -> Result { Ok(KeyForDecoding { key: DecodingKey::from_rsa_pem(key)?, }) } + #[cfg(feature = "noring")] + pub fn from_rsa_pem(key: &[u8]) -> Result { + let rsa_key = RsaPublicKey::from_pkcs1_pem(std::str::from_utf8(key)?)?; + + let modulus = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be()); + let exponent = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be()); + + Self::from_rsa_components(&modulus, &exponent) + } + pub fn from_rsa_components(modulus: &str, exponent: &str) -> Result { Ok(KeyForDecoding { key: DecodingKey::from_rsa_components(modulus, exponent)?, }) } + #[cfg(feature = "ring")] pub fn from_ec_pem(key: &[u8]) -> Result { Ok(KeyForDecoding { key: DecodingKey::from_ec_pem(key)?, }) } + #[cfg(feature = "ring")] pub fn from_ed_pem(key: &[u8]) -> Result { Ok(KeyForDecoding { key: DecodingKey::from_ed_pem(key)?, }) } + #[cfg(feature = "ring")] pub fn from_rsa_der(der: &[u8]) -> Self { KeyForDecoding { key: DecodingKey::from_rsa_der(der), } } + #[cfg(feature = "ring")] pub fn from_ec_der(der: &[u8]) -> Self { KeyForDecoding { key: DecodingKey::from_ec_der(der), } } + #[cfg(feature = "ring")] pub fn from_ed_der(der: &[u8]) -> Self { KeyForDecoding { key: DecodingKey::from_ed_der(der), @@ -63,21 +96,23 @@ impl KeyForDecoding { } } -fn build_validation(validation: &Validation) -> jsonwebtoken::Validation { - let mut valid = jsonwebtoken::Validation::new(match validation.algorithms { - Algorithm::HS256 => jsonwebtoken::Algorithm::HS256, - Algorithm::HS384 => jsonwebtoken::Algorithm::HS384, - Algorithm::HS512 => jsonwebtoken::Algorithm::HS512, - Algorithm::RS256 => jsonwebtoken::Algorithm::RS256, - Algorithm::RS384 => jsonwebtoken::Algorithm::RS384, - Algorithm::RS512 => jsonwebtoken::Algorithm::RS512, - Algorithm::ES256 => jsonwebtoken::Algorithm::ES256, - Algorithm::ES384 => jsonwebtoken::Algorithm::ES384, - Algorithm::PS256 => jsonwebtoken::Algorithm::PS256, - Algorithm::PS384 => jsonwebtoken::Algorithm::PS384, - Algorithm::PS512 => jsonwebtoken::Algorithm::PS512, - Algorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA, +#[cfg(feature = "ring")] +fn build_validation(validation: &Validation) -> JwtValidation { + let mut valid = JwtValidation::new(match validation.algorithms { + Algorithm::HS256 => JwtAlgorithm::HS256, + Algorithm::HS384 => JwtAlgorithm::HS384, + Algorithm::HS512 => JwtAlgorithm::HS512, + Algorithm::RS256 => JwtAlgorithm::RS256, + Algorithm::RS384 => JwtAlgorithm::RS384, + Algorithm::RS512 => JwtAlgorithm::RS512, + Algorithm::ES256 => JwtAlgorithm::ES256, + Algorithm::ES384 => JwtAlgorithm::ES384, + Algorithm::PS256 => JwtAlgorithm::PS256, + Algorithm::PS384 => JwtAlgorithm::PS384, + Algorithm::PS512 => JwtAlgorithm::PS512, + Algorithm::EdDSA => JwtAlgorithm::EdDSA, }); + valid.required_spec_claims = validation.required_spec_claims.clone(); valid.leeway = validation.leeway; valid.validate_exp = validation.validate_exp; @@ -89,13 +124,39 @@ fn build_validation(validation: &Validation) -> jsonwebtoken::Validation { valid } +#[cfg(feature = "noring")] +fn build_validation(validation: &Validation) -> JwtValidation { + let mut valid = JwtValidation::new(match validation.algorithms { + Algorithm::HS256 => JwtAlgorithm::HS256, + Algorithm::HS384 => JwtAlgorithm::HS384, + Algorithm::HS512 => JwtAlgorithm::HS512, + Algorithm::RS256 => JwtAlgorithm::RS256, + Algorithm::RS384 => JwtAlgorithm::RS384, + Algorithm::RS512 => JwtAlgorithm::RS512, + Algorithm::ES256 => JwtAlgorithm::ES256, + Algorithm::ES384 => JwtAlgorithm::ES384, + Algorithm::PS256 => JwtAlgorithm::PS256, + Algorithm::PS384 => JwtAlgorithm::PS384, + Algorithm::PS512 => JwtAlgorithm::PS512, + Algorithm::EdDSA => JwtAlgorithm::EdDSA, + }); + + valid.leeway = validation.leeway; + valid.validate_exp = validation.validate_exp; + valid.validate_nbf = validation.validate_nbf; + valid.aud = validation.aud.clone(); + valid.iss = validation.iss.clone().and_then(|mut hs| hs.drain().next()); + valid.sub = validation.sub.clone(); + valid +} + pub fn decode( token: &str, key: &KeyForDecoding, validation: &Validation, ) -> Result<(Value, Value), Error> { let validation = build_validation(validation); - let token_data = jsonwebtoken::decode(token, &key.key, &validation)?; + let token_data = jwt_decode(token, &key.key, &validation)?; let header: Value = serde_json::from_str(&serde_json::to_string(&token_data.header)?)?; Ok((header, token_data.claims)) } @@ -124,7 +185,9 @@ mod tests { use super::*; use chrono::{Duration, Utc}; use rand::rngs::OsRng; - use rsa::{pkcs1::ToRsaPublicKey, pkcs8::ToPrivateKey, RsaPrivateKey, RsaPublicKey}; + use rsa::pkcs1::EncodeRsaPublicKey; + use rsa::pkcs8::EncodePrivateKey; + use rsa::{RsaPrivateKey, RsaPublicKey}; const TEST_CLAIMS: &str = r#"{ "sub": "user_42", @@ -158,8 +221,11 @@ mod tests { fn convert_to_pem(private_key: RsaPrivateKey, public_key: RsaPublicKey) -> (String, String) { ( - private_key.to_pkcs8_pem().unwrap().to_string(), - public_key.to_pkcs1_pem().unwrap(), + private_key + .to_pkcs8_pem(rsa::pkcs8::LineEnding::CR) + .unwrap() + .to_string(), + public_key.to_pkcs1_pem(rsa::pkcs1::LineEnding::CR).unwrap(), ) } diff --git a/src/encoding.rs b/src/encoding.rs index 1960df7..1fe41d0 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,7 +1,20 @@ use crate::Algorithm; use crate::Error; use crate::Header; -use jsonwebtoken::EncodingKey; +#[cfg(feature = "ring")] +use jsonwebtoken::{ + encode as jwt_encode, Algorithm as JwtAlgorithm, EncodingKey, Header as JwtHeader, +}; + +#[cfg(feature = "noring")] +use jsonwebtoken_rustcrypto::{ + encode as jwt_encode, + headers::{JwtHeader, X509Headers}, + Algorithm as JwtAlgorithm, EncodingKey, +}; +#[cfg(feature = "noring")] +use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; + use serde::Serialize; #[derive(Clone)] @@ -22,36 +35,51 @@ impl KeyForEncoding { }) } + #[cfg(feature = "ring")] pub fn from_rsa_pem(key: &[u8]) -> Result { Ok(KeyForEncoding { key: EncodingKey::from_rsa_pem(key)?, }) } + #[cfg(feature = "noring")] + pub fn from_rsa_pem(key: &[u8]) -> Result { + let rsa_key = RsaPrivateKey::from_pkcs8_pem(std::str::from_utf8(key)?)?; + + Ok(KeyForEncoding { + key: EncodingKey::from_rsa(rsa_key)?, + }) + } + + #[cfg(feature = "ring")] pub fn from_ec_pem(key: &[u8]) -> Result { Ok(KeyForEncoding { key: EncodingKey::from_ec_pem(key)?, }) } + #[cfg(feature = "ring")] pub fn from_ed_pem(key: &[u8]) -> Result { Ok(KeyForEncoding { key: EncodingKey::from_ed_pem(key)?, }) } + #[cfg(feature = "ring")] pub fn from_rsa_der(der: &[u8]) -> Self { KeyForEncoding { key: EncodingKey::from_rsa_der(der), } } + #[cfg(feature = "ring")] pub fn from_ec_der(der: &[u8]) -> Self { KeyForEncoding { key: EncodingKey::from_ec_der(der), } } + #[cfg(feature = "ring")] pub fn from_ed_der(der: &[u8]) -> Self { KeyForEncoding { key: EncodingKey::from_ed_der(der), @@ -59,26 +87,27 @@ impl KeyForEncoding { } } -fn build_header(header: &Header) -> Result { +#[cfg(feature = "ring")] +fn build_header(header: &Header) -> Result { let jwk = match &header.jwk { Some(jwk) => Some(serde_json::from_value(jwk.clone())?), None => None, }; - Ok(jsonwebtoken::Header { + Ok(JwtHeader { typ: header.typ.clone(), alg: match header.alg { - Algorithm::HS256 => jsonwebtoken::Algorithm::HS256, - Algorithm::HS384 => jsonwebtoken::Algorithm::HS384, - Algorithm::HS512 => jsonwebtoken::Algorithm::HS512, - Algorithm::RS256 => jsonwebtoken::Algorithm::RS256, - Algorithm::RS384 => jsonwebtoken::Algorithm::RS384, - Algorithm::RS512 => jsonwebtoken::Algorithm::RS512, - Algorithm::ES256 => jsonwebtoken::Algorithm::ES256, - Algorithm::ES384 => jsonwebtoken::Algorithm::ES384, - Algorithm::PS256 => jsonwebtoken::Algorithm::PS256, - Algorithm::PS384 => jsonwebtoken::Algorithm::PS384, - Algorithm::PS512 => jsonwebtoken::Algorithm::PS512, - Algorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA, + Algorithm::HS256 => JwtAlgorithm::HS256, + Algorithm::HS384 => JwtAlgorithm::HS384, + Algorithm::HS512 => JwtAlgorithm::HS512, + Algorithm::RS256 => JwtAlgorithm::RS256, + Algorithm::RS384 => JwtAlgorithm::RS384, + Algorithm::RS512 => JwtAlgorithm::RS512, + Algorithm::ES256 => JwtAlgorithm::ES256, + Algorithm::ES384 => JwtAlgorithm::ES384, + Algorithm::PS256 => JwtAlgorithm::PS256, + Algorithm::PS384 => JwtAlgorithm::PS384, + Algorithm::PS512 => JwtAlgorithm::PS512, + Algorithm::EdDSA => JwtAlgorithm::EdDSA, }, cty: header.cty.clone(), jku: header.jku.clone(), @@ -91,14 +120,57 @@ fn build_header(header: &Header) -> Result { }) } +#[cfg(feature = "noring")] +fn build_header(header: &Header) -> Result { + let jwk = match &header.jwk { + Some(jwk) => Some(serde_json::from_value(jwk.clone())?), + None => None, + }; + + let alg = match header.alg { + Algorithm::HS256 => JwtAlgorithm::HS256, + Algorithm::HS384 => JwtAlgorithm::HS384, + Algorithm::HS512 => JwtAlgorithm::HS512, + Algorithm::RS256 => JwtAlgorithm::RS256, + Algorithm::RS384 => JwtAlgorithm::RS384, + Algorithm::RS512 => JwtAlgorithm::RS512, + Algorithm::ES256 => JwtAlgorithm::ES256, + Algorithm::ES384 => JwtAlgorithm::ES384, + Algorithm::PS256 => JwtAlgorithm::PS256, + Algorithm::PS384 => JwtAlgorithm::PS384, + Algorithm::PS512 => JwtAlgorithm::PS512, + Algorithm::EdDSA => JwtAlgorithm::EdDSA, + }; + + let mut jwt_header = JwtHeader::new(alg); + jwt_header.general_headers.typ = header.typ.clone(); + jwt_header.jwk_set_headers.jku = header.jku.clone(); + jwt_header.jwk_set_headers.kid = header.kid.clone(); + jwt_header.general_headers.cty = header.cty.clone(); + jwt_header.jwk_set_headers.jwk = jwk; + + let mut x509_headers = None; + if header.x5u.is_some() + || header.x5c.is_some() + || header.x5t.is_some() + || header.x5t_s256.is_some() + { + x509_headers = Some(Box::new(X509Headers { + x5u: header.x5u.clone(), + x5c: header.x5c.clone(), + x5t: header.x5t.clone(), + x5t_s256: header.x5t_s256.clone(), + })); + } + jwt_header.x509_headers = x509_headers; + + Ok(jwt_header) +} + pub fn encode( header: &Header, claims: &T, key: &KeyForEncoding, ) -> Result { - Ok(jsonwebtoken::encode( - &build_header(header)?, - claims, - &key.key, - )?) + Ok(jwt_encode(&build_header(header)?, claims, &key.key)?) } diff --git a/src/error.rs b/src/error.rs index 64df06f..6503d3a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,12 @@ use serde_json::Error as SerdeError; use thiserror::Error; +#[cfg(feature = "ring")] +use jsonwebtoken::errors::Error as JwtError; + +#[cfg(feature = "noring")] +use jsonwebtoken_rustcrypto::errors::Error as JwtError; + #[derive(Error, Debug)] pub enum Error { #[error("failed to form disclosuer")] @@ -8,7 +14,7 @@ pub enum Error { #[error("invalid disclosure key {0}")] InvalidDisclosureKey(String), #[error("encoding key error")] - EncodingKeyError(#[from] jsonwebtoken::errors::Error), + EncodingKeyError(#[from] JwtError), #[error("invalid path pointer to disclosure")] InvalidPathPointer, #[error("invalid path pointer array index")] @@ -31,4 +37,10 @@ pub enum Error { KeyBindingJWTRequired, #[error("KB-JWT parameter missing: {0}")] KeyBindingJWTParameterMissing(String), + #[error("RSA PKCS1 error")] + RsaError(#[from] rsa::pkcs1::Error), + #[error("RSA PKCS8 error")] + RsaPkcs8Error(#[from] rsa::pkcs8::Error), + #[error("UTF8 conversion error")] + Utf8Error(#[from] std::str::Utf8Error), } diff --git a/src/jwk.rs b/src/jwk.rs index c0bb91f..a461493 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -1,9 +1,15 @@ use crate::Error; use std::ops::{Deref, DerefMut}; +#[cfg(feature = "ring")] +use jsonwebtoken::jwk::Jwk as JwtJwk; + +#[cfg(feature = "noring")] +use crate::registries::Jwk as JwtJwk; + #[derive(Debug, Clone)] pub struct Jwk { - jwk: jsonwebtoken::jwk::Jwk, + jwk: JwtJwk, } impl Jwk { @@ -15,7 +21,7 @@ impl Jwk { } impl Deref for Jwk { - type Target = jsonwebtoken::jwk::Jwk; + type Target = JwtJwk; fn deref(&self) -> &Self::Target { &self.jwk diff --git a/src/lib.rs b/src/lib.rs index d552fe7..b113a04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ pub mod header; pub mod holder; pub mod issuer; pub mod jwk; +#[cfg(feature = "noring")] +pub(crate) mod registries; mod utils; pub mod validation; pub mod verifier; diff --git a/src/registries.rs b/src/registries.rs new file mode 100644 index 0000000..ff45d6d --- /dev/null +++ b/src/registries.rs @@ -0,0 +1,406 @@ +// This comes from: https://github.com/JadedBlueEyes/jsonwebtoken/blob/a7758b08c3b63d7308539a8e50e3dd61369644e1/src/registries/mod.rs +use serde::{self, Deserialize, Serialize}; + +// See https://www.iana.org/assignments/jose/jose.xhtml +use jsonwebtoken_rustcrypto::errors::{Error, ErrorKind, Result}; +use std::str::FromStr; + +macro_rules! make_values_enum { + ( $(#[$meta:meta])* + $vis:vis enum $name:ident { + $($(#[$item_meta:meta])* $item_name:ident, $value:literal, $docstring:literal, $($spec:literal)?)* + } + ) => { + + $(#[$meta])* + #[non_exhaustive] + $vis enum $name { + $( + // $(#[depreciated = $depreciated])? + #[serde(rename = $value)] + #[doc = $docstring] + $(#[doc ="\n"] #[doc ="Spec: "] #[doc = $spec])? + $(#[$item_meta])* + $item_name + ),* + } + } +} + +// https://www.iana.org/assignments/jose/jose.xhtml#web-key-use +make_values_enum! { + /// The intended use of the key. + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Hash)] + pub enum WebKeyUse { + Signature, "sig","Digital Signature or MAC ", "RFC7517, Section 4.2" + Encryption, "enc","Encryption ", "RFC7517, Section 4.2" + } +} + +// https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms +// web-signature-encryption-algorithms.csv +make_values_enum! { + /// The possible algorithms for signing JWTs. + #[derive(Debug, PartialEq, Hash, Copy, Clone, Serialize, Deserialize, Default)] + pub enum Algorithm { +#[default] None,"none","No digital signature or MAC performed", "RFC7518, Section 3.6" +HS256,"HS256","HMAC using SHA-256", "RFC7518, Section 3.2" +HS384,"HS384","HMAC using SHA-384", "RFC7518, Section 3.2" +HS512,"HS512","HMAC using SHA-512", "RFC7518, Section 3.2" +RS256,"RS256","RSASSA-PKCS1-v1_5 using SHA-256", "RFC7518, Section 3.3" +RS384,"RS384","RSASSA-PKCS1-v1_5 using SHA-384", "RFC7518, Section 3.3" +RS512,"RS512","RSASSA-PKCS1-v1_5 using SHA-512", "RFC7518, Section 3.3" +PS256,"PS256","RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "RFC7518, Section 3.5" +PS384,"PS384","RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "RFC7518, Section 3.5" +PS512,"PS512","RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "RFC7518, Section 3.5" +Rsa15,"RSA1_5","RSAES-PKCS1-v1_5", "RFC7518, Section 4.2" +RsaOeap,"RSA-OAEP","RSAES OAEP using default parameters", "RFC7518, Section 4.3" +RsaOeap256,"RSA-OAEP-256","RSAES OAEP using SHA-256 and MGF1 with SHA-256", "RFC7518, Section 4.3" +ES256,"ES256","ECDSA using P-256 and SHA-256", "RFC7518, Section 3.4" +ES256K,"ES256K","ECDSA using secp256k1 curve and SHA-256", "RFC8812, Section 3.2" +ES384,"ES384","ECDSA using P-384 and SHA-384", "RFC7518, Section 3.4" +ES512,"ES512","ECDSA using P-521 and SHA-512", "RFC7518, Section 3.4" +EdDSA,"EdDSA","EdDSA signature algorithms", "RFC8037, Section 3.1" +EcdhEs,"ECDH-ES","ECDH-ES using Concat KDF", "RFC7518, Section 4.6" +EcdhEsA128Kw,"ECDH-ES+A128KW","ECDH-ES using Concat KDF and \"A128KW\" wrapping", "RFC7518, Section 4.6" +EcdhEsA192Kw,"ECDH-ES+A192KW","ECDH-ES using Concat KDF and \"A192KW\" wrapping", "RFC7518, Section 4.6" +EcdhEsA256Kw,"ECDH-ES+A256KW","ECDH-ES using Concat KDF and \"A256KW\" wrapping", "RFC7518, Section 4.6" +A128Kw,"A128KW","AES Key Wrap using 128-bit key", "RFC7518, Section 4.4" +A192Kw,"A192KW","AES Key Wrap using 192-bit key", "RFC7518, Section 4.4" +A256Kw,"A256KW","AES Key Wrap using 256-bit key", "RFC7518, Section 4.4" +A128GcmKw,"A128GCMKW","Key wrapping with AES GCM using 128-bit key", "RFC7518, Section 4.7" +A192GcmKw,"A192GCMKW","Key wrapping with AES GCM using 192-bit key", "RFC7518, Section 4.7" +A256GcmKw,"A256GCMKW","Key wrapping with AES GCM using 256-bit key", "RFC7518, Section 4.7" +Pbes2HS256A128Kw,"PBES2-HS256+A128KW","PBES2 with HMAC SHA-256 and \"A128KW\" wrapping", "RFC7518, Section 4.8" +Pbes2HS384A192Kw,"PBES2-HS384+A192KW","PBES2 with HMAC SHA-384 and \"A192KW\" wrapping", "RFC7518, Section 4.8" +Pbes2HS512A256Kw,"PBES2-HS512+A256KW","PBES2 with HMAC SHA-512 and \"A256KW\" wrapping", "RFC7518, Section 4.8" +Direct,"dir","Direct use of a shared symmetric key", "RFC7518, Section 4.5" + } +} + +impl FromStr for Algorithm { + type Err = Error; + fn from_str(s: &str) -> Result { + serde_plain::from_str::(s).or(Err(ErrorKind::InvalidAlgorithmName.into())) + } +} + +make_values_enum! { +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Hash)] +/// The eliptic curve used in this JWK, if the key type is "EC" + pub enum ElipticCurve { +P256,"P-256","P-256 Curve","RFC7518, Section 6.2.1.1" +P384,"P-384","P-384 Curve","RFC7518, Section 6.2.1.1" +P521,"P-521","P-521 Curve","RFC7518, Section 6.2.1.1" +Ed25519,"Ed25519","Ed25519 signature algorithm key pairs","RFC8037, Section 3.1" +Ed448,"Ed448","Ed448 signature algorithm key pairs","RFC8037, Section 3.1" +X25519,"X25519","X25519 function key pairs","RFC8037, Section 3.1" +X448,"X448","X448 function key pairs","RFC8037, Section 3.1" +Secp256k1,"secp256k1","SECG secp256k1 curve","RFC8812, Section 3.1" + } +} + +make_values_enum! { +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Hash)] +/// The type of key in this JWK. + pub enum KeyOps { +Sign,"sign","Compute digital signature or MAC","RFC7517, Section 4.3" +Verify,"verify","Verify digital signature or MAC","RFC7517, Section 4.3" +Encrypt,"encrypt","Encrypt content","RFC7517, Section 4.3" +Decrypt,"decrypt","Decrypt content and validate decryption, if applicable","RFC7517, Section 4.3" +WrapKey,"wrapKey","Encrypt key","RFC7517, Section 4.3" +UnwrapKey,"unwrapKey","Decrypt key and validate decryption, if applicable","RFC7517, Section 4.3" +DeriveKey,"deriveKey","Derive key","RFC7517, Section 4.3" +DeriveBits,"deriveBits","Derive bits not to be used as a key","RFC7517, Section 4.3" + } +} + +macro_rules! make_struct { + ( $(#[$meta:meta])* + $vis:vis struct $name:ident {$( + $(#[$item_meta:meta])* $item_name:ident, + $value:literal, + $docstring_head:literal, + $type:ident$(<$lt:tt$(<$lt2:tt>)?>)?, + $($docstring_body:literal)?, + $($spec:literal)? + )*} + ) => { + + $(#[$meta])* + #[non_exhaustive] + $vis struct $name { + $( + #[serde(rename = $value)] + #[doc = $docstring_head] + $(#[doc = "\n"] #[doc = $docstring_body])? + $(#[doc ="\n"] #[doc ="Spec: "] #[doc = $spec])? + $(#[$item_meta])* + pub $item_name: $type$(<$lt$(<$lt2>)?>)? + ),* + } + } +} + +make_struct! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)] + pub struct JwkSet { +keys,"keys","# Array of JWK Values",Vec,,"RFC7517, Section 5.1" + } +} + +make_struct! { + /// see [`Jwk`] `oth` property + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)] + pub struct OtherPrimeInfo { +r,"r","# Prime Factor",Option,,"RFC7518, Section 6.3.2.7.1" +d,"d","# Factor CRT Exponent",Option,,"RFC7518, Section 6.3.2.7.2" +t,"t","# Factor CRT Coefficient",Option,,"RFC7518, Section 6.3.2.7.3" + } +} + +macro_rules! make_keys_enum { + ( $(#[$meta:meta])* + $vis:vis enum $name:ident { + $($(#[$variant_meta:meta])* $variant_name:ident, $variant_value:literal, $docstring:literal, $($variant_spec:literal)? {$( + $(#[$item_meta:meta])* $item_name:ident, + $item_value:literal, + $docstring_head:literal, + $type:ident$(<$lt:tt$(<$lt2:tt$(<$lt3:tt>)?>)?>)?, + $($docstring_body:literal)?, + $($item_spec:literal)? + )* + })* + } + ) => { + + $(#[$meta])* + #[non_exhaustive] + $vis enum $name { + $( + // $(#[depreciated = $depreciated])? + #[serde(rename = $variant_value)] + #[doc = $docstring] + $(#[doc ="\n"] #[doc ="Spec: "] #[doc = $variant_spec])? + $(#[$variant_meta])* + #[non_exhaustive] + $variant_name + + { + $( + #[serde(rename = $item_value)] + #[doc = $docstring_head] + #[doc = "\n"] + $(#[doc = $docstring_body])? + $(#[doc ="\n"] #[doc ="Spec: "] #[doc = $item_spec])? + $(#[$item_meta])* + $item_name: $type$(<$lt$(<$lt2$(<$lt3>)?>)?>)? + ),* + } + ),* + } + } +} + +make_keys_enum! { +/// The type of key in this JWK. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Hash)] +#[serde(tag = "kty")] + pub enum JsonWebKeyType { +Rsa,"RSA","RSA","RFC7518, Section 6.3" { + n,"n","Modulus",Option,,"RFC7518, Section 6.3.1.1" + e,"e","Exponent",Option,,"RFC7518, Section 6.3.1.2" + d,"d","Private Exponent",Option,,"RFC7518, Section 6.3.2.1" + p,"p","First Prime Factor",Option,,"RFC7518, Section 6.3.2.2" + q,"q","Second Prime Factor",Option,,"RFC7518, Section 6.3.2.3" + dp,"dp","First Factor CRT Exponent",Option,,"RFC7518, Section 6.3.2.4" + dq,"dq","Second Factor CRT Exponent",Option,,"RFC7518, Section 6.3.2.5" + qi,"qi","First CRT Coefficient",Option,,"RFC7518, Section 6.3.2.6" + oth,"oth","Other Primes Info",Option >,"Contains any third and subsequent primes.","RFC7518, Section 6.3.2.7" +} +OctetSeq,"oct","Octet sequence","RFC7518, Section 6.4" { + k,"k","Key Value",Option,,"RFC7518, Section 6.4.1" +} +Ec,"EC","Elliptic Curve","RFC7518, Section 6.2" { + crv,"crv","Curve",Option,,"RFC7518, Section 6.2.1.1" + x,"x","X Coordinate",Option,,"RFC7518, Section 6.2.1.2" + y,"y","Y Coordinate",Option,,"RFC7518, Section 6.2.1.3" + d,"d","ECC Private Key",Option,,"RFC7518, Section 6.2.2.1" + +} +OctetStringPairs,"OKP","Octet string key pairs","RFC8037, Section 2" { + crv,"crv","The subtype of key pair",Option,,"RFC8037, Section 2" + d,"d","The private key",Option,,"RFC8037, Section 2" + x,"x","The public key",Option,,"RFC8037, Section 2" +} + } +} + +make_struct! { + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Hash)] + pub struct Jwk { +#[serde(flatten)] kty,"kty","Key Type",JsonWebKeyType,,"RFC7517, Section 4.1" +key_use,"use","Public Key Use",Option,,"RFC7517, Section 4.2" +key_ops,"key_ops","Key Operations",Option,,"RFC7517, Section 4.3" +alg,"alg","Algorithm",Option,,"RFC7517, Section 4.4" +kid,"kid","Key ID",Option,,"RFC7517, Section 4.5" +x5u,"x5u","X.509 URL",Option,,"RFC7517, Section 4.6" +x5c,"x5c","X.509 Certificate Chain",Option >,,"RFC7517, Section 4.7" +x5t,"x5t","X.509 Certificate SHA-1 Thumbprint",Option,,"RFC7517, Section 4.8" +x5t_s256,"x5t#S256","X.509 Certificate SHA-256 Thumbprint",Option,,"RFC7517, Section 4.9" +ext,"ext","Extractable",Option,,"" + } +} + +macro_rules! make_header { + ( $(#[$meta:meta])* + $vis:vis struct $name:ident {$( + $(#[$item_meta:meta])* $item_name:ident, + $value:literal, + $docstring_head:literal, + $type:ident$(<$lt:tt$(<$lt2:tt$(<$lt3:tt>)?>)?>)?, + $($docstring_body:literal)?, $($formats:literal)?, + $($jwe_spec:literal)?, $($jws_spec:literal)? + )*} + ) => { + + $(#[$meta])* + $vis struct $name { + $( + #[serde(rename = $value)] // skip_serializing_if = "Option::is_none" + #[doc = $docstring_head] + #[doc = "\n"] + $(#[doc = $docstring_body])? + $(#[doc ="## Formats\n"] #[doc = $formats])? + #[doc ="## Specification\n"] + $(#[doc = $jwe_spec] #[doc ="\n"] )? + $(#[doc = $jws_spec] #[doc ="\n"] )? + $(#[$item_meta])* + pub $item_name: $type$(<$lt$(<$lt2$(<$lt3>)?>)?>)? + ),* + } + } +} + +// see web-signature-encryption-header-paramaters.csv +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, Default)] + pub struct GeneralHeaders { +#[serde(skip_serializing_if = "Option::is_none")] typ,"typ","# Type",Option,"The type of content encoded in the complete object (for example, JWT).","JWE, JWS","RFC7516, Section 4.1.11","RFC7515, Section 4.1.9" +#[serde(skip_serializing_if = "Option::is_none")] +alg,"alg","# Algorithm",Option,"The specific [`Algorithm`] used to encrypt or sign the object.","JWE, JWS","RFC7516, Section 4.1.1","RFC7515, Section 4.1.1" +#[serde(skip_serializing_if = "Option::is_none")] +cty,"cty","# Content Type",Option,"The type of the secured content / payload.","JWE, JWS","RFC7516, Section 4.1.12","RFC7515, Section 4.1.10" +#[serde(skip_serializing_if = "Option::is_none")] +b64,"b64","# Base64url-Encode Payload",Option,"Whether the payload is base64 encoded. If not present, defaults to true.","JWS",,"RFC7797, Section 3" +#[serde(skip_serializing_if = "Option::is_none")] +url,"url","# URL",Option,"The URL to which the object is directed.","JWE, JWS","RFC8555, Section 6.4.1","RFC8555, Section 6.4.1" +#[serde(skip_serializing_if = "Option::is_none")] +nonce,"nonce","# Nonce",Option,"A unique octet string that enables the verifier of a JWS to recognize when replay has occurred.","JWE, JWS","RFC8555, Section 6.5.2","RFC8555, Section 6.5.2" + } +} + +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, Default)] + pub struct MiscProtectedHeaders { +#[serde(skip_serializing_if = "Option::is_none")] +crit,"crit","# Critical",Option >,"Any extensions to the header that MUST be understood.","JWE, JWS","RFC7516, Section 4.1.13","RFC7515, Section 4.1.11" +#[serde(skip_serializing_if = "Option::is_none")] +ppt,"ppt","# PASSporT extension identifier",Option >,"Required extensions to parse the object.","JWS",,"RFC8225, Section 8.1" // PASSporT MUST use the JWS Protected Header? Section 6. + // "zip" would also go here + } +} +// svt,"svt","# Signature Validation Token",Vec,"An array of JWTs in string format.\n","JWS",,"RFC9321" + +// JWK Set headers + +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, Default)] + pub struct JwkSetHeaders { +#[serde(skip_serializing_if = "Option::is_none")] +jku,"jku","# JWK Set URL",Option,"A URI that refers to a JWK Set containing the public key used to sign the object.","JWE, JWS","RFC7516, Section 4.1.4","RFC7515, Section 4.1.2" +#[serde(skip_serializing_if = "Option::is_none")] +jwk,"jwk","# JSON Web Key",Option >,"The public key used to sign the object, represented as a JWK.","JWE, JWS","RFC7516, Section 4.1.5","RFC7515, Section 4.1.3" +#[serde(skip_serializing_if = "Option::is_none")] +kid,"kid","# Key ID",Option,"A hint indicating which key was used to secure the JWS.","JWE, JWS","RFC7516, Section 4.1.6","RFC7515, Section 4.1.4" + } +} + +// X.509 certificate Agreement +// see + +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, Default)] + pub struct X509Headers { +#[serde(skip_serializing_if = "Option::is_none")] +x5u,"x5u","# X.509 URL",Option,,"JWE, JWS","RFC7516, Section 4.1.7","RFC7515, Section 4.1.5" +#[serde(skip_serializing_if = "Option::is_none")] +x5c,"x5c","# X.509 Certificate Chain",Option >,,"JWE, JWS","RFC7516, Section 4.1.8","RFC7515, Section 4.1.6" +#[serde(skip_serializing_if = "Option::is_none")] +x5t,"x5t","# X.509 Certificate SHA-1 Thumbprint",Option,,"JWE, JWS","RFC7516, Section 4.1.9","RFC7515, Section 4.1.7" +#[serde(skip_serializing_if = "Option::is_none")] +x5t_s256,"# x5t#S256","X.509 Certificate SHA-256 Thumbprint",Option,,"JWE, JWS","RFC7516, Section 4.1.10","RFC7515, Section 4.1.8" + } +} + +// Claims that might be replicated to the header in a JWE +// see +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, Default)] + pub struct ClaimHeaders { +#[serde(skip_serializing_if = "Option::is_none")] +iss,"iss","# Issuer",Option,"The principal that issued the object.","JWE","RFC7519, Section 4.1.1", +#[serde(skip_serializing_if = "Option::is_none")] +sub,"sub","# Subject",Option,"The principal that is the subject of the object.","JWE","RFC7519, Section 4.1.2", +#[serde(skip_serializing_if = "Option::is_none")] +aud,"aud","# Audience",Option >,"The recipients that the object is intended for.","JWE","RFC7519, Section 4.1.3", + } +} +// ECDH Key Agreement +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)] + pub struct ECDHKeyAgreementHeaders { +epk,"epk","# Ephemeral Public Key",Box,,"JWE","RFC7518, Section 4.6.1.1", +#[serde(skip_serializing_if = "Option::is_none")] +apu,"apu","# Agreement PartyUInfo",Option,,"JWE","RFC7518, Section 4.6.1.2", +#[serde(skip_serializing_if = "Option::is_none")] +apv,"apv","# Agreement PartyVInfo",Option,,"JWE","RFC7518, Section 4.6.1.3", + } +} +// AES GCM Key Encryption +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)] + pub struct AesGcmHeaders { +iv,"iv","# Initialization Vector",String,,"JWE","RFC7518, Section 4.7.1.1", +tag,"tag","# Authentication Tag",String,,"JWE","RFC7518, Section 4.7.1.2", + } +} +// PBES2 Key Encryption +make_header! { + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)] + pub struct Pbes2Headers { +p2s,"p2s","# PBES2 Salt Input",String,,"JWE","RFC7518, Section 4.8.1.1", +p2c,"p2c","# PBES2 Count",u64,,"JWE","RFC7518, Section 4.8.1.2", + } +} + +#[cfg(test)] +mod tests { + use jsonwebtoken_rustcrypto::Algorithm; + + use super::*; + + #[test] + fn generate_algorithm_enum_from_str() { + assert!(Algorithm::from_str("HS256").is_ok()); + assert!(Algorithm::from_str("HS384").is_ok()); + assert!(Algorithm::from_str("HS512").is_ok()); + assert!(Algorithm::from_str("RS256").is_ok()); + assert!(Algorithm::from_str("RS384").is_ok()); + assert!(Algorithm::from_str("RS512").is_ok()); + assert!(Algorithm::from_str("PS256").is_ok()); + assert!(Algorithm::from_str("PS384").is_ok()); + assert!(Algorithm::from_str("PS512").is_ok()); + assert!(Algorithm::from_str("").is_err()); + } +} diff --git a/src/test_utils.rs b/src/test_utils.rs index 52ab68a..2c801ed 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -2,8 +2,10 @@ pub mod common_test_utils { use base64::Engine; use rand::rngs::OsRng; + use rsa::pkcs1::EncodeRsaPublicKey; + use rsa::pkcs8::EncodePrivateKey; use rsa::PublicKeyParts; - use rsa::{pkcs1::ToRsaPublicKey, pkcs8::ToPrivateKey, RsaPrivateKey, RsaPublicKey}; + use rsa::{RsaPrivateKey, RsaPublicKey}; use serde_json::value::{Map, Value}; use std::collections::HashSet; @@ -21,8 +23,11 @@ pub mod common_test_utils { public_key: RsaPublicKey, ) -> (String, String) { ( - private_key.to_pkcs8_pem().unwrap().to_string(), - public_key.to_pkcs1_pem().unwrap(), + private_key + .to_pkcs8_pem(rsa::pkcs8::LineEnding::CR) + .unwrap() + .to_string(), + public_key.to_pkcs1_pem(rsa::pkcs1::LineEnding::CR).unwrap(), ) } diff --git a/src/validation.rs b/src/validation.rs index aa0f6bd..edf2c0b 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -6,18 +6,24 @@ use std::collections::HashSet; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Validation { + #[cfg(not(target_arch = "wasm32"))] pub required_spec_claims: HashSet, pub leeway: u64, pub validate_exp: bool, pub validate_nbf: bool, + #[cfg(not(target_arch = "wasm32"))] pub validate_aud: bool, pub aud: Option>, + #[cfg(not(target_arch = "wasm32"))] pub iss: Option>, + #[cfg(target_arch = "wasm32")] + pub iss: Option, pub sub: Option, pub algorithms: Algorithm, } impl Validation { + #[cfg(not(target_arch = "wasm32"))] pub fn new(alg: Algorithm) -> Validation { let mut required_claims = HashSet::with_capacity(1); required_claims.insert("exp".to_owned()); @@ -37,11 +43,36 @@ impl Validation { } } + #[cfg(not(target_arch = "wasm32"))] pub fn no_exp(mut self) -> Self { self.validate_exp = false; self.required_spec_claims.remove("exp"); self } + + #[cfg(target_arch = "wasm32")] + pub fn new(alg: Algorithm) -> Validation { + let mut required_claims = HashSet::with_capacity(1); + required_claims.insert("exp".to_owned()); + + Validation { + algorithms: alg, + leeway: 60, + + validate_exp: true, + validate_nbf: false, + + iss: None, + sub: None, + aud: None, + } + } + + #[cfg(target_arch = "wasm32")] + pub fn no_exp(mut self) -> Self { + self.validate_exp = false; + self + } } impl Default for Validation {