Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/jose-jwk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ jobs:
- p256
- p384
- rsa
- thumbprint
- url
- p256,p384,rsa,url
- p256,p384,rsa,thumbprint,url

# Test all combinations of crypto enablement
- p256,p384
Expand Down
43 changes: 35 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions jose-jwk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ rust-version = "1.85"
default = ["crypto"]
crypto = ["p256", "p384", "p521", "k256", "rsa"]
legacy = ["dep:base64ct", "dep:elliptic-curve", "dep:serde_json", "dep:serdect", "dep:subtle"]
thumbprint = ["dep:sha2"]

[dependencies]
jose-b64 = { version = "=0.2.0-pre", default-features = false, features = ["secret"] }
Expand All @@ -34,6 +35,7 @@ p384 = { version = "0.14.0-pre.9", default-features = false, optional = true, fe
p521 = { version = "0.14.0-pre.9", default-features = false, optional = true, features = ["arithmetic"] }
k256 = { version = "0.14.0-pre.9", default-features = false, optional = true, features = ["arithmetic"] }
rsa = { version = "0.10.0-rc.6", default-features = false, optional = true }
sha2 = { version = "0.11.0-rc.2", default-features = false, optional = true }
url = { version = "2.4.1", default-features = false, optional = true, features = ["serde"] }

# legacy dependencies
Expand Down
26 changes: 22 additions & 4 deletions jose-jwk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
![Rust Version][rustc-image]
[![Project Chat][chat-image]][chat-link]

Pure Rust implementation of the JSON Web Key ([JWK]) component of the
Javascript Object Signing and Encryption ([JOSE]) specification as described
in [RFC7517].
Pure Rust implementation of the JSON Web Key ([JWK]) and JWK Thumbprint
components of the Javascript Object Signing and Encryption ([JOSE])
specification as described in [RFC7517] and [RFC7638].

A JWK is a way to represent cryptographic keys in JSON, typically public keys.
This format contains information about how the key needs to be used so a child
node can validate what a parent node sends (e.g. with JWTs) or encrypt messages
for the parent node using this key (e.g. with JWEs). This crate provides data
structures to interface with this format.

A JWK Thumbprint is a hash of the required members of a JWK, and provides a
deterministic and unique identifier for the key.

```rust
use jose_jwk::{Jwk, JwkSet, Key};
use jose_jwk::jose_jwa::{Algorithm, Signing};
use jose_jwk::{Jwk, JwkSet, Key};

let keys = serde_json::json!({
"keys": [
Expand Down Expand Up @@ -57,6 +60,20 @@ assert_eq!(ec_jwk.prm.kid, Some(String::from("some-ec-kid")));
assert_eq!(rsa_jwk.prm.kid, Some(String::from("some-rsa-kid")));

assert_eq!(rsa_jwk.prm.alg, Some(Algorithm::Signing(Signing::Rs256)));

#[cfg(feature = "thumbprint")]
{
use jose_jwk::JwkThumbprint;

assert_eq!(
ec_jwk.thumbprint(),
"cn-I_WNMClehiVp51i_0VpOENW1upEerA8sEam5hn-s"
);
assert_eq!(
rsa_jwk.thumbprint(),
"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
);
}
```

[Documentation][docs-link]
Expand Down Expand Up @@ -102,3 +119,4 @@ dual licensed as above, without any additional terms or conditions.
[JWK]: https://jose.readthedocs.io/en/latest/#jwk
[JOSE]: https://jose.readthedocs.io/
[RFC7517]: https://www.rfc-editor.org/rfc/rfc7517
[RFC7638]: https://datatracker.ietf.org/doc/html/rfc7638
13 changes: 13 additions & 0 deletions jose-jwk/src/key/ec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,16 @@ pub enum EcCurves {
#[serde(rename = "secp256k1")]
P256K,
}

impl EcCurves {
/// Returns the string representation of the curve.
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
EcCurves::P256 => "P-256",
EcCurves::P384 => "P-384",
EcCurves::P521 => "P-521",
EcCurves::P256K => "secp256k1",
}
}
}
13 changes: 13 additions & 0 deletions jose-jwk/src/key/okp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,16 @@ pub enum OkpCurves {
/// X448
X448,
}

impl OkpCurves {
/// Returns the string representation of the curve.
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
OkpCurves::Ed25519 => "Ed25519",
OkpCurves::Ed448 => "Ed448",
OkpCurves::X25519 => "X25519",
OkpCurves::X448 => "X448",
}
}
}
3 changes: 3 additions & 0 deletions jose-jwk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ pub mod legacy;

mod key;
mod prm;
mod thumbprint;

pub use key::*;
pub use prm::{Class, Operations, Parameters, Thumbprint};
#[cfg(feature = "thumbprint")]
pub use thumbprint::JwkThumbprint;

pub use jose_b64;
pub use jose_jwa;
Expand Down
123 changes: 123 additions & 0 deletions jose-jwk/src/thumbprint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2025 Phantom Technologies, Inc. <legal@phantom.app>
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! JWK Thumbprint as defined in RFC 7638.
//!
//! This module provides methods for computing JWK Thumbprints, which are
//! cryptographic hash values computed over the required members of a JWK.

#![cfg(feature = "thumbprint")]

extern crate alloc;

use alloc::string::String;

use crate::{Ec, Jwk, Key, Oct, Okp, Rsa};
use jose_b64::base64ct::{Base64UrlUnpadded, Encoding};
use jose_b64::stream::{Encoder, Update};
use sha2::{Digest, Sha256};

/// Trait for computing JWK thumbprints.
pub trait JwkThumbprint {
/// Compute the JWK thumbprint using SHA-256.
fn thumbprint(&self) -> String {
self.thumbprint_with_digest::<Sha256>()
}

/// Compute the JWK thumbprint using a custom digest function.
fn thumbprint_with_digest<D: Digest>(&self) -> String;
}

impl JwkThumbprint for Ec {
fn thumbprint_with_digest<D: Digest>(&self) -> String {
let mut digest = D::new();

// Required members in lexicographic order: crv, kty, x, y
digest.update(r#"{"crv":""#);
digest.update(self.crv.as_str());
digest.update(r#"","kty":"EC","x":""#);
b64digest(&mut digest, &self.x);
digest.update(r#"","y":""#);
b64digest(&mut digest, &self.y);
digest.update(r#""}"#);

Base64UrlUnpadded::encode_string(&digest.finalize())
}
}

impl JwkThumbprint for Rsa {
fn thumbprint_with_digest<D: Digest>(&self) -> String {
let mut digest = D::new();

// Required members in lexicographic order: e, kty, n
digest.update(r#"{"e":""#);
b64digest(&mut digest, &self.e);
digest.update(r#"","kty":"RSA","n":""#);
b64digest(&mut digest, &self.n);
digest.update(r#""}"#);

Base64UrlUnpadded::encode_string(&digest.finalize())
}
}

impl JwkThumbprint for Oct {
fn thumbprint_with_digest<D: Digest>(&self) -> String {
let mut digest = D::new();

// Required members in lexicographic order: k, kty
digest.update(r#"{"k":""#);
b64digest(&mut digest, &*self.k);
digest.update(r#"","kty":"oct"}"#);

Base64UrlUnpadded::encode_string(&digest.finalize())
}
}

impl JwkThumbprint for Okp {
fn thumbprint_with_digest<D: Digest>(&self) -> String {
let mut digest = D::new();

// Required members in lexicographic order: crv, kty, x
digest.update(r#"{"crv":""#);
digest.update(self.crv.as_str());
digest.update(r#"","kty":"OKP","x":""#);
b64digest(&mut digest, &self.x);
digest.update(r#""}"#);

Base64UrlUnpadded::encode_string(&digest.finalize())
}
}

impl JwkThumbprint for Jwk {
fn thumbprint_with_digest<D: Digest>(&self) -> String {
self.key.thumbprint_with_digest::<D>()
}
}

impl JwkThumbprint for Key {
fn thumbprint_with_digest<D: Digest>(&self) -> String {
match self {
Key::Ec(ec) => ec.thumbprint_with_digest::<D>(),
Key::Rsa(rsa) => rsa.thumbprint_with_digest::<D>(),
Key::Oct(oct) => oct.thumbprint_with_digest::<D>(),
Key::Okp(okp) => okp.thumbprint_with_digest::<D>(),
}
}
}

struct DigestUpdater<'a, D>(&'a mut D);

impl<'a, D: Digest> Update for DigestUpdater<'a, D> {
type Error = ();

fn update(&mut self, chunk: impl AsRef<[u8]>) -> Result<(), Self::Error> {
self.0.update(chunk);
Ok(())
}
}

fn b64digest<D: Digest>(digest: &mut D, chunk: impl AsRef<[u8]>) {
let mut encoder: Encoder<DigestUpdater<'_, D>> = Encoder::from(DigestUpdater(digest));
let _ = encoder.update(chunk);
let _ = encoder.finish();
}
Loading
Loading