Skip to content

Commit a65c9af

Browse files
committed
Switch to JWK Thumbprints
1 parent 3d911a8 commit a65c9af

File tree

6 files changed

+83
-44
lines changed

6 files changed

+83
-44
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ features = ["std"]
474474
# PKCS#8 encoding
475475
[workspace.dependencies.pkcs8]
476476
version = "0.10.2"
477-
features = ["alloc", "std", "pkcs5", "encryption"]
477+
features = ["std", "pkcs5", "encryption"]
478478

479479
# Public Suffix List
480480
[workspace.dependencies.psl]

crates/config/src/sections/secrets.rs

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,9 @@ use std::borrow::Cow;
99
use anyhow::{Context, bail};
1010
use camino::Utf8PathBuf;
1111
use futures_util::future::{try_join, try_join_all};
12-
use mas_jose::jwk::{JsonWebKey, JsonWebKeySet};
12+
use mas_jose::jwk::{JsonWebKey, JsonWebKeySet, Thumbprint};
1313
use mas_keystore::{Encrypter, Keystore, PrivateKey};
14-
use rand::{
15-
Rng, SeedableRng,
16-
distributions::{Alphanumeric, DistString, Standard},
17-
prelude::Distribution as _,
18-
};
14+
use rand::{Rng, SeedableRng, distributions::Standard, prelude::Distribution as _};
1915
use schemars::JsonSchema;
2016
use serde::{Deserialize, Serialize};
2117
use serde_with::serde_as;
@@ -134,8 +130,7 @@ impl From<Key> for KeyRaw {
134130
pub struct KeyConfig {
135131
/// The key ID `kid` of the key as used by JWKs.
136132
///
137-
/// If not given, `kid` will be derived from the key by hex-encoding the
138-
/// first four bytes of the key’s fingerprint.
133+
/// If not given, `kid` will be the key’s RFC 7638 JWK Thumbprint.
139134
#[serde(skip_serializing_if = "Option::is_none")]
140135
kid: Option<String>,
141136

@@ -185,7 +180,7 @@ impl KeyConfig {
185180

186181
let kid = match self.kid.clone() {
187182
Some(kid) => kid,
188-
None => kid_from_key(&private_key)?,
183+
None => private_key.thumbprint_sha256_base64(),
189184
};
190185

191186
Ok(JsonWebKey::new(private_key)
@@ -194,13 +189,6 @@ impl KeyConfig {
194189
}
195190
}
196191

197-
/// Returns a kid derived from the given key.
198-
fn kid_from_key(private_key: &PrivateKey) -> anyhow::Result<String> {
199-
let fingerprint = private_key.fingerprint()?;
200-
let head = fingerprint.first_chunk::<4>().unwrap();
201-
Ok(hex::encode(head))
202-
}
203-
204192
/// Encryption config option.
205193
#[derive(Debug, Clone)]
206194
pub enum Encryption {
@@ -494,7 +482,7 @@ mod tests {
494482

495483
let key_store = config.key_store().await.unwrap();
496484
assert!(key_store.iter().any(|k| k.kid() == Some("lekid0")));
497-
assert!(key_store.iter().any(|k| k.kid() == Some("040b0ab8")));
485+
assert!(key_store.iter().any(|k| k.kid() == Some("ONUCn80fsiISFWKrVMEiirNVr-QEvi7uQI0QH9q9q4o")));
498486
});
499487

500488
Ok(())

crates/jose/src/jwk/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use mas_iana::jose::{
1313
use schemars::JsonSchema;
1414
use serde::{Deserialize, Serialize};
1515
use serde_with::skip_serializing_none;
16+
use sha2::{Digest, Sha256};
1617
use url::Url;
1718

1819
use crate::{
@@ -239,6 +240,28 @@ impl<P> JsonWebKey<P> {
239240
}
240241
}
241242

243+
/// Methods to calculate RFC 7638 JWK Thumbprints.
244+
pub trait Thumbprint {
245+
/// Returns the RFC 7638 JWK Thumbprint JSON string.
246+
fn thumbprint_prehashed(&self) -> String;
247+
248+
/// Returns the RFC 7638 SHA256-hashed JWK Thumbprint.
249+
fn thumbprint_sha256(&self) -> [u8; 32] {
250+
Sha256::digest(self.thumbprint_prehashed()).into()
251+
}
252+
253+
/// Returns the RFC 7638 SHA256-hashed JWK Thumbprint as base64url string.
254+
fn thumbprint_sha256_base64(&self) -> String {
255+
Base64UrlNoPad::new(self.thumbprint_sha256().into()).encode()
256+
}
257+
}
258+
259+
impl<P: Thumbprint> Thumbprint for JsonWebKey<P> {
260+
fn thumbprint_prehashed(&self) -> String {
261+
self.parameters.thumbprint_prehashed()
262+
}
263+
}
264+
242265
impl<P> Constrainable for JsonWebKey<P>
243266
where
244267
P: ParametersInfo,

crates/jose/src/jwk/public_parameters.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use schemars::JsonSchema;
1111
use serde::{Deserialize, Serialize};
1212

1313
use super::ParametersInfo;
14-
use crate::base64::Base64UrlNoPad;
14+
use crate::{base64::Base64UrlNoPad, jwk::Thumbprint};
1515

1616
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1717
#[serde(tag = "kty")]
@@ -52,6 +52,22 @@ impl JsonWebKeyPublicParameters {
5252
}
5353
}
5454

55+
impl Thumbprint for JsonWebKeyPublicParameters {
56+
fn thumbprint_prehashed(&self) -> String {
57+
match self {
58+
JsonWebKeyPublicParameters::Rsa(RsaPublicParameters { n, e }) => {
59+
format!("{{\"e\":\"{e}\",\"kty\":\"RSA\",\"n\":\"{n}\"}}")
60+
}
61+
JsonWebKeyPublicParameters::Ec(EcPublicParameters { crv, x, y }) => {
62+
format!("{{\"crv\":\"{crv}\",\"kty\":\"EC\",\"x\":\"{x}\",\"y\":\"{y}\"}}")
63+
}
64+
JsonWebKeyPublicParameters::Okp(OkpPublicParameters { crv, x }) => {
65+
format!("{{\"crv\":\"{crv}\",\"kty\":\"OKP\",\"x\":\"{x}\"}}")
66+
}
67+
}
68+
}
69+
}
70+
5571
impl ParametersInfo for JsonWebKeyPublicParameters {
5672
fn kty(&self) -> JsonWebKeyType {
5773
match self {
@@ -300,3 +316,31 @@ mod ec_impls {
300316
}
301317
}
302318
}
319+
320+
#[cfg(test)]
321+
mod tests {
322+
use super::*;
323+
324+
#[test]
325+
fn test_thumbprint_rfc_example() {
326+
// From https://www.rfc-editor.org/rfc/rfc7638.html#section-3.1
327+
let n = Base64UrlNoPad::parse(
328+
"\
329+
0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt\
330+
VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6\
331+
4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD\
332+
W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9\
333+
1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH\
334+
aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
335+
)
336+
.unwrap();
337+
let e = Base64UrlNoPad::parse("AQAB").unwrap();
338+
339+
let jwkpps = JsonWebKeyPublicParameters::Rsa(RsaPublicParameters { n, e });
340+
341+
assert_eq!(
342+
jwkpps.thumbprint_sha256_base64(),
343+
"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
344+
);
345+
}
346+
}

crates/keystore/src/lib.rs

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@
99
use std::{ops::Deref, sync::Arc};
1010

1111
use der::{Decode, Encode, EncodePem, zeroize::Zeroizing};
12-
use elliptic_curve::{
13-
pkcs8::{EncodePrivateKey, EncodePublicKey},
14-
sec1::ToEncodedPoint,
15-
};
16-
use k256::sha2::{Digest, Sha256};
12+
use elliptic_curve::{pkcs8::EncodePrivateKey, sec1::ToEncodedPoint};
1713
use mas_iana::jose::{JsonWebKeyType, JsonWebSignatureAlg};
1814
pub use mas_jose::jwk::{JsonWebKey, JsonWebKeySet};
1915
use mas_jose::{
2016
jwa::{AsymmetricSigningKey, AsymmetricVerifyingKey},
21-
jwk::{JsonWebKeyPublicParameters, ParametersInfo, PublicJsonWebKeySet},
17+
jwk::{JsonWebKeyPublicParameters, ParametersInfo, PublicJsonWebKeySet, Thumbprint},
2218
};
2319
use pem_rfc7468::PemLabel;
2420
use pkcs1::EncodeRsaPrivateKey;
@@ -183,24 +179,6 @@ impl PrivateKey {
183179
}
184180
}
185181

186-
/// Returns the fingerprint of the private key.
187-
///
188-
/// The fingerprint is calculated as the SHA256 sum over the PKCS#8 ASN.1
189-
/// DER-encoded bytes of the private key’s corresponding public key.
190-
///
191-
/// # Errors
192-
///
193-
/// Errors if the DER representation of the public key can’t be derived.
194-
pub fn fingerprint(&self) -> pkcs8::spki::Result<[u8; 32]> {
195-
let bytes = match self {
196-
PrivateKey::Rsa(key) => key.to_public_key().to_public_key_der()?,
197-
PrivateKey::EcP256(key) => key.public_key().to_public_key_der()?,
198-
PrivateKey::EcP384(key) => key.public_key().to_public_key_der()?,
199-
PrivateKey::EcK256(key) => key.public_key().to_public_key_der()?,
200-
};
201-
Ok(Sha256::digest(bytes).into())
202-
}
203-
204182
/// Serialize the key as a DER document
205183
///
206184
/// It will use the most common format depending on the key type: PKCS1 for
@@ -621,6 +599,12 @@ impl ParametersInfo for PrivateKey {
621599
}
622600
}
623601

602+
impl Thumbprint for PrivateKey {
603+
fn thumbprint_prehashed(&self) -> String {
604+
JsonWebKeyPublicParameters::from(self).thumbprint_prehashed()
605+
}
606+
}
607+
624608
/// A structure to store a list of [`PrivateKey`]. The keys are held in an
625609
/// [`Arc`] to ensure they are only loaded once in memory and allow cheap
626610
/// cloning

docs/config.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1555,7 +1555,7 @@
15551555
"type": "object",
15561556
"properties": {
15571557
"kid": {
1558-
"description": "The key ID `kid` of the key as used by JWKs.\n\nIf not given, `kid` will be derived from the key by hex-encoding the first four bytes of the key’s fingerprint.",
1558+
"description": "The key ID `kid` of the key as used by JWKs.\n\nIf not given, `kid` will be the key’s RFC 7638 JWK Thumbprint.",
15591559
"type": "string"
15601560
},
15611561
"password_file": {

0 commit comments

Comments
 (0)