use serde::{Deserialize, Serialize, de::Error as _};
use crate::algorithm::JwsAlgorithm;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct JwkSet {
pub keys: Vec<PublicJwk>,
}
impl JwkSet {
pub fn new<I: IntoIterator<Item = PublicJwk>>(keys: I) -> Self {
Self {
keys: keys.into_iter().collect(),
}
}
pub fn find(&self, key_id: &str) -> Option<&PublicJwk> {
self.keys.iter().find(|key| key.key_id() == Some(key_id))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum KeyUse {
#[default]
#[serde(rename = "sig")]
Signature,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EcCurve {
#[serde(rename = "P-256")]
P256,
#[serde(rename = "P-384")]
P384,
}
impl EcCurve {
pub const fn as_str(&self) -> &'static str {
match self {
Self::P256 => "P-256",
Self::P384 => "P-384",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum OkpCurve {
Ed25519,
}
impl OkpCurve {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Ed25519 => "Ed25519",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kty")]
#[non_exhaustive]
pub enum PublicKey {
#[serde(rename = "EC")]
Ec {
crv: EcCurve,
x: String,
y: String,
},
#[serde(rename = "RSA")]
Rsa {
n: String,
e: String,
},
#[serde(rename = "OKP")]
Okp {
crv: OkpCurve,
x: String,
},
}
impl PublicKey {
pub const fn key_type(&self) -> &'static str {
match self {
Self::Ec { .. } => "EC",
Self::Rsa { .. } => "RSA",
Self::Okp { .. } => "OKP",
}
}
pub const fn supports(&self, algorithm: JwsAlgorithm) -> bool {
matches!(
(self, algorithm),
(
Self::Ec {
crv: EcCurve::P256,
..
},
JwsAlgorithm::ES256
) | (
Self::Ec {
crv: EcCurve::P384,
..
},
JwsAlgorithm::ES384
) | (
Self::Okp {
crv: OkpCurve::Ed25519,
..
},
JwsAlgorithm::EdDSA
) | (
Self::Rsa { .. },
JwsAlgorithm::RS256
| JwsAlgorithm::RS384
| JwsAlgorithm::RS512
| JwsAlgorithm::PS256
| JwsAlgorithm::PS384
| JwsAlgorithm::PS512
)
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnsupportedAlgorithm {
algorithm: JwsAlgorithm,
key_type: &'static str,
}
impl UnsupportedAlgorithm {
#[inline]
pub fn algorithm(&self) -> JwsAlgorithm {
self.algorithm
}
#[inline]
pub fn key_type(&self) -> &'static str {
self.key_type
}
}
impl std::fmt::Display for UnsupportedAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"a {} key cannot carry the {} algorithm",
self.key_type, self.algorithm
)
}
}
impl std::error::Error for UnsupportedAlgorithm {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PublicJwk {
#[serde(flatten)]
key: PublicKey,
#[serde(rename = "kid", skip_serializing_if = "Option::is_none")]
key_id: Option<String>,
#[serde(rename = "alg", skip_serializing_if = "Option::is_none")]
algorithm: Option<JwsAlgorithm>,
#[serde(rename = "use")]
key_use: KeyUse,
}
impl PublicJwk {
pub fn new(key: PublicKey) -> Self {
Self {
key,
key_id: None,
algorithm: None,
key_use: KeyUse::Signature,
}
}
pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
self.key_id = Some(key_id.into());
self
}
pub fn with_algorithm(mut self, algorithm: JwsAlgorithm) -> Result<Self, UnsupportedAlgorithm> {
if !self.key.supports(algorithm) {
return Err(UnsupportedAlgorithm {
algorithm,
key_type: self.key.key_type(),
});
}
self.algorithm = Some(algorithm);
Ok(self)
}
#[inline]
pub fn key(&self) -> &PublicKey {
&self.key
}
#[inline]
pub fn key_id(&self) -> Option<&str> {
self.key_id.as_deref()
}
#[inline]
pub fn algorithm(&self) -> Option<JwsAlgorithm> {
self.algorithm
}
pub fn thumbprint_input(&self) -> String {
let members: std::collections::BTreeMap<&str, &str> = match &self.key {
PublicKey::Ec { crv, x, y } => [
("kty", "EC"),
("crv", crv.as_str()),
("x", x.as_str()),
("y", y.as_str()),
]
.into(),
PublicKey::Rsa { n, e } => {
[("kty", "RSA"), ("n", n.as_str()), ("e", e.as_str())].into()
}
PublicKey::Okp { crv, x } => {
[("kty", "OKP"), ("crv", crv.as_str()), ("x", x.as_str())].into()
}
};
serde_json::to_string(&members).expect("a map of strings always serializes")
}
}
const PRIVATE_MEMBERS: [&str; 8] = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
impl<'de> Deserialize<'de> for PublicJwk {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Public {
#[serde(flatten)]
key: PublicKey,
#[serde(rename = "kid")]
key_id: Option<String>,
#[serde(rename = "alg")]
algorithm: Option<JwsAlgorithm>,
#[serde(rename = "use", default)]
key_use: KeyUse,
}
let document = serde_json::Value::deserialize(deserializer)?;
if let Some(members) = document.as_object() {
if let Some(member) = PRIVATE_MEMBERS
.iter()
.find(|member| members.contains_key(**member))
{
return Err(D::Error::custom(format!(
"the JWK carries the private member '{member}'; a published key holds \
public material only"
)));
}
if let Some(operations) = members.get("key_ops")
&& !operations
.as_array()
.is_some_and(|operations| operations.iter().any(|op| op == "verify"))
{
return Err(D::Error::custom(
"the JWK is restricted by `key_ops` to operations that exclude 'verify'; \
a published signing key has to permit signature verification",
));
}
}
let public: Public = serde_json::from_value(document).map_err(D::Error::custom)?;
let jwk = Self::new(public.key);
let jwk = match public.algorithm {
Some(algorithm) => jwk.with_algorithm(algorithm).map_err(D::Error::custom)?,
None => jwk,
};
Ok(Self {
key_id: public.key_id,
key_use: public.key_use,
..jwk
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ec_key() -> PublicKey {
PublicKey::Ec {
crv: EcCurve::P256,
x: "z9O8S-Itj6aJliZmUmWTG0Ko-GG23Wi6M3qbdjh5w-g".into(),
y: "nuk1MebXY11oQniSfOsKSnqmGjYQUhCHlwqeoeb6FDA".into(),
}
}
#[test]
fn it_serializes_every_key_type_per_rfc7518() {
let cases = [
(
PublicKey::Ec {
crv: EcCurve::P384,
x: "x".into(),
y: "y".into(),
},
json!({"kty": "EC", "crv": "P-384", "x": "x", "y": "y", "use": "sig"}),
),
(
PublicKey::Rsa {
n: "n".into(),
e: "AQAB".into(),
},
json!({"kty": "RSA", "n": "n", "e": "AQAB", "use": "sig"}),
),
(
PublicKey::Okp {
crv: OkpCurve::Ed25519,
x: "x".into(),
},
json!({"kty": "OKP", "crv": "Ed25519", "x": "x", "use": "sig"}),
),
];
for (key, expected) in cases {
let jwk = PublicJwk::new(key);
assert_eq!(serde_json::to_value(&jwk).unwrap(), expected);
assert_eq!(serde_json::from_value::<PublicJwk>(expected).unwrap(), jwk);
}
}
#[test]
fn it_carries_the_optional_members() {
let jwk = PublicJwk::new(ec_key())
.with_key_id("2026-08")
.with_algorithm(JwsAlgorithm::ES256)
.unwrap();
assert_eq!(jwk.key_id(), Some("2026-08"));
assert_eq!(jwk.algorithm(), Some(JwsAlgorithm::ES256));
assert!(matches!(jwk.key(), PublicKey::Ec { .. }));
let json = serde_json::to_value(&jwk).unwrap();
assert_eq!(json["kid"], "2026-08");
assert_eq!(json["alg"], "ES256");
assert_eq!(serde_json::from_value::<PublicJwk>(json).unwrap(), jwk);
let bare = serde_json::to_value(PublicJwk::new(ec_key())).unwrap();
assert!(bare.get("kid").is_none() && bare.get("alg").is_none());
}
#[test]
fn it_refuses_a_private_key() {
for member in PRIVATE_MEMBERS {
let mut document = serde_json::to_value(PublicJwk::new(ec_key())).unwrap();
document[member] = "the-secret".into();
let err = serde_json::from_value::<PublicJwk>(document)
.unwrap_err()
.to_string();
assert!(err.contains(member), "{member} was not refused: {err}");
}
}
#[test]
fn it_pairs_algorithms_with_the_material_that_carries_them() {
let ec384 = PublicKey::Ec {
crv: EcCurve::P384,
x: "x".into(),
y: "y".into(),
};
let rsa = PublicKey::Rsa {
n: "n".into(),
e: "AQAB".into(),
};
let okp = PublicKey::Okp {
crv: OkpCurve::Ed25519,
x: "x".into(),
};
assert!(ec_key().supports(JwsAlgorithm::ES256));
assert!(!ec_key().supports(JwsAlgorithm::ES384));
assert!(ec384.supports(JwsAlgorithm::ES384));
assert!(!ec384.supports(JwsAlgorithm::ES256));
assert!(okp.supports(JwsAlgorithm::EdDSA));
for rsa_alg in [
JwsAlgorithm::RS256,
JwsAlgorithm::RS384,
JwsAlgorithm::RS512,
JwsAlgorithm::PS256,
JwsAlgorithm::PS384,
JwsAlgorithm::PS512,
] {
assert!(rsa.supports(rsa_alg));
assert!(!ec_key().supports(rsa_alg));
}
for hmac in [
JwsAlgorithm::HS256,
JwsAlgorithm::HS384,
JwsAlgorithm::HS512,
] {
for key in [ec_key(), rsa.clone(), okp.clone()] {
assert!(!key.supports(hmac), "{} accepted {hmac}", key.key_type());
}
}
let err = PublicJwk::new(rsa.clone())
.with_algorithm(JwsAlgorithm::ES256)
.unwrap_err();
assert_eq!(err.key_type(), "RSA");
assert_eq!(err.algorithm(), JwsAlgorithm::ES256);
assert_eq!(
err.to_string(),
"a RSA key cannot carry the ES256 algorithm"
);
assert!(
PublicJwk::new(rsa)
.with_algorithm(JwsAlgorithm::RS256)
.is_ok()
);
}
#[test]
fn it_refuses_a_declared_algorithm_the_key_cannot_carry() {
for document in [
json!({"kty": "RSA", "n": "n", "e": "AQAB", "alg": "ES256"}),
json!({"kty": "EC", "crv": "P-384", "x": "x", "y": "y", "alg": "ES256"}),
json!({"kty": "EC", "crv": "P-256", "x": "x", "y": "y", "alg": "HS256"}),
json!({"kty": "OKP", "crv": "Ed25519", "x": "x", "alg": "ES256"}),
] {
let err = serde_json::from_value::<PublicJwk>(document.clone())
.unwrap_err()
.to_string();
assert!(err.contains("cannot carry"), "accepted {document}: {err}");
}
for document in [
json!({"kty": "RSA", "n": "n", "e": "AQAB", "alg": "PS512"}),
json!({"kty": "EC", "crv": "P-384", "x": "x", "y": "y", "alg": "ES384"}),
json!({"kty": "OKP", "crv": "Ed25519", "x": "x", "alg": "EdDSA"}),
json!({"kty": "EC", "crv": "P-256", "x": "x", "y": "y"}),
] {
assert!(
serde_json::from_value::<PublicJwk>(document.clone()).is_ok(),
"refused {document}"
);
}
}
#[test]
fn it_refuses_documents_it_cannot_publish() {
for document in [
json!({"kty": "oct", "k": "c2VjcmV0"}),
json!({"kty": "EC", "crv": "P-521", "x": "x", "y": "y"}),
json!({"kty": "EC", "crv": "P-256", "x": "x"}),
json!({"kty": "RSA", "n": "n"}),
json!({"kty": "EC", "crv": "Ed25519", "x": "x", "y": "y"}),
json!({"kty": "OKP", "crv": "P-256", "x": "x"}),
json!({"kty": "OKP", "crv": "X25519", "x": "x"}),
] {
assert!(
serde_json::from_value::<PublicJwk>(document.clone()).is_err(),
"accepted {document}"
);
}
let mut document = serde_json::to_value(PublicJwk::new(ec_key())).unwrap();
document["use"] = "enc".into();
assert!(serde_json::from_value::<PublicJwk>(document).is_err());
let mut document = serde_json::to_value(PublicJwk::new(ec_key())).unwrap();
document["x5t#S256"] = "thumbprint".into();
assert!(serde_json::from_value::<PublicJwk>(document).is_ok());
}
#[test]
fn it_refuses_a_key_restricted_away_from_verification() {
let with_ops = |operations: serde_json::Value| {
let mut document = serde_json::to_value(PublicJwk::new(ec_key())).unwrap();
document["key_ops"] = operations;
serde_json::from_value::<PublicJwk>(document)
};
for operations in [
json!(["encrypt"]),
json!(["decrypt", "unwrapKey"]),
json!(["deriveKey"]),
json!(["sign"]),
json!([]),
json!("verify"),
json!(null),
] {
let err = with_ops(operations.clone()).unwrap_err().to_string();
assert!(err.contains("key_ops"), "accepted {operations}: {err}");
}
for operations in [json!(["verify"]), json!(["sign", "verify"])] {
assert!(with_ops(operations.clone()).is_ok(), "refused {operations}");
}
}
#[test]
fn it_renders_the_rfc7638_thumbprint_input() {
let rfc_example = PublicJwk::new(PublicKey::Rsa {
n: RFC7638_MODULUS.into(),
e: "AQAB".into(),
});
assert_eq!(
rfc_example.thumbprint_input(),
format!(r#"{{"e":"AQAB","kty":"RSA","n":"{RFC7638_MODULUS}"}}"#)
);
assert_eq!(
PublicJwk::new(ec_key()).thumbprint_input(),
r#"{"crv":"P-256","kty":"EC","x":"z9O8S-Itj6aJliZmUmWTG0Ko-GG23Wi6M3qbdjh5w-g","y":"nuk1MebXY11oQniSfOsKSnqmGjYQUhCHlwqeoeb6FDA"}"#
);
assert_eq!(
PublicJwk::new(PublicKey::Okp {
crv: OkpCurve::Ed25519,
x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo".into(),
})
.thumbprint_input(),
r#"{"crv":"Ed25519","kty":"OKP","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}"#
);
let described = PublicJwk::new(ec_key())
.with_key_id("2026-08")
.with_algorithm(JwsAlgorithm::ES256)
.unwrap();
assert_eq!(
described.thumbprint_input(),
PublicJwk::new(ec_key()).thumbprint_input()
);
}
const RFC7638_MODULUS: &str = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86z\
wu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5\
v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4\
vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw";
#[test]
fn it_builds_and_searches_a_set() {
let set = JwkSet::new([
PublicJwk::new(ec_key()).with_key_id("old"),
PublicJwk::new(ec_key()).with_key_id("current"),
]);
assert_eq!(set.find("current").unwrap().key_id(), Some("current"));
assert!(set.find("missing").is_none());
assert!(JwkSet::default().keys.is_empty());
let json = serde_json::to_value(&set).unwrap();
assert_eq!(json["keys"][1]["kid"], "current");
assert_eq!(serde_json::from_value::<JwkSet>(json).unwrap(), set);
}
}