diff --git a/.github/workflows/jose-jwk.yml b/.github/workflows/jose-jwk.yml index ceb6314..06c8b2f 100644 --- a/.github/workflows/jose-jwk.yml +++ b/.github/workflows/jose-jwk.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 837bfc8..6751a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "block-buffer" -version = "0.11.0-rc.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ef36a6fcdb072aa548f3da057640ec10859eb4e91ddf526ee648d50c76a949" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ "hybrid-array", ] @@ -47,6 +47,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crypto-bigint" version = "0.7.0-rc.4" @@ -63,9 +72,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.0-rc.4" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "hybrid-array", ] @@ -94,9 +103,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.0-rc.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a4aae35a0fcbe22ff1be50fe96df72002d5a4a6fb4aae9193cf2da0daa36da2" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer", "const-oid", @@ -162,9 +171,9 @@ checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" [[package]] name = "hybrid-array" -version = "0.4.0" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe39a812f039072707ce38020acbab2f769087952eddd9e2b890f37654b2349" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" dependencies = [ "typenum", "zeroize", @@ -225,6 +234,7 @@ dependencies = [ "serde", "serde_json", "serdect 0.2.0", + "sha2", "subtle", "url", "zeroize", @@ -255,6 +265,12 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + [[package]] name = "libm" version = "0.2.15" @@ -464,6 +480,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.11.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signature" version = "3.0.0-rc.3" diff --git a/jose-jwk/Cargo.toml b/jose-jwk/Cargo.toml index f76c0ba..018b05a 100644 --- a/jose-jwk/Cargo.toml +++ b/jose-jwk/Cargo.toml @@ -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"] } @@ -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 diff --git a/jose-jwk/README.md b/jose-jwk/README.md index 35a2b7d..c7748a7 100644 --- a/jose-jwk/README.md +++ b/jose-jwk/README.md @@ -7,9 +7,9 @@ ![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 @@ -17,9 +17,12 @@ 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": [ @@ -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] @@ -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 diff --git a/jose-jwk/src/key/ec.rs b/jose-jwk/src/key/ec.rs index bfa8e4a..4149255 100644 --- a/jose-jwk/src/key/ec.rs +++ b/jose-jwk/src/key/ec.rs @@ -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", + } + } +} diff --git a/jose-jwk/src/key/okp.rs b/jose-jwk/src/key/okp.rs index 00f7edd..97cb46c 100644 --- a/jose-jwk/src/key/okp.rs +++ b/jose-jwk/src/key/okp.rs @@ -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", + } + } +} diff --git a/jose-jwk/src/lib.rs b/jose-jwk/src/lib.rs index 178c5f7..6f437bc 100644 --- a/jose-jwk/src/lib.rs +++ b/jose-jwk/src/lib.rs @@ -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; diff --git a/jose-jwk/src/thumbprint.rs b/jose-jwk/src/thumbprint.rs new file mode 100644 index 0000000..1b91f89 --- /dev/null +++ b/jose-jwk/src/thumbprint.rs @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2025 Phantom Technologies, Inc. +// 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::() + } + + /// Compute the JWK thumbprint using a custom digest function. + fn thumbprint_with_digest(&self) -> String; +} + +impl JwkThumbprint for Ec { + fn thumbprint_with_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(&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(&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(&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(&self) -> String { + self.key.thumbprint_with_digest::() + } +} + +impl JwkThumbprint for Key { + fn thumbprint_with_digest(&self) -> String { + match self { + Key::Ec(ec) => ec.thumbprint_with_digest::(), + Key::Rsa(rsa) => rsa.thumbprint_with_digest::(), + Key::Oct(oct) => oct.thumbprint_with_digest::(), + Key::Okp(okp) => okp.thumbprint_with_digest::(), + } + } +} + +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(digest: &mut D, chunk: impl AsRef<[u8]>) { + let mut encoder: Encoder> = Encoder::from(DigestUpdater(digest)); + let _ = encoder.update(chunk); + let _ = encoder.finish(); +} diff --git a/jose-jwk/tests/thumbprint.rs b/jose-jwk/tests/thumbprint.rs new file mode 100644 index 0000000..1b9f021 --- /dev/null +++ b/jose-jwk/tests/thumbprint.rs @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: 2025 Phantom Technologies, Inc. +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! JWK Thumbprint tests including RFC 7638 examples. + +#![cfg(feature = "thumbprint")] +#![allow(clippy::indexing_slicing)] // usage is always valid + +use std::collections::HashSet; + +use jose_jwk::*; +use sha2::{Sha256, Sha384, Sha512}; + +/// Test the RSA key example from RFC 7638 Section 3.1. +#[test] +fn test_rfc7638_rsa_example() { + // This is the RSA key from RFC 7638 Section 3.1 + let rsa_key = Rsa { + e: vec![1, 0, 1].into(), // "AQAB" in base64url + n: vec![ + 210, 252, 123, 106, 10, 30, 108, 103, 16, 74, 235, 143, 136, 178, 87, 102, 155, 77, + 246, 121, 221, 173, 9, 155, 92, 74, 108, 217, 168, 128, 21, 181, 161, 51, 191, 11, 133, + 108, 120, 113, 182, 223, 0, 11, 85, 79, 206, 179, 194, 237, 81, 43, 182, 143, 20, 92, + 110, 132, 52, 117, 47, 171, 82, 161, 207, 193, 36, 64, 143, 121, 181, 138, 69, 120, + 193, 100, 40, 133, 87, 137, 247, 162, 73, 227, 132, 203, 45, 159, 174, 45, 103, 253, + 150, 251, 146, 108, 25, 142, 7, 115, 153, 253, 200, 21, 192, 175, 9, 125, 222, 90, 173, + 239, 244, 77, 231, 14, 130, 127, 72, 120, 67, 36, 57, 191, 238, 185, 96, 104, 208, 71, + 79, 197, 13, 109, 144, 191, 58, 152, 223, 175, 16, 64, 200, 156, 2, 214, 146, 171, 59, + 60, 40, 150, 96, 157, 134, 253, 115, 183, 116, 206, 7, 64, 100, 124, 238, 234, 163, 16, + 189, 18, 249, 133, 168, 235, 159, 89, 253, 212, 38, 206, 165, 178, 18, 15, 79, 42, 52, + 188, 171, 118, 75, 126, 108, 84, 214, 132, 2, 56, 188, 196, 5, 135, 165, 158, 102, 237, + 31, 51, 137, 69, 119, 99, 92, 71, 10, 247, 92, 249, 44, 32, 209, 218, 67, 225, 191, + 196, 25, 226, 34, 166, 240, 208, 187, 53, 140, 94, 56, 249, 203, 5, 10, 234, 254, 144, + 72, 20, 241, 172, 26, 164, 156, 202, 158, 160, 202, 131, + ] + .into(), + prv: None, + }; + + let jwk = Jwk { + key: Key::Rsa(rsa_key), + prm: Parameters::default(), + }; + + let thumbprint = jwk.thumbprint(); + + // Expected result from RFC 7638 Section 3.1 + assert_eq!(thumbprint, "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"); +} + +/// Test EC P-256 key thumbprint using RFC 7517 Appendix A.1 example. +/// Reference: https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.1 +#[test] +fn test_ec_p256_thumbprint() { + // Key values from RFC 7517 A.1: + // x: MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4 + // y: 4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM + // crv: P-256 + let ec_key = Ec { + crv: EcCurves::P256, + x: vec![ + 48, 160, 66, 76, 210, 28, 41, 68, 131, 138, 45, 117, 201, 43, 55, 231, 110, 162, 13, + 159, 0, 137, 58, 59, 78, 238, 138, 60, 10, 175, 236, 62, + ] + .into(), + y: vec![ + 224, 75, 101, 233, 36, 86, 217, 136, 139, 82, 179, 121, 189, 251, 213, 30, 232, 105, + 239, 31, 15, 198, 91, 102, 89, 105, 91, 108, 206, 8, 23, 35, + ] + .into(), + d: None, + }; + + let jwk = Jwk { + key: Key::Ec(ec_key), + prm: Parameters::default(), + }; + + let thumbprint = jwk.thumbprint(); + + // Computed from SHA-256 of: + // {"crv":"P-256","kty":"EC","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"} + assert_eq!(thumbprint, "cn-I_WNMClehiVp51i_0VpOENW1upEerA8sEam5hn-s"); +} + +/// Test OKP Ed25519 key thumbprint using RFC 8037 Appendix A.2 example. +/// Reference: https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.2 +#[test] +fn test_okp_ed25519_thumbprint() { + // Key values from RFC 8037 A.2: + // x: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo + // crv: Ed25519 + let okp_key = Okp { + crv: OkpCurves::Ed25519, + x: vec![ + 215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114, + 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26, + ] + .into(), + d: None, + }; + + let jwk = Jwk { + key: Key::Okp(okp_key), + prm: Parameters::default(), + }; + + let thumbprint = jwk.thumbprint(); + + // Computed from SHA-256 of: + // {"crv":"Ed25519","kty":"OKP","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"} + assert_eq!(thumbprint, "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k"); +} + +/// Test Oct (symmetric) key thumbprint. +#[test] +fn test_oct_thumbprint() { + // k = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + // Base64url: AQIDBAUGBwgJCgsMDQ4PEA + let oct_key = Oct { + k: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].into(), + }; + + let jwk = Jwk { + key: Key::Oct(oct_key), + prm: Parameters::default(), + }; + + let thumbprint = jwk.thumbprint(); + + // Computed from SHA-256 of: {"k":"AQIDBAUGBwgJCgsMDQ4PEA","kty":"oct"} + assert_eq!(thumbprint, "enhFVP7_AlQw84MU6KWN9VKO0JkRsxKFtrIwGhIJhzo"); +} + +/// Test that private and public key representations produce the same thumbprint. +#[test] +fn test_private_public_key_same_thumbprint() { + // EC test + let ec_public = Ec { + crv: EcCurves::P256, + x: vec![1; 32].into(), + y: vec![2; 32].into(), + d: None, + }; + let ec_private = Ec { + crv: ec_public.crv, + x: ec_public.x.clone(), + y: ec_public.y.clone(), + d: Some(vec![100; 32].into()), + }; + assert_eq!(ec_public.thumbprint(), ec_private.thumbprint()); + + // OKP test + let okp_public = Okp { + crv: OkpCurves::Ed25519, + x: vec![1; 32].into(), + d: None, + }; + let okp_private = Okp { + crv: okp_public.crv, + x: okp_public.x.clone(), + d: Some(vec![100; 32].into()), + }; + assert_eq!(okp_public.thumbprint(), okp_private.thumbprint()); + + // RSA test + let rsa_public = Rsa { + e: vec![1, 0, 1].into(), + n: vec![0x12, 0x34].into(), + prv: None, + }; + let rsa_private = Rsa { + e: rsa_public.e.clone(), + n: rsa_public.n.clone(), + prv: Some(RsaPrivate { + d: vec![0xAB].into(), + opt: None, + }), + }; + assert_eq!(rsa_public.thumbprint(), rsa_private.thumbprint()); +} + +/// Test different hash algorithms produce different results with expected lengths. +#[test] +fn test_hash_algorithms() { + let rsa_key = Rsa { + e: vec![1, 0, 1].into(), + n: vec![0xAB, 0xCD, 0xEF, 0x12].into(), + prv: None, + }; + + let jwk = Jwk { + key: Key::Rsa(rsa_key), + prm: Parameters::default(), + }; + + let sha256_tp = jwk.thumbprint_with_digest::(); + let sha384_tp = jwk.thumbprint_with_digest::(); + let sha512_tp = jwk.thumbprint_with_digest::(); + + // Verify expected lengths + assert_eq!(sha256_tp.len(), 43); // 256 bits + assert_eq!(sha384_tp.len(), 64); // 384 bits + assert_eq!(sha512_tp.len(), 86); // 512 bits + + // All should be different + assert_ne!(sha256_tp, sha384_tp); + assert_ne!(sha256_tp, sha512_tp); + assert_ne!(sha384_tp, sha512_tp); + + // All should be valid base64url + for tp in [&sha256_tp, &sha384_tp, &sha512_tp] { + assert!(!tp.contains('=')); + assert!(!tp.contains('+')); + assert!(!tp.contains('/')); + } +} + +/// Test that different curves produce different thumbprints. +#[test] +fn test_curve_differentiation() { + // EC curves + let ec_curves = [ + EcCurves::P256, + EcCurves::P384, + EcCurves::P521, + EcCurves::P256K, + ]; + + let mut ec_thumbprints = Vec::new(); + for curve in ec_curves { + let ec = Ec { + crv: curve, + x: vec![1; 32].into(), + y: vec![2; 32].into(), + d: None, + }; + ec_thumbprints.push(ec.thumbprint()); + } + + // All EC thumbprints should be different + for i in 0..ec_thumbprints.len() { + for j in (i + 1)..ec_thumbprints.len() { + assert_ne!(ec_thumbprints[i], ec_thumbprints[j]); + } + } + + // OKP curves + let okp_curves = [ + OkpCurves::Ed25519, + OkpCurves::Ed448, + OkpCurves::X25519, + OkpCurves::X448, + ]; + + // All OKP thumbprints should be different + let mut okp_thumbprints = HashSet::new(); + for curve in okp_curves { + let okp = Okp { + crv: curve, + x: vec![1; 32].into(), + d: None, + }; + let thumbprint = okp.thumbprint(); + assert!(!okp_thumbprints.contains(&thumbprint)); + okp_thumbprints.insert(thumbprint); + } +} + +/// Test thumbprint consistency and determinism. +#[test] +fn test_thumbprint_determinism() { + let rsa_key = Rsa { + e: vec![1, 0, 1].into(), + n: vec![0xDE, 0xAD, 0xBE, 0xEF].into(), + prv: None, + }; + + // Test Jwk vs Key consistency + let key_tp = rsa_key.thumbprint(); + let jwk_tp = Jwk { + key: Key::Rsa(rsa_key.clone()), + prm: Parameters::default(), + } + .thumbprint(); + assert_eq!(key_tp, jwk_tp); + + // Test determinism across multiple computations + let first = rsa_key.thumbprint(); + for _ in 0..10 { + assert_eq!(rsa_key.thumbprint(), first); + } +} + +/// Test various key sizes and edge cases. +#[test] +fn test_key_size_edge_cases() { + // RSA with different sizes + let test_keys = vec![ + Key::Rsa(Rsa { + e: vec![1].into(), + n: vec![3].into(), + prv: None, + }), + Key::Rsa(Rsa { + e: vec![0x00, 0x01, 0x00, 0x01].into(), // Leading zeros + n: vec![0xFF; 256].into(), // Large modulus + prv: None, + }), + Key::Ec(Ec { + crv: EcCurves::P256, + x: vec![1].into(), + y: vec![2].into(), + d: None, + }), + Key::Ec(Ec { + crv: EcCurves::P521, + x: vec![0xFF; 66].into(), + y: vec![0xEE; 66].into(), + d: None, + }), + Key::Oct(Oct { + k: vec![0x42].into(), + }), + Key::Oct(Oct { + k: vec![0x5A; 1024].into(), + }), + Key::Okp(Okp { + crv: OkpCurves::Ed25519, + x: vec![1].into(), + d: None, + }), + ]; + + for key in test_keys { + let thumbprint = key.thumbprint(); + assert!(!thumbprint.is_empty()); + assert!(!thumbprint.contains('=')); + } +} + +/// Test that different key types produce different thumbprints. +#[test] +fn test_key_type_differentiation() { + let same_bytes = vec![0x01, 0x02, 0x03, 0x04]; + + let oct_tp = Oct { + k: same_bytes.clone().into(), + } + .thumbprint(); + + let okp_tp = Okp { + crv: OkpCurves::Ed25519, + x: same_bytes.into(), + d: None, + } + .thumbprint(); + + assert_ne!(oct_tp, okp_tp); +}