diff --git a/doc/supported.md b/doc/supported.md index deb72d28..c68b70cf 100644 --- a/doc/supported.md +++ b/doc/supported.md @@ -38,7 +38,7 @@ JWK is defined in [RFC 7517](https://tools.ietf.org/html/rfc7517). Both `JWK` and `JWKSet`are supported (_as of v0.0.2_). -[JWK Thumbprint](https://tools.ietf.org/html/rfc7638) is not supported. +[JWK Thumbprint](https://tools.ietf.org/html/rfc7638) is supported (_as of v0.5.0_). JWK Common Parameters are defined in [RFC 7517 Section 4](https://tools.ietf.org/html/rfc7517#section-4). diff --git a/src/digest.rs b/src/digest.rs new file mode 100644 index 00000000..430fe7b2 --- /dev/null +++ b/src/digest.rs @@ -0,0 +1,25 @@ +//! Secure cryptographic digests +//! +//! Currently used by JWK thumbprints. +//! This simply wraps the ring::digest module, while providing forward compatibility +//! should the implementation change. + +/// A digest algorithm +pub struct Algorithm(pub(crate) &'static ring::digest::Algorithm); + +// SHA-1 as specified in FIPS 180-4. Deprecated. +// SHA-1 is not exposed at the moment, as the only user is JWK thumbprints, +// which postdate SHA-1 deprecation and don't have a backwards-compatibility reason to use it. +//pub static SHA1_FOR_LEGACY_USE_ONLY: Algorithm = Algorithm(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY); + +/// SHA-256 as specified in FIPS 180-4. +pub static SHA256: Algorithm = Algorithm(&ring::digest::SHA256); + +/// SHA-384 as specified in FIPS 180-4. +pub static SHA384: Algorithm = Algorithm(&ring::digest::SHA384); + +/// SHA-512 as specified in FIPS 180-4. +pub static SHA512: Algorithm = Algorithm(&ring::digest::SHA512); + +/// SHA-512/256 as specified in FIPS 180-4. +pub static SHA512_256: Algorithm = Algorithm(&ring::digest::SHA512_256); diff --git a/src/jwk.rs b/src/jwk.rs index 8f9be1d7..5aa8dc17 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -4,6 +4,7 @@ use std::fmt; +use data_encoding::BASE64URL_NOPAD; use num::BigUint; use serde::de::{self, DeserializeOwned}; use serde::{self, Deserialize, Deserializer, Serialize, Serializer}; @@ -271,6 +272,72 @@ impl AlgorithmParameters { _ => Err(unexpected_key_type_error!(KeyType::Octet, self.key_type())), } } + + /// JWK thumbprints are digests for identifying key material. + /// Their computation is specified in + /// [RFC 7638](https://tools.ietf.org/html/rfc7638). + /// + /// This can be used to identify a public key; when the underlying digest algorithm + /// is collision-resistant (currently, the SHA-2 family is provided), it is infeasible + /// to build two keys sharing a thumbprint. + /// + /// As mentioned in the RFC's security considerations, it remains possible to build + /// related keys with distinct parameters and thumbprints. + /// + /// ``` + /// // Example from https://tools.ietf.org/html/rfc7638#section-3.1 + /// let jwk: biscuit::jwk::JWK = serde_json::from_str( + /// r#"{ + /// "kty": "RSA", + /// "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + /// "e": "AQAB", + /// "alg": "RS256", + /// "kid": "2011-04-29" + /// }"#, + /// ).unwrap(); + /// assert_eq!( + /// jwk.algorithm.thumbprint(&biscuit::digest::SHA256).unwrap(), + /// "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" + /// ); + /// ``` + pub fn thumbprint( + &self, + algorithm: &'static crate::digest::Algorithm, + ) -> Result { + use serde::ser::SerializeMap; + + use crate::serde_custom::{base64_url_uint, byte_sequence}; + + let mut serializer = serde_json::Serializer::new(Vec::new()); + let mut map = serializer.serialize_map(None)?; + // https://tools.ietf.org/html/rfc7638#section-3.2 + // Write required public key parameters in lexicographic order + match self { + AlgorithmParameters::EllipticCurve(params) => { + map.serialize_entry("crv", ¶ms.curve)?; + map.serialize_entry("kty", ¶ms.key_type)?; + map.serialize_entry("x", &byte_sequence::wrap(¶ms.x))?; + map.serialize_entry("y", &byte_sequence::wrap(¶ms.y))?; + } + AlgorithmParameters::RSA(params) => { + map.serialize_entry("e", &base64_url_uint::wrap(¶ms.e))?; + map.serialize_entry("kty", ¶ms.key_type)?; + map.serialize_entry("n", &base64_url_uint::wrap(¶ms.n))?; + } + AlgorithmParameters::OctetKey(params) => { + map.serialize_entry("k", &byte_sequence::wrap(¶ms.value))?; + map.serialize_entry("kty", ¶ms.key_type)?; + } + AlgorithmParameters::OctetKeyPair(params) => { + map.serialize_entry("crv", ¶ms.curve)?; + map.serialize_entry("kty", ¶ms.key_type)?; + map.serialize_entry("x", &byte_sequence::wrap(¶ms.x))?; + } + } + map.end()?; + let json_u8 = serializer.into_inner(); + Ok(BASE64URL_NOPAD.encode(ring::digest::digest(algorithm.0, &json_u8).as_ref())) + } } /// Parameters for an Elliptic Curve Key @@ -1322,4 +1389,96 @@ mod tests { let keys = find_key_set(); assert_eq!(keys.find("third"), None); } + + #[test] + fn jwk_ec_thumbprint() { + let jwk: JWK = serde_json::from_str( + r#"{ + "alg":"ES256", + "crv":"P-256", + "key_ops":["sign","verify"], + "kty":"EC", + "x":"XAyWu1zgShU0q_C5EtiM4QuFfVqRo51J-4FdeBQVTXE", + "y":"rvz9yHRaFcFn1vBIykwudyK85TEqR0OXOgBYnCeqN-M" + }"#, + ) + .unwrap(); + assert_eq!( + jwk.algorithm.thumbprint(&crate::digest::SHA256).unwrap(), + "5RQpPyszBq9VihghaQY1Ptj4OdOpQH7AIOOnngMEKrA" + ); + } + + #[test] + fn jwk_rsa_thumbprint() { + // Example from https://tools.ietf.org/html/rfc7638#section-3.1 + let jwk: JWK = serde_json::from_str( + r#"{ + "kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e": "AQAB", + "alg": "RS256", + "kid": "2011-04-29" + }"#, + ) + .unwrap(); + assert_eq!( + jwk.algorithm.thumbprint(&crate::digest::SHA256).unwrap(), + "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" + ); + } + + #[test] + fn jwk_rsa_thumbprint_normalization_gotcha() { + // Modified from https://tools.ietf.org/html/rfc7638#section-3.1 + // to exemplify a gotcha from https://tools.ietf.org/html/rfc7638#section-7 + let jwk: JWK = serde_json::from_str( + r#"{ + "kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e": "AAEAAQ", + "alg": "RS256", + "kid": "2011-04-29" + }"#, + ) + .unwrap(); + assert_eq!( + jwk.algorithm.thumbprint(&crate::digest::SHA256).unwrap(), + "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" + ); + } + + #[test] + fn jwk_oct_thumbprint() { + let jwk: JWK = serde_json::from_str( + r#"{ + "kty": "oct", + "kid": "77c7e2b8-6e13-45cf-8672-617b5b45243a", + "use": "enc", + "alg": "A128GCM", + "k": "XctOhJAkA-pD9Lh7ZgW_2A" + }"#, + ) + .unwrap(); + assert_eq!( + jwk.algorithm.thumbprint(&crate::digest::SHA256).unwrap(), + "svOLuZiKpi3RFmSHAcCJqsQqjBmWR4egaIsgk-2uBak" + ); + } + + #[test] + fn jwk_okp_thumbprint() { + let jwk: JWK = serde_json::from_str( + r#"{ + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }"#, + ) + .unwrap(); + assert_eq!( + jwk.algorithm.thumbprint(&crate::digest::SHA256).unwrap(), + "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k" + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 4a4959a5..d4e151b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,6 +132,8 @@ pub mod jwe; pub mod jwk; pub mod jws; +pub mod digest; + use crate::errors::{Error, ValidationError}; /// A convenience type alias of the common "JWT" which is a secured/unsecured compact JWS. diff --git a/src/serde_custom/base64_url_uint.rs b/src/serde_custom/base64_url_uint.rs index 29ab918c..425eba7d 100644 --- a/src/serde_custom/base64_url_uint.rs +++ b/src/serde_custom/base64_url_uint.rs @@ -46,6 +46,21 @@ where deserializer.deserialize_str(BigUintVisitor) } +pub struct Wrapper<'a>(&'a BigUint); + +pub fn wrap(data: &BigUint) -> Wrapper { + Wrapper(data) +} + +impl<'a> serde::Serialize for Wrapper<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize(self.0, serializer) + } +} + #[cfg(test)] mod tests { use num::cast::FromPrimitive; diff --git a/src/serde_custom/byte_sequence.rs b/src/serde_custom/byte_sequence.rs index 438d0e53..3c894147 100644 --- a/src/serde_custom/byte_sequence.rs +++ b/src/serde_custom/byte_sequence.rs @@ -42,6 +42,21 @@ where deserializer.deserialize_str(BytesVisitor) } +pub struct Wrapper<'a>(&'a [u8]); + +pub fn wrap(data: &[u8]) -> Wrapper { + Wrapper(data) +} + +impl<'a> serde::Serialize for Wrapper<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize(self.0, serializer) + } +} + #[cfg(test)] mod tests { use serde::{Deserialize, Serialize};