diff --git a/README.md b/README.md index f5c5fffc..4e691999 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,14 @@ my_claims = decode_jws(&encoded, &DecodingKey::from_secret("secret".as_ref()), & 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/jwk.rs b/src/jwk.rs index 49c58003..9a656bb9 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -6,6 +6,7 @@ use crate::{ errors::{self, Error, ErrorKind}, + serialization::b64_encode, Algorithm, }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -402,6 +403,13 @@ pub enum AlgorithmParameters { OctetKeyPair(OctetKeyPairParameters), } +/// The function to use to hash the intermediate thumbprint data. +pub enum ThumbprintHash { + SHA256, + SHA384, + SHA512, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] pub struct Jwk { #[serde(flatten)] @@ -416,6 +424,60 @@ impl Jwk { pub fn is_supported(&self) -> bool { self.common.key_algorithm.unwrap().to_algorithm().is_ok() } + + /// Compute the thumbprint of the JWK. + /// + /// Per (RFC-7638)[https://datatracker.ietf.org/doc/html/rfc7638] + pub fn thumbprint(&self, hash_function: 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 +497,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 +535,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"); + } }