Skip to content

Commit

Permalink
Add support for JWK thumbprints (RFC 7638) (#156)
Browse files Browse the repository at this point in the history
* Add support for JWK thumbprints (RFC 7638)

* Expand thumbprint tests

* Add a test for badly-normalized values

This is for the RSA exponent case; other members / key types have not
been checked.

* JWK thumbprints: Hide ring from public API, expand documentation
  • Loading branch information
g2p authored Jul 17, 2020
1 parent d3868c5 commit eefe29b
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 1 deletion.
2 changes: 1 addition & 1 deletion doc/supported.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
25 changes: 25 additions & 0 deletions src/digest.rs
Original file line number Diff line number Diff line change
@@ -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);
159 changes: 159 additions & 0 deletions src/jwk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<biscuit::Empty> = 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<String, serde_json::error::Error> {
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", &params.curve)?;
map.serialize_entry("kty", &params.key_type)?;
map.serialize_entry("x", &byte_sequence::wrap(&params.x))?;
map.serialize_entry("y", &byte_sequence::wrap(&params.y))?;
}
AlgorithmParameters::RSA(params) => {
map.serialize_entry("e", &base64_url_uint::wrap(&params.e))?;
map.serialize_entry("kty", &params.key_type)?;
map.serialize_entry("n", &base64_url_uint::wrap(&params.n))?;
}
AlgorithmParameters::OctetKey(params) => {
map.serialize_entry("k", &byte_sequence::wrap(&params.value))?;
map.serialize_entry("kty", &params.key_type)?;
}
AlgorithmParameters::OctetKeyPair(params) => {
map.serialize_entry("crv", &params.curve)?;
map.serialize_entry("kty", &params.key_type)?;
map.serialize_entry("x", &byte_sequence::wrap(&params.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
Expand Down Expand Up @@ -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<Empty> = 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<Empty> = 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<Empty> = 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<Empty> = 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<Empty> = serde_json::from_str(
r#"{
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}"#,
)
.unwrap();
assert_eq!(
jwk.algorithm.thumbprint(&crate::digest::SHA256).unwrap(),
"kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k"
);
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions src/serde_custom/base64_url_uint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize(self.0, serializer)
}
}

#[cfg(test)]
mod tests {
use num::cast::FromPrimitive;
Expand Down
15 changes: 15 additions & 0 deletions src/serde_custom/byte_sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize(self.0, serializer)
}
}

#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
Expand Down

0 comments on commit eefe29b

Please sign in to comment.