Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JWK thumbprints (RFC 7638) #156

Merged
merged 4 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
lawliet89 marked this conversation as resolved.
Show resolved Hide resolved
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