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
27 changes: 27 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions jose-jwk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ legacy = ["dep:base64ct", "dep:elliptic-curve", "dep:serde_json", "dep:serdect",
jose-b64 = { version = "=0.2.0-pre", default-features = false, features = ["secret"] }
jose-jwa = { version = "=0.2.0-pre" }
serde = { version = "1", default-features = false, features = ["alloc", "derive"] }
sha2 = { version = "=0.11.0-rc.2", default-features = false }
zeroize = { version = "1.8.1", default-features = false, features = ["alloc"] }

# optional dependencies
Expand Down
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",
}
}
}
2 changes: 2 additions & 0 deletions jose-jwk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ extern crate alloc;

pub mod crypto;
pub mod legacy;
pub mod thumbprint;

mod key;
mod prm;

pub use key::*;
pub use prm::{Class, Operations, Parameters, Thumbprint};
pub use thumbprint::{JwkThumbprint, ThumbprintError};

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

//! JWK Thumbprint implementation 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.

use alloc::string::String;

use jose_b64::base64ct::{Base64UrlUnpadded, Encoding};
use sha2::{Digest, Sha256};

use crate::key::{Ec, Oct, Okp, Rsa};
use crate::{Jwk, Key};

/// Trait for computing JWK thumbprints.
///
/// This trait is implemented for each key type to compute the thumbprint
/// according to RFC 7638 requirements.
pub trait JwkThumbprint {
/// Compute the JWK thumbprint using SHA-256.
///
/// Returns the base64url-encoded thumbprint string.
fn jwk_thumbprint(&self) -> Result<String, ThumbprintError>;

/// Compute the JWK thumbprint using a custom hash function.
///
/// Returns the base64url-encoded thumbprint string.
fn jwk_thumbprint_with_hash<D>(&self) -> Result<String, ThumbprintError>
where
D: Digest;
}

/// Errors that can occur during thumbprint computation.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ThumbprintError {
/// Failed to format the JSON representation.
JsonFormatError,
}

impl core::fmt::Display for ThumbprintError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ThumbprintError::JsonFormatError => write!(f, "failed to format JSON for thumbprint"),
}
}
}

impl JwkThumbprint for Ec {
fn jwk_thumbprint(&self) -> Result<String, ThumbprintError> {
self.jwk_thumbprint_with_hash::<Sha256>()
}

fn jwk_thumbprint_with_hash<D>(&self) -> Result<String, ThumbprintError>
where
D: Digest,
{
// Required members in lexicographic order: crv, kty, x, y
let buf = D::new()
.chain_update(r#"{"crv":""#)
.chain_update(self.crv.as_str())
.chain_update(r#"","kty":"EC","x":""#)
.chain_update(Base64UrlUnpadded::encode_string(&self.x))
.chain_update(r#"","y":""#)
.chain_update(Base64UrlUnpadded::encode_string(&self.y))
.chain_update(r#""}"#)
.finalize();

Ok(Base64UrlUnpadded::encode_string(&buf))
}
}

impl JwkThumbprint for Jwk {
/// Compute the JWK thumbprint using SHA-256 as defined in RFC 7638.
///
/// This method computes a cryptographic hash over the required members
/// of the JWK and returns the base64url-encoded result.
///
/// # Examples
///
/// ```
/// # use jose_jwk::{Jwk, JwkThumbprint, Key, Rsa};
/// let jwk = Jwk {
/// key: Key::Rsa(Rsa {
/// e: vec![1, 0, 1].into(),
/// n: vec![0xAB, 0xCD, 0xEF].into(),
/// prv: None,
/// }),
/// prm: Default::default(),
/// };
///
/// let thumbprint = jwk.jwk_thumbprint().unwrap();
/// assert!(!thumbprint.is_empty());
/// ```
fn jwk_thumbprint(&self) -> Result<String, ThumbprintError> {
self.key.jwk_thumbprint()
}

/// Compute the JWK thumbprint using a custom hash function as defined in RFC 7638.
///
/// This method allows using a different hash function than the default SHA-256.
///
/// # Examples
///
/// ```
/// # use jose_jwk::{Jwk, JwkThumbprint, Key, Rsa};
/// # use sha2::Sha512;
/// let jwk = Jwk {
/// key: Key::Rsa(Rsa {
/// e: vec![1, 0, 1].into(),
/// n: vec![0xAB, 0xCD, 0xEF].into(),
/// prv: None,
/// }),
/// prm: Default::default(),
/// };
///
/// let thumbprint = jwk.jwk_thumbprint_with_hash::<Sha512>().unwrap();
/// assert!(!thumbprint.is_empty());
/// ```
fn jwk_thumbprint_with_hash<D>(&self) -> Result<String, ThumbprintError>
where
D: Digest,
{
self.key.jwk_thumbprint_with_hash::<D>()
}
}

impl JwkThumbprint for Rsa {
fn jwk_thumbprint(&self) -> Result<String, ThumbprintError> {
self.jwk_thumbprint_with_hash::<Sha256>()
}

fn jwk_thumbprint_with_hash<D>(&self) -> Result<String, ThumbprintError>
where
D: Digest,
{
// Required members in lexicographic order: e, kty, n
let buf = D::new()
.chain_update(r#"{"e":""#)
.chain_update(Base64UrlUnpadded::encode_string(&self.e))
.chain_update(r#"","kty":"RSA","n":""#)
.chain_update(Base64UrlUnpadded::encode_string(&self.n))
.chain_update(r#""}"#)
.finalize();

Ok(Base64UrlUnpadded::encode_string(&buf))
}
}

impl JwkThumbprint for Oct {
fn jwk_thumbprint(&self) -> Result<String, ThumbprintError> {
self.jwk_thumbprint_with_hash::<Sha256>()
}

fn jwk_thumbprint_with_hash<D>(&self) -> Result<String, ThumbprintError>
where
D: Digest,
{
// Required members in lexicographic order: k, kty
let buf = D::new()
.chain_update(r#"{"k":""#)
.chain_update(Base64UrlUnpadded::encode_string(&self.k))
.chain_update(r#"","kty":"oct"}"#)
.finalize();

Ok(Base64UrlUnpadded::encode_string(&buf))
}
}

impl JwkThumbprint for Okp {
fn jwk_thumbprint(&self) -> Result<String, ThumbprintError> {
self.jwk_thumbprint_with_hash::<Sha256>()
}

fn jwk_thumbprint_with_hash<D>(&self) -> Result<String, ThumbprintError>
where
D: Digest,
{
// Required members in lexicographic order: crv, kty, x
let buf = D::new()
.chain_update(r#"{"crv":""#)
.chain_update(self.crv.as_str())
.chain_update(r#"","kty":"OKP","x":""#)
.chain_update(Base64UrlUnpadded::encode_string(&self.x))
.chain_update(r#""}"#)
.finalize();

Ok(Base64UrlUnpadded::encode_string(&buf))
}
}

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

fn jwk_thumbprint_with_hash<D>(&self) -> Result<String, ThumbprintError>
where
D: Digest,
{
match self {
Key::Ec(ec) => ec.jwk_thumbprint_with_hash::<D>(),
Key::Rsa(rsa) => rsa.jwk_thumbprint_with_hash::<D>(),
Key::Oct(oct) => oct.jwk_thumbprint_with_hash::<D>(),
Key::Okp(okp) => okp.jwk_thumbprint_with_hash::<D>(),
}
}
}
Loading