use base64ct::{Base64UrlUnpadded, Encoding};
use sha2::{Digest, Sha256};
pub fn compute_jwk_thumbprint(pubkey_bytes: &[u8; 32]) -> String {
let x = Base64UrlUnpadded::encode_string(pubkey_bytes);
let canonical_jwk = format!(r#"{{"crv":"Ed25519","kty":"OKP","x":"{}"}}"#, x);
let digest = Sha256::digest(canonical_jwk.as_bytes());
Base64UrlUnpadded::encode_string(&digest)
}
pub fn compute_jwk_thumbprint_p256(x: &[u8; 32], y: &[u8; 32]) -> String {
let x_b64 = Base64UrlUnpadded::encode_string(x);
let y_b64 = Base64UrlUnpadded::encode_string(y);
let canonical_jwk = format!(
r#"{{"crv":"P-256","kty":"EC","x":"{}","y":"{}"}}"#,
x_b64, y_b64
);
let digest = Sha256::digest(canonical_jwk.as_bytes());
Base64UrlUnpadded::encode_string(&digest)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thumbprint_is_reproducible() {
let key = [0xABu8; 32];
let t1 = compute_jwk_thumbprint(&key);
let t2 = compute_jwk_thumbprint(&key);
assert_eq!(t1, t2);
assert_eq!(t1.len(), 43);
}
#[test]
fn p256_thumbprint_is_reproducible_and_well_formed() {
let x = [0x11u8; 32];
let y = [0x22u8; 32];
let t1 = compute_jwk_thumbprint_p256(&x, &y);
let t2 = compute_jwk_thumbprint_p256(&x, &y);
assert_eq!(t1, t2);
assert_eq!(t1.len(), 43);
}
#[test]
fn p256_thumbprint_distinct_from_ed25519_for_same_x() {
let x = [0x11u8; 32];
let y = [0x00u8; 32];
let p256_t = compute_jwk_thumbprint_p256(&x, &y);
let ed_t = compute_jwk_thumbprint(&x);
assert_ne!(p256_t, ed_t);
}
#[test]
fn p256_thumbprint_canonical_form_is_lex_sorted_no_whitespace() {
let x = [0x33u8; 32];
let y = [0x44u8; 32];
let ours = compute_jwk_thumbprint_p256(&x, &y);
let x_b64 = Base64UrlUnpadded::encode_string(&x);
let y_b64 = Base64UrlUnpadded::encode_string(&y);
let canonical = format!(
"{{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"{}\",\"y\":\"{}\"}}",
x_b64, y_b64
);
let expected = Base64UrlUnpadded::encode_string(&Sha256::digest(canonical.as_bytes()));
assert_eq!(ours, expected);
}
}