use base64ct::Encoding;
use sha2::{Digest, Sha256};
use crate::jwk::{EcParams, Key, KeyParams, OkpParams, RsaParams, SymmetricParams};
pub(crate) fn calculate_thumbprint(jwk: &Key) -> String {
let canonical_json = build_canonical_json(jwk);
let hash = Sha256::digest(canonical_json.as_bytes());
base64ct::Base64UrlUnpadded::encode_string(&hash)
}
fn build_canonical_json(jwk: &Key) -> String {
match &jwk.params {
KeyParams::Rsa(params) => build_rsa_canonical(params),
KeyParams::Ec(params) => build_ec_canonical(params),
KeyParams::Symmetric(params) => build_symmetric_canonical(params),
KeyParams::Okp(params) => build_okp_canonical(params),
}
}
fn build_rsa_canonical(params: &RsaParams) -> String {
format!(
r#"{{"e":"{}","kty":"RSA","n":"{}"}}"#,
params.e.to_base64url(),
params.n.to_base64url()
)
}
fn build_ec_canonical(params: &EcParams) -> String {
format!(
r#"{{"crv":"{}","kty":"EC","x":"{}","y":"{}"}}"#,
params.crv.as_str(),
params.x.to_base64url(),
params.y.to_base64url()
)
}
fn build_symmetric_canonical(params: &SymmetricParams) -> String {
format!(r#"{{"k":"{}","kty":"oct"}}"#, params.k.to_base64url())
}
fn build_okp_canonical(params: &OkpParams) -> String {
format!(
r#"{{"crv":"{}","kty":"OKP","x":"{}"}}"#,
params.crv.as_str(),
params.x.to_base64url()
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoding::Base64UrlBytes;
use crate::{EcCurve, OkpCurve};
#[test]
fn test_rsa_canonical_order() {
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![1, 0, 1]),
);
let canonical = build_rsa_canonical(¶ms);
assert!(canonical.starts_with(r#"{"e":"#));
assert!(canonical.contains(r#","kty":"RSA","#));
}
#[test]
fn test_ec_canonical_order() {
let params = EcParams::new_public(
EcCurve::P256,
Base64UrlBytes::new(vec![0; 32]),
Base64UrlBytes::new(vec![0; 32]),
);
let canonical = build_ec_canonical(¶ms);
assert!(canonical.starts_with(r#"{"crv":"#));
}
#[test]
fn test_symmetric_canonical_order() {
let params = SymmetricParams::new(Base64UrlBytes::new(vec![1, 2, 3]));
let canonical = build_symmetric_canonical(¶ms);
assert!(canonical.starts_with(r#"{"k":"#));
}
#[test]
fn test_okp_canonical_order() {
let params = OkpParams::new_public(OkpCurve::Ed25519, Base64UrlBytes::new(vec![0; 32]));
let canonical = build_okp_canonical(¶ms);
assert!(canonical.starts_with(r#"{"crv":"#));
}
#[test]
fn test_thumbprint_deterministic() {
let jwk = Key::new(KeyParams::Rsa(RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3, 4, 5]),
Base64UrlBytes::new(vec![1, 0, 1]),
)))
.with_kid("test-kid");
let t1 = calculate_thumbprint(&jwk);
let t2 = calculate_thumbprint(&jwk);
assert_eq!(t1, t2);
}
#[test]
fn test_thumbprint_ignores_optional_fields() {
let jwk1 = Key::new(KeyParams::Rsa(RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![1, 0, 1]),
)));
let jwk2 = Key::new(KeyParams::Rsa(RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![1, 0, 1]),
)))
.with_kid("different-kid")
.with_use(crate::KeyUse::Signature)
.with_alg(crate::Algorithm::Rs256);
assert_eq!(calculate_thumbprint(&jwk1), calculate_thumbprint(&jwk2));
}
#[test]
fn test_rfc7638_rsa_vector() {
let jwk_json = r#"{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e": "AQAB",
"alg": "RS256",
"kid": "2011-04-29"
}"#;
let jwk: Key = serde_json::from_str(jwk_json).unwrap();
assert_eq!(
calculate_thumbprint(&jwk),
"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
);
}
#[test]
fn test_rfc7638_ec_vector() {
let jwk_json = r#"{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"
}"#;
let jwk: Key = serde_json::from_str(jwk_json).unwrap();
assert_eq!(
calculate_thumbprint(&jwk),
"cn-I_WNMClehiVp51i_0VpOENW1upEerA8sEam5hn-s"
);
}
}