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 {