diff --git a/README.md b/README.md index ccd4e759..4e691999 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,28 @@ let token = decode::(&token, &DecodingKey::from_rsa_components(jwk["n"], If your key is in PEM format, it is better performance wise to generate the `DecodingKey` once in a `lazy_static` or something similar and reuse it. +### Encoding and decoding JWS + +JWS is handled the same way as JWT, but using `encode_jws` and `decode_jws`: + +```rust +let encoded = encode_jws(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; +my_claims = decode_jws(&encoded, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?.claims; +``` + +`encode_jws` returns a `Jws` struct which can be placed in other structs or serialized/deserialized from JSON directly. + +The generic parameter in `Jws` indicates the claims type and prevents accidentally encoding or decoding the wrong claims type +when the Jws is nested in another struct. + +### JWK Thumbprints + +If you have a JWK object, you can generate a thumbprint like + +``` +let tp = my_jwk.thumbprint(&jsonwebtoken::DIGEST_SHA256); +``` + ### Convert SEC1 private key to PKCS8 `jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top, this is a SEC1 type and can be converted to PKCS8 like so: diff --git a/src/decoding.rs b/src/decoding.rs index 8d87f03d..6f87fe61 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -6,6 +6,7 @@ use crate::crypto::verify; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; use crate::jwk::{AlgorithmParameters, Jwk}; +use crate::jws::Jws; #[cfg(feature = "use_pem")] use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, DecodedJwtPartClaims}; @@ -201,14 +202,13 @@ impl DecodingKey { } } -/// Verify signature of a JWT, and return header object and raw payload -/// -/// If the token or its signature is invalid, it will return an error. -fn verify_signature<'a>( - token: &'a str, +fn verify_signature_body( + header: &Header, + message: &str, + signature: &str, key: &DecodingKey, validation: &Validation, -) -> Result<(Header, &'a str)> { +) -> Result<()> { if validation.validate_signature && validation.algorithms.is_empty() { return Err(new_error(ErrorKind::MissingAlgorithm)); } @@ -221,10 +221,6 @@ fn verify_signature<'a>( } } - let (signature, message) = expect_two!(token.rsplitn(2, '.')); - let (payload, header) = expect_two!(message.rsplitn(2, '.')); - let header = Header::from_encoded(header)?; - if validation.validate_signature && !validation.algorithms.contains(&header.alg) { return Err(new_error(ErrorKind::InvalidAlgorithm)); } @@ -233,6 +229,23 @@ fn verify_signature<'a>( return Err(new_error(ErrorKind::InvalidSignature)); } + Ok(()) +} + +/// Verify signature of a JWT, and return header object and raw payload +/// +/// If the token or its signature is invalid, it will return an error. +fn verify_signature<'a>( + token: &'a str, + key: &DecodingKey, + validation: &Validation, +) -> Result<(Header, &'a str)> { + let (signature, message) = expect_two!(token.rsplitn(2, '.')); + let (payload, header) = expect_two!(message.rsplitn(2, '.')); + let header = Header::from_encoded(header)?; + + verify_signature_body(&header, message, signature, key, validation)?; + Ok((header, payload)) } @@ -286,3 +299,37 @@ pub fn decode_header(token: &str) -> Result
{ let (_, header) = expect_two!(message.rsplitn(2, '.')); Header::from_encoded(header) } + +/// Verify signature of a JWS, and return the header object +/// +/// If the token or its signature is invalid, it will return an error. +fn verify_jws_signature( + jws: &Jws, + key: &DecodingKey, + validation: &Validation, +) -> Result
{ + let header = Header::from_encoded(&jws.protected)?; + let message = [jws.protected.as_str(), jws.payload.as_str()].join("."); + + verify_signature_body(&header, &message, &jws.signature, key, validation)?; + + Ok(header) +} + +/// Validate a received JWS and decode into the header and claims. +pub fn decode_jws( + jws: &Jws, + key: &DecodingKey, + validation: &Validation, +) -> Result> { + match verify_jws_signature(jws, key, validation) { + Err(e) => Err(e), + Ok(header) => { + let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(&jws.payload)?; + let claims = decoded_claims.deserialize()?; + validate(decoded_claims.deserialize()?, validation)?; + + Ok(TokenData { header, claims }) + } + } +} diff --git a/src/encoding.rs b/src/encoding.rs index 26f5c4c3..5cf59b5b 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,10 +1,14 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::{ + engine::general_purpose::{STANDARD, URL_SAFE}, + Engine, +}; use serde::ser::Serialize; use crate::algorithms::AlgorithmFamily; use crate::crypto; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; +use crate::jws::Jws; #[cfg(feature = "use_pem")] use crate::pem::decoder::PemEncodedKey; use crate::serialization::b64_encode_part; @@ -14,7 +18,7 @@ use crate::serialization::b64_encode_part; #[derive(Clone)] pub struct EncodingKey { pub(crate) family: AlgorithmFamily, - content: Vec, + pub(crate) content: Vec, } impl EncodingKey { @@ -29,6 +33,12 @@ impl EncodingKey { Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out }) } + /// For loading websafe base64 HMAC secrets, ex: ACME EAB credentials. + pub fn from_urlsafe_base64_secret(secret: &str) -> Result { + let out = URL_SAFE.decode(secret)?; + Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out }) + } + /// If you are loading a RSA key from a .pem file. /// This errors if the key is not a valid RSA key. /// Only exists if the feature `use_pem` is enabled. @@ -129,3 +139,30 @@ pub fn encode(header: &Header, claims: &T, key: &EncodingKey) -> R Ok([message, signature].join(".")) } + +/// Encode the header and claims given and sign the payload using the algorithm from the header and the key. +/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. This produces a JWS instead of +/// a JWT -- usage is similar to `encode`, see that for more details. +pub fn encode_jws( + header: &Header, + claims: Option<&T>, + key: &EncodingKey, +) -> Result> { + if key.family != header.alg.family() { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + let encoded_header = b64_encode_part(header)?; + let encoded_claims = match claims { + Some(claims) => b64_encode_part(claims)?, + None => "".to_string(), + }; + let message = [encoded_header.as_str(), encoded_claims.as_str()].join("."); + let signature = crypto::sign(message.as_bytes(), key, header.alg)?; + + Ok(Jws { + protected: encoded_header, + payload: encoded_claims, + signature, + _pd: Default::default(), + }) +} diff --git a/src/header.rs b/src/header.rs index 220f0fa4..16e7bde3 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,13 +1,109 @@ 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, Hash)] @@ -64,6 +160,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, } impl Header { @@ -80,6 +197,11 @@ impl Header { x5c: None, x5t: None, x5t_s256: None, + crit: None, + enc: None, + zip: None, + url: None, + nonce: None, } } diff --git a/src/jwk.rs b/src/jwk.rs index 49c58003..2e53905d 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -5,8 +5,14 @@ //! tweaked to remove the private bits as it's not the goal for this crate currently. use crate::{ + crypto::ecdsa::alg_to_ec_signing, errors::{self, Error, ErrorKind}, - Algorithm, + serialization::b64_encode, + Algorithm, EncodingKey, +}; +use ring::{ + rand, + signature::{self, KeyPair}, }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::{fmt, str::FromStr}; @@ -402,6 +408,13 @@ pub enum AlgorithmParameters { OctetKeyPair(OctetKeyPairParameters), } +/// The function to use to hash the intermediate thumbprint data. +pub enum ThumbprintHash { + SHA256, + SHA384, + SHA512, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] pub struct Jwk { #[serde(flatten)] @@ -416,6 +429,139 @@ impl Jwk { pub fn is_supported(&self) -> bool { self.common.key_algorithm.unwrap().to_algorithm().is_ok() } + + 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 key_pair = signature::RsaKeyPair::from_der(&key.content) + .map_err(|e| ErrorKind::InvalidRsaKey(e.to_string()))?; + let public = key_pair.public(); + let components = + ring::signature::RsaPublicKeyComponents::>::from(public); + AlgorithmParameters::RSA(RSAKeyParameters { + key_type: RSAKeyType::RSA, + n: b64_encode(components.n), + e: b64_encode(components.e), + }) + } + crate::algorithms::AlgorithmFamily::Ec => { + let rng = rand::SystemRandom::new(); + let key_pair = signature::EcdsaKeyPair::from_pkcs8( + alg_to_ec_signing(alg), + &key.content, + &rng, + )?; + // Ring has this as `ring::ec::suite_b::curve::P384.elem_scalar_seed_len` but + // it's private and not exposed via any methods AFAICT. + let pub_elem_bytes; + let curve; + match alg { + Algorithm::ES256 => { + pub_elem_bytes = 32; + curve = EllipticCurve::P256; + } + Algorithm::ES384 => { + pub_elem_bytes = 48; + curve = EllipticCurve::P384; + } + _ => unreachable!(), + }; + let pub_bytes = key_pair.public_key().as_ref(); + if pub_bytes[0] != 4 { + panic!("Compressed coordinates in public key!"); + } + let (x, y) = pub_bytes[1..].split_at(pub_elem_bytes); + AlgorithmParameters::EllipticCurve(EllipticCurveKeyParameters { + key_type: EllipticCurveKeyType::EC, + curve, + 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 hash_function = match hash_function { + ThumbprintHash::SHA256 => &ring::digest::SHA256, + ThumbprintHash::SHA384 => &ring::digest::SHA384, + ThumbprintHash::SHA512 => &ring::digest::SHA512, + }; + let pre = match &self.algorithm { + AlgorithmParameters::EllipticCurve(a) => match a.curve { + EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 => { + format!( + r#"{{"crv":{},"kty":{},"x":"{}","y":"{}"}}"#, + serde_json::to_string(&a.curve).unwrap(), + serde_json::to_string(&a.key_type).unwrap(), + a.x, + a.y, + ) + } + EllipticCurve::Ed25519 => panic!("EllipticCurve can't contain this curve type"), + }, + AlgorithmParameters::RSA(a) => { + format!( + r#"{{"e":"{}","kty":{},"n":"{}"}}"#, + a.e, + serde_json::to_string(&a.key_type).unwrap(), + a.n, + ) + } + AlgorithmParameters::OctetKey(a) => { + format!( + r#"{{"k":"{}","kty":{}}}"#, + a.value, + serde_json::to_string(&a.key_type).unwrap() + ) + } + AlgorithmParameters::OctetKeyPair(a) => match a.curve { + EllipticCurve::P256 | EllipticCurve::P384 | EllipticCurve::P521 => { + panic!("OctetKeyPair can't contain this curve type") + } + EllipticCurve::Ed25519 => { + format!( + r#"{{crv:{},"kty":{},"x":"{}"}}"#, + serde_json::to_string(&a.curve).unwrap(), + serde_json::to_string(&a.key_type).unwrap(), + a.x, + ) + } + }, + }; + return b64_encode(ring::digest::digest(hash_function, pre.as_bytes())); + } } /// A JWK set @@ -435,7 +581,9 @@ impl JwkSet { #[cfg(test)] mod tests { - use crate::jwk::{AlgorithmParameters, JwkSet, OctetKeyType}; + use crate::jwk::{ + AlgorithmParameters, Jwk, JwkSet, OctetKeyType, RSAKeyParameters, ThumbprintHash, + }; use crate::serialization::b64_encode; use crate::Algorithm; use serde_json::json; @@ -471,4 +619,19 @@ mod tests { _ => panic!("Unexpected key algorithm"), } } + + #[test] + #[wasm_bindgen_test] + fn check_thumbprint() { + let tp = Jwk { + common: crate::jwk::CommonParameters { key_id: Some("2011-04-29".to_string()), ..Default::default() }, + algorithm: AlgorithmParameters::RSA(RSAKeyParameters { + key_type: crate::jwk::RSAKeyType::RSA, + n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw".to_string(), + e: "AQAB".to_string(), + }), + } + .thumbprint(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..0d1328f2 --- /dev/null +++ b/src/jws.rs @@ -0,0 +1,24 @@ +//! JSON Web Signatures data type. +use std::marker::PhantomData; + +use serde::{Deserialize, Serialize}; + +/// This is a serde-compatible JSON Web Signature structure. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Jws { + /// The base64 encoded header data. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub protected: String, + /// The base64 encoded claims data. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub payload: String, + /// The signature on the other fields. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub signature: String, + /// Unused, for associating type metadata. + #[serde(skip)] + pub _pd: PhantomData, +} diff --git a/src/lib.rs b/src/lib.rs index 0c8664bf..c7195e66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,13 +12,14 @@ mod encoding; pub mod errors; mod header; pub mod jwk; +pub mod jws; #[cfg(feature = "use_pem")] mod pem; mod serialization; mod validation; pub use algorithms::Algorithm; -pub use decoding::{decode, decode_header, DecodingKey, TokenData}; -pub use encoding::{encode, EncodingKey}; +pub use decoding::{decode, decode_header, decode_jws, DecodingKey, TokenData}; +pub use encoding::{encode, encode_jws, EncodingKey}; pub use header::Header; pub use validation::{get_current_timestamp, Validation}; diff --git a/tests/ecdsa/mod.rs b/tests/ecdsa/mod.rs index 8c06910f..66ff22c1 100644 --- a/tests/ecdsa/mod.rs +++ b/tests/ecdsa/mod.rs @@ -103,6 +103,29 @@ fn ec_x_y() { assert!(res.is_ok()); } +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn ec_jwk_from_key() { + use jsonwebtoken::jwk::Jwk; + use serde_json::json; + + let privkey = include_str!("private_ecdsa_key.pem"); + let encoding_key = EncodingKey::from_ec_pem(privkey.as_ref()).unwrap(); + let jwk = Jwk::from_encoding_key(&encoding_key, Algorithm::ES256).unwrap(); + assert_eq!( + jwk, + serde_json::from_value(json!({ + "kty": "EC", + "crv": "P-256", + "x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ", + "y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4", + "alg": "ES256", + })) + .unwrap() + ); +} + #[cfg(feature = "use_pem")] #[test] #[wasm_bindgen_test] diff --git a/tests/rsa/mod.rs b/tests/rsa/mod.rs index 3297149f..f31969c3 100644 --- a/tests/rsa/mod.rs +++ b/tests/rsa/mod.rs @@ -169,6 +169,24 @@ fn rsa_modulus_exponent() { assert!(res.is_ok()); } +#[cfg(feature = "use_pem")] +#[test] +#[wasm_bindgen_test] +fn rsa_jwk_from_key() { + use jsonwebtoken::jwk::Jwk; + use serde_json::json; + + let privkey = include_str!("private_rsa_key_pkcs8.pem"); + let encoding_key = EncodingKey::from_rsa_pem(privkey.as_ref()).unwrap(); + let jwk = Jwk::from_encoding_key(&encoding_key, Algorithm::RS256).unwrap(); + assert_eq!(jwk, serde_json::from_value(json!({ + "kty": "RSA", + "n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ", + "e": "AQAB", + "alg": "RS256", + })).unwrap()); +} + #[cfg(feature = "use_pem")] #[test] #[wasm_bindgen_test]