//! JSON Web Key Set (JWKS) support per RFC 7517, with key rotation.
//!
//! Gated behind the `asym-jwt` feature. Provides:
//!
//! * [`Jwk`] — a single public JWK (RFC 7517 §4) for an Ed25519 (`OKP`,
//! RFC 8037 §2) or P-256 (`EC`, RFC 7518 §6.2.1) key, with a `to_json`
//! emitter and a parser that selects the verifying key.
//! * [`JwkSet`] — a `keys` array (RFC 7517 §5): an authorization server
//! publishes this at `/.well-known/jwks.json`; a relying party parses it
//! and selects a key by `kid`.
//! * [`KeyRing`] — the rotation affordance: one active signing key plus a set
//! of still-valid verifying keys, so a server signs with the active key and
//! verifies against the whole set during a rollover window.
//!
//! # Design
//!
//! Like the rest of the crate, JSON is emitted by hand (the crate's
//! [`JsonValue`](crate::json::JsonValue) parses but does not serialise) using
//! the shared [`escape_json_string`](crate::json::escape_json_string)
//! helper — the same approach as `jwt::JwtEncoder` and `oauth::server`'s
//! introspection serializer. Parsing reuses [`JsonValue`]. Everything is
//! storage-free: the server owns persistence and rotation policy; this module
//! only models key *selection*.
//!
//! # Security
//!
//! A JWKS contains only **public** keys — no secret material is ever
//! serialised. [`KeyRing`] holds the private signing key in the
//! [`AsymmetricSigningKey`](crate::jwt::AsymmetricSigningKey)'s [`Zeroizing`](crate::crypto::zeroize::Zeroizing)
//! buffer; its [`fmt::Debug`] omits it.
use core::fmt;
use crate::encoding::base64url_encode;
use crate::json::{JsonValue, escape_json_string};
use super::asymmetric::{AsymmetricAlgorithm, AsymmetricKeyError, AsymmetricVerifyingKey};
use super::signature::JwtSignatureError;
use super::{JwtClaims, JwtHeader, verify_jwt_asymmetric};
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
/// The category of JWKS parse failure.
#[derive(Debug, Clone, PartialEq, Eq)]
enum JwkErrorKind {
/// The document is not valid JSON.
InvalidJson,
/// A required member (`kty`, `crv`, `x`, or `y` for EC) is missing.
MissingMember,
/// The `kty`/`crv` pair is not a supported key type.
UnsupportedKeyType,
/// A coordinate did not base64url-decode or was the wrong length / not on
/// the curve.
InvalidKeyMaterial,
/// Two keys in the set share the same `kid` (RFC 7517 §4.5 requires `kid`
/// values to be distinct).
DuplicateKid,
}
/// Error returned when parsing a JWK or JWK Set fails.
///
/// Error messages contain no key material.
#[doc(alias = "jwk_error")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JwkError {
kind: JwkErrorKind,
}
impl JwkError {
/// Returns `true` if the document was not valid JSON.
#[must_use]
#[inline]
pub fn is_invalid_json(&self) -> bool {
self.kind == JwkErrorKind::InvalidJson
}
/// Returns `true` if a required JWK member was missing.
#[must_use]
#[inline]
pub fn is_missing_member(&self) -> bool {
self.kind == JwkErrorKind::MissingMember
}
/// Returns `true` if the key type (`kty`/`crv`) is unsupported.
#[must_use]
#[inline]
pub fn is_unsupported_key_type(&self) -> bool {
self.kind == JwkErrorKind::UnsupportedKeyType
}
/// Returns `true` if a coordinate was malformed or off-curve.
#[must_use]
#[inline]
pub fn is_invalid_key_material(&self) -> bool {
self.kind == JwkErrorKind::InvalidKeyMaterial
}
/// Returns `true` if two keys in the set shared the same `kid`.
#[must_use]
#[inline]
pub fn is_duplicate_kid(&self) -> bool {
self.kind == JwkErrorKind::DuplicateKid
}
}
impl fmt::Display for JwkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
JwkErrorKind::InvalidJson => write!(f, "jwk: invalid JSON"),
JwkErrorKind::MissingMember => write!(f, "jwk: missing required member"),
JwkErrorKind::UnsupportedKeyType => write!(f, "jwk: unsupported key type"),
JwkErrorKind::InvalidKeyMaterial => write!(f, "jwk: invalid key material"),
JwkErrorKind::DuplicateKid => write!(f, "jwk: duplicate kid in set"),
}
}
}
impl std::error::Error for JwkError {}
impl From<AsymmetricKeyError> for JwkError {
fn from(_: AsymmetricKeyError) -> Self {
// The inner error never carries key material; collapse to the
// coordinate-level category.
Self {
kind: JwkErrorKind::InvalidKeyMaterial,
}
}
}
// ---------------------------------------------------------------------------
// Single JWK
// ---------------------------------------------------------------------------
/// A single public JSON Web Key (RFC 7517 §4).
///
/// Wraps an [`AsymmetricVerifyingKey`] and renders / parses its JWK JSON.
/// Carries `kty`, `crv`, `x` (and `y` for EC), `kid`, `alg`, and `use:"sig"`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Jwk {
key: AsymmetricVerifyingKey,
}
impl Jwk {
/// Wraps a verifying key as a JWK.
#[must_use]
#[inline]
pub fn new(key: AsymmetricVerifyingKey) -> Self {
Self { key }
}
/// Returns the wrapped verifying key.
#[must_use]
#[inline]
pub fn verifying_key(&self) -> &AsymmetricVerifyingKey {
&self.key
}
/// Returns the key identifier (`kid`).
#[must_use]
#[inline]
pub fn kid(&self) -> &str {
self.key.kid()
}
/// Serialises this key as a JWK JSON object (RFC 7517 §4).
///
/// Members are emitted in a fixed order: for `OKP` / `EC` keys `kty`,
/// `crv`, `x`, `y` (EC only); for `RSA` keys `kty`, `n`, `e`. Then
/// `use`, `alg`, `kid` for all. The base64url encoding is unpadded
/// (RFC 7518 §2 / §6.3.1 / RFC 8037 §2). Only public material is written.
#[must_use]
pub fn to_json(&self) -> String {
let alg = self.key.algorithm();
let mut s = String::with_capacity(512);
s.push('{');
let _ = write_member(&mut s, "kty", alg.key_type(), true);
if alg.is_rsa() {
// RSA JWK (RFC 7518 §6.3.1): modulus `n`, exponent `e`, no curve.
let _ = write_member(&mut s, "n", &base64url_encode(self.key.x_bytes()), false);
if let Some(e) = self.key.y_bytes() {
let _ = write_member(&mut s, "e", &base64url_encode(e), false);
}
} else {
if let Some(crv) = alg.curve() {
let _ = write_member(&mut s, "crv", crv, false);
}
let _ = write_member(&mut s, "x", &base64url_encode(self.key.x_bytes()), false);
if let Some(y) = self.key.y_bytes() {
let _ = write_member(&mut s, "y", &base64url_encode(y), false);
}
}
let _ = write_member(&mut s, "use", "sig", false);
let _ = write_member(&mut s, "alg", alg.as_str(), false);
let _ = write_member(&mut s, "kid", self.key.kid(), false);
s.push('}');
s
}
/// Parses a single JWK JSON object, returning its public verifying key.
///
/// Supports `kty:"OKP"`+`crv:"Ed25519"` (RFC 8037), `kty:"EC"`+
/// `crv:"P-256"` (RFC 7518), and `kty:"RSA"` (RFC 7518 §6.3.1, verify-only;
/// hash chosen by the optional `alg` member, default `RS256`). The `kid`
/// is read from the JWK; if absent it defaults to the RFC 7638 thumbprint.
///
/// # Errors
///
/// Returns [`JwkError`] for invalid JSON, a missing required member, an
/// unsupported key type, or key material that is not valid.
pub fn parse(json: &str) -> Result<Self, JwkError> {
let value = JsonValue::parse(json).map_err(|_| JwkError {
kind: JwkErrorKind::InvalidJson,
})?;
Self::from_value(&value)
}
/// Parses a JWK from an already-parsed [`JsonValue`] object (used by the
/// set parser to avoid re-parsing).
fn from_value(value: &JsonValue) -> Result<Self, JwkError> {
// SECURITY / RFC 7517 §4.2-4.3: honour `use` and `key_ops`. A key
// published as encryption-only (`"use":"enc"`) was loaded into the set
// and would happily verify signatures — the publisher's own statement
// of intended use was ignored. Reported as an unsupported key type so
// the set parser SKIPS it rather than failing the whole JWKS.
if value.get_str("use").is_some_and(|u| u != "sig") {
return Err(JwkError {
kind: JwkErrorKind::UnsupportedKeyType,
});
}
if let Some(JsonValue::Array(ops)) = value.get("key_ops") {
if !ops.iter().any(|o| o.as_str() == Some("verify")) {
return Err(JwkError {
kind: JwkErrorKind::UnsupportedKeyType,
});
}
}
let kty = value.get_str("kty").ok_or(missing())?;
// SECURITY / RFC 7517 §5: an unsupported `kty` (e.g. `oct`) is not an
// error here — the set parser skips it. Branch on `kty` first so a key
// with no `crv` (like RSA) is classified correctly rather than as
// "missing member".
match kty {
"RSA" => Self::from_rsa_value(value),
"OKP" | "EC" => Self::from_curve_value(value, kty),
_ => Err(JwkError {
kind: JwkErrorKind::UnsupportedKeyType,
}),
}
}
/// Parses an Ed25519 (`OKP`) or P-256 (`EC`) JWK from its coordinates.
fn from_curve_value(value: &JsonValue, kty: &str) -> Result<Self, JwkError> {
let unsupported = JwkError {
kind: JwkErrorKind::UnsupportedKeyType,
};
let crv = value.get_str("crv").ok_or(missing())?;
let alg = match (kty, crv) {
("OKP", "Ed25519") => AsymmetricAlgorithm::EdDsa,
("EC", "P-256") => AsymmetricAlgorithm::Es256,
_ => return Err(unsupported),
};
let x = decode_coordinate(value.get_str("x").ok_or(missing())?)?;
let y = match alg {
AsymmetricAlgorithm::Es256 => {
Some(decode_coordinate(value.get_str("y").ok_or(missing())?)?)
}
_ => None,
};
// Build the key once; the canonical kid is the RFC 7638 thumbprint,
// but honour an explicit `kid` if the JWK carries one (a server may
// number its keys). `with_kid` patches it in place — no second build.
let key = AsymmetricVerifyingKey::from_coordinates(alg, String::new(), &x, y.as_deref())?;
let kid = value
.get_str("kid")
.map_or_else(|| key.thumbprint(), ToOwned::to_owned);
Ok(Self {
key: key.with_kid(kid),
})
}
/// Parses an RSA JWK (RFC 7518 §6.3.1): modulus `n` and exponent `e`.
///
/// `alg` is OPTIONAL in a JWK. When the publisher omits it (Azure AD among
/// others), BOTH `RS256` and `RS512` variants are produced so the token
/// header selects the hash — unconditionally pinning to RS256 meant an
/// RS512-signed token from such a provider could never be verified.
/// Unsupported `alg` values (e.g. `PS256`) are reported as an unsupported
/// key type so the set parser skips them per RFC 7517 §5.
fn from_rsa_value(value: &JsonValue) -> Result<Self, JwkError> {
let alg = match value.get_str("alg") {
None | Some("RS256") => AsymmetricAlgorithm::Rs256,
Some("RS512") => AsymmetricAlgorithm::Rs512,
Some(_) => {
return Err(JwkError {
kind: JwkErrorKind::UnsupportedKeyType,
});
}
};
let n = decode_coordinate(value.get_str("n").ok_or(missing())?)?;
let e = decode_coordinate(value.get_str("e").ok_or(missing())?)?;
// Build once; patch the kid in place (thumbprint default, or an
// explicit `kid`) rather than reconstructing — RSA construction
// (BigUint parse + modulus-size check) is not cheap.
let key = AsymmetricVerifyingKey::from_rsa_components(alg, String::new(), &n, &e)?;
let kid = value
.get_str("kid")
.map_or_else(|| key.thumbprint(), ToOwned::to_owned);
Ok(Self {
key: key.with_kid(kid),
})
}
}
// ---------------------------------------------------------------------------
// JWK Set
// ---------------------------------------------------------------------------
/// A JSON Web Key Set: the `keys` array of an authorization server's
/// published JWKS document (RFC 7517 §5).
///
/// A server builds one from its active and rolling-out keys and serves
/// `to_json()` at `/.well-known/jwks.json`; a relying party parses one with
/// [`parse`](Self::parse) and selects a verifying key by `kid`.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct JwkSet {
keys: Vec<Jwk>,
}
impl JwkSet {
/// Creates an empty key set.
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Builds a key set from an iterator of verifying keys.
pub fn from_keys<I>(keys: I) -> Self
where
I: IntoIterator<Item = AsymmetricVerifyingKey>,
{
Self {
keys: keys.into_iter().map(Jwk::new).collect(),
}
}
/// Adds a key to the set.
pub fn push(&mut self, key: AsymmetricVerifyingKey) {
self.keys.push(Jwk::new(key));
}
/// Returns the JWKs in the set.
#[must_use]
#[inline]
pub fn keys(&self) -> &[Jwk] {
&self.keys
}
/// Returns the verifying key with the given `kid`, if present.
///
/// `kid`s within a JWKS are expected to be unique (RFC 7517 §4.5). If a
/// set contains duplicate `kid`s this returns the **first** match in
/// document order; a shadowed duplicate is never a forgery vector (the
/// selected key must still verify the signature), but operators supplying
/// an external JWKS should keep `kid`s unique to avoid surprising
/// selection.
#[must_use]
pub fn find(&self, kid: &str) -> Option<&AsymmetricVerifyingKey> {
self.keys
.iter()
.find(|k| k.kid() == kid)
.map(Jwk::verifying_key)
}
/// Serialises the set as the RFC 7517 §5 JWKS document
/// (`{"keys":[ … ]}`).
#[must_use]
pub fn to_json(&self) -> String {
let mut s = String::with_capacity(64 + self.keys.len() * 256);
s.push_str(r#"{"keys":["#);
for (i, k) in self.keys.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(&k.to_json());
}
s.push_str("]}");
s
}
/// Parses a JWKS document (RFC 7517 §5).
///
/// Unsupported keys (a `kty`/`crv`/`alg` this crate does not implement,
/// e.g. an `oct` symmetric key or a `PS256` RSA key) are **skipped**
/// rather than failing the whole document, matching RFC 7517 §5's
/// guidance that a consumer ignore keys it cannot use. A key of a
/// supported type with present-but-malformed material is an error.
///
/// # Errors
///
/// Returns [`JwkError`] if the document is not a JSON object with a
/// `keys` array, or if a key of a supported type has malformed
/// coordinates.
pub fn parse(json: &str) -> Result<Self, JwkError> {
let value = JsonValue::parse(json).map_err(|_| JwkError {
kind: JwkErrorKind::InvalidJson,
})?;
let keys = value
.get("keys")
.and_then(JsonValue::as_array)
.ok_or(JwkError {
kind: JwkErrorKind::MissingMember,
})?;
let mut out = Vec::with_capacity(keys.len());
for entry in keys {
match Jwk::from_value(entry) {
Ok(jwk) => out.push(jwk),
// RFC 7517 §5: ignore keys whose type we do not support.
Err(e) if e.is_unsupported_key_type() => {}
Err(e) => return Err(e),
}
}
// RFC 7517 §4.5: `kid` values must be distinct within a set. Reject a
// duplicate non-empty kid — first-match selection over a shadowed key
// could otherwise deny verification of the legitimate one (a merged or
// proxied multi-issuer document is the realistic source).
for (i, jwk) in out.iter().enumerate() {
let kid = jwk.kid();
if !kid.is_empty() && out[..i].iter().any(|k| k.kid() == kid) {
return Err(JwkError {
kind: JwkErrorKind::DuplicateKid,
});
}
}
Ok(Self { keys: out })
}
/// Verifies an asymmetric JWT against the key whose `kid` matches the
/// token header (RFC 7515 §4.1.4).
///
/// If the header carries no `kid` and the set holds exactly one key, that
/// key is used; otherwise a `kid` is required.
///
/// # Errors
///
/// Returns [`JwtSignatureError`] if the token is malformed, no matching
/// key is found, the algorithm does not match the selected key, or the
/// signature is invalid.
#[must_use = "verification may fail; handle the Result"]
pub fn verify(&self, token: &str) -> Result<(JwtHeader, JwtClaims), JwtSignatureError> {
let header = parse_header_only(token)?;
let key = match header.kid() {
Some(kid) => self.find(kid),
None if self.keys.len() == 1 => Some(self.keys[0].verifying_key()),
None => None,
}
.ok_or_else(JwtSignatureError::no_matching_key)?;
verify_jwt_asymmetric(token, key)
}
}
// ---------------------------------------------------------------------------
// Key ring (rotation)
// ---------------------------------------------------------------------------
/// A key-rotation set: one **active** signing key plus zero or more retired
/// keys still trusted for verification during a rollover window.
///
/// The model is intentionally minimal and storage-free — the crate does not
/// persist keys or decide *when* to rotate. A server:
///
/// 1. holds a [`KeyRing`] in memory, seeded from persisted keys;
/// 2. signs every new token with [`active`](Self::active) /
/// [`sign`](Self::sign);
/// 3. verifies inbound tokens with [`verify`](Self::verify), which selects by
/// `kid` across the active key and all retained verification keys;
/// 4. on rotation, calls [`rotate`](Self::rotate) to promote a fresh key to
/// active while demoting the previous active key into the verification
/// set, then drops keys past their grace window with
/// [`retain_verification_keys`](Self::retain_verification_keys);
/// 5. publishes [`jwks`](Self::jwks) so relying parties accept tokens signed
/// by any currently-trusted key.
///
/// # Security
///
/// The active key's private material lives in the
/// [`AsymmetricSigningKey`](crate::jwt::AsymmetricSigningKey)'s [`Zeroizing`](crate::crypto::zeroize::Zeroizing)
/// buffer; [`fmt::Debug`] for the ring omits it.
pub struct KeyRing {
active: super::AsymmetricSigningKey,
/// Retired-but-trusted verification keys, keyed by `kid` via linear scan
/// (a JWKS rarely holds more than a handful of keys).
verification: Vec<AsymmetricVerifyingKey>,
}
impl fmt::Debug for KeyRing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeyRing")
.field("active_kid", &self.active.kid())
.field("verification_kids", &self.verification_kids())
.finish_non_exhaustive()
}
}
impl KeyRing {
/// Creates a key ring with `active` as the sole signing and verification
/// key.
#[must_use]
pub fn new(active: super::AsymmetricSigningKey) -> Self {
Self {
active,
verification: Vec::new(),
}
}
/// Returns the active signing key.
#[must_use]
#[inline]
pub fn active(&self) -> &super::AsymmetricSigningKey {
&self.active
}
/// Adds a retired key that should still be trusted for verification
/// (e.g. one loaded from storage at startup).
pub fn add_verification_key(&mut self, key: AsymmetricVerifyingKey) {
self.verification.push(key);
}
/// Promotes `new_active` to the active signing key, demoting the previous
/// active key into the verification set so tokens it already signed keep
/// validating through the rollover window.
pub fn rotate(&mut self, new_active: super::AsymmetricSigningKey) {
let previous = std::mem::replace(&mut self.active, new_active);
self.verification.push(previous.to_verifying_key());
}
/// Retains only the verification keys whose `kid` satisfies `keep`
/// (drop keys past their grace window). The active key is never removed.
pub fn retain_verification_keys<F>(&mut self, mut keep: F)
where
F: FnMut(&str) -> bool,
{
self.verification.retain(|k| keep(k.kid()));
}
/// Returns the `kid`s of the retired verification keys.
#[must_use]
pub fn verification_kids(&self) -> Vec<&str> {
self.verification
.iter()
.map(AsymmetricVerifyingKey::kid)
.collect()
}
/// Signs an encoder's claims with the active key (sets the `kid` header).
///
/// Convenience for `encoder.sign_asymmetric(ring.active())`.
#[must_use = "this returns the signed JWT string"]
pub fn sign(&self, encoder: &super::JwtEncoder) -> String {
encoder.sign_asymmetric(&self.active)
}
/// Verifies a token against the active key or any retained verification
/// key, selecting by the header `kid`.
///
/// # Errors
///
/// Returns [`JwtSignatureError`] if no key matches the token's `kid`, the
/// algorithm mismatches, or the signature is invalid.
#[must_use = "verification may fail; handle the Result"]
pub fn verify(&self, token: &str) -> Result<(JwtHeader, JwtClaims), JwtSignatureError> {
let header = parse_header_only(token)?;
let key = self
.select(header.kid())
.ok_or_else(JwtSignatureError::no_matching_key)?;
verify_jwt_asymmetric(token, key)
}
/// Builds the JWKS document for all currently-trusted public keys
/// (active first, then retained verification keys).
#[must_use]
pub fn jwks(&self) -> JwkSet {
let mut set = JwkSet::new();
set.push(self.active.to_verifying_key());
for k in &self.verification {
set.push(k.clone());
}
set
}
/// Selects the verifying key for `kid` (or the active key when `kid` is
/// `None` — useful for single-key rings).
fn select(&self, kid: Option<&str>) -> Option<&AsymmetricVerifyingKey> {
match kid {
Some(kid) => {
if self.active.kid() == kid {
Some(self.active.verifying_key())
} else {
self.verification.iter().find(|k| k.kid() == kid)
}
}
None => Some(self.active.verifying_key()),
}
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
#[inline]
fn missing() -> JwkError {
JwkError {
kind: JwkErrorKind::MissingMember,
}
}
/// Decodes an unpadded base64url JWK coordinate.
fn decode_coordinate(s: &str) -> Result<Vec<u8>, JwkError> {
crate::encoding::base64url_decode(s).map_err(|_| JwkError {
kind: JwkErrorKind::InvalidKeyMaterial,
})
}
/// Appends `"name":"value"` to a JSON object body, with a leading comma
/// unless `first`. Values are escaped.
fn write_member(out: &mut String, name: &str, value: &str, first: bool) -> fmt::Result {
use core::fmt::Write as _;
if !first {
out.push(',');
}
write!(out, r#""{name}":{}"#, escape_json_string(value))
}
/// Parses only the header of a compact JWT to read its `kid` / `alg` before
/// key selection, reusing [`JwtHeader::parse`]. Malformed structure maps to
/// [`JwtSignatureError`].
fn parse_header_only(token: &str) -> Result<JwtHeader, JwtSignatureError> {
let (header_b64, _payload_b64, _signature_b64) = super::signature::split_compact(token)?;
JwtHeader::parse(header_b64).map_err(JwtSignatureError::from)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::jwt::{AsymmetricSigningKey, JwtAlgorithm, JwtEncoder};
fn keypair(alg: AsymmetricAlgorithm) -> AsymmetricSigningKey {
AsymmetricSigningKey::generate(alg).unwrap()
}
// --- JWK JSON round-trip ---
#[test]
fn ed25519_jwk_round_trip() {
let key = keypair(AsymmetricAlgorithm::EdDsa);
let jwk = Jwk::new(key.to_verifying_key());
let json = jwk.to_json();
assert!(json.contains(r#""kty":"OKP""#));
assert!(json.contains(r#""crv":"Ed25519""#));
assert!(json.contains(r#""use":"sig""#));
assert!(json.contains(r#""alg":"EdDSA""#));
assert!(!json.contains(r#""y":"#), "OKP has no y: {json}");
let parsed = Jwk::parse(&json).unwrap();
assert_eq!(parsed.kid(), jwk.kid());
let sig = key.sign(b"abc");
assert!(parsed.verifying_key().verify(b"abc", &sig));
}
#[test]
fn es256_jwk_round_trip() {
let key = keypair(AsymmetricAlgorithm::Es256);
let jwk = Jwk::new(key.to_verifying_key());
let json = jwk.to_json();
assert!(json.contains(r#""kty":"EC""#));
assert!(json.contains(r#""crv":"P-256""#));
assert!(json.contains(r#""y":"#), "EC must carry y: {json}");
let parsed = Jwk::parse(&json).unwrap();
let sig = key.sign(b"abc");
assert!(parsed.verifying_key().verify(b"abc", &sig));
}
// --- RFC 8037 Appendix A.2 public-key JWK ---
#[test]
fn rfc8037_a2_public_jwk_parses() {
// RFC 8037 Appendix A.2 public key.
let json = r#"{"kty":"OKP","crv":"Ed25519",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}"#;
let jwk = Jwk::parse(json).unwrap();
assert_eq!(jwk.verifying_key().algorithm(), AsymmetricAlgorithm::EdDsa);
// The thumbprint from RFC 7638 / 8037 of this key is well-known.
assert_eq!(
jwk.verifying_key().thumbprint(),
"kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
);
}
// --- JWKS document ---
#[test]
fn jwks_document_round_trip() {
let ed = keypair(AsymmetricAlgorithm::EdDsa);
let ec = keypair(AsymmetricAlgorithm::Es256);
let set = JwkSet::from_keys([ed.to_verifying_key(), ec.to_verifying_key()]);
let json = set.to_json();
assert!(json.starts_with(r#"{"keys":[{"#));
let parsed = JwkSet::parse(&json).unwrap();
assert_eq!(parsed.keys().len(), 2);
assert!(parsed.find(ed.kid()).is_some());
assert!(parsed.find(ec.kid()).is_some());
assert!(parsed.find("nope").is_none());
}
#[test]
fn jwks_skips_unsupported_keys() {
// An `oct` symmetric key and a PS256 RSA key are unsupported and must
// be ignored, not fail the document.
let ed = keypair(AsymmetricAlgorithm::EdDsa);
let json = format!(
r#"{{"keys":[{{"kty":"oct","k":"c2VjcmV0","kid":"sym1"}},{{"kty":"RSA","alg":"PS256","n":"{RSA_N}","e":"AQAB","kid":"ps1"}},{}]}}"#,
Jwk::new(ed.to_verifying_key()).to_json(),
);
let parsed = JwkSet::parse(&json).unwrap();
assert_eq!(parsed.keys().len(), 1);
assert!(parsed.find(ed.kid()).is_some());
assert!(parsed.find("sym1").is_none());
assert!(parsed.find("ps1").is_none());
}
// --- RSA (RS256) JWK + JWKS verification ---
//
// Reuses the 2048-bit known-answer vector from `asymmetric.rs` (public
// modulus + an RS256 ID-token-shaped JWT it signed) to exercise the
// RSA path through `Jwk::parse` and `JwkSet::verify`.
const RSA_N: &str = "l12KvkYdWWq2IwpT4kSOh-eC0kIGQzD4AgRAQ2WZY6-RC0m5X3yolmLIwCzH4CJhq1vm7mhG76RgvXoC2VlP7B2nHlz8-wPhk33Re4ia-Z4J6E_aIFn56Y5t01tv2N52rcijgS1Drvkqo2VvO9HWjBdpKi7cpW-lwG0fPYWQ0ibv1IrALZV66Qkp6QMT_wPtgbEYoeocMkSb7URUQFuFqL4BnucW7s8rQ1bFsw7oZ-_uQx_3JN0d3FQgJC2PUe-k2A7U8srQjKI26y3rKkNOe8n7LMUVFEj1rEbSn_OxEiLfUrjIMIJHikWd1QWH8uIpULs8ExeezpOgaeNQOUvdOw";
const RS256_TOKEN: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJzYS10ZXN0LTEifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoia29uc29sZS1jbGllbnQiLCJleHAiOjk5OTk5OTk5OTksImlhdCI6MTcwMDAwMDAwMCwiZW1haWwiOiJ1c2VyQGVudHJvcHlzb2Z0d29ya3MuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJUZXN0IFVzZXIifQ.GSt8PH2tF-Yd-O9qLZGXgMgXX8NISLN3svmYC245mACNYnHCPm5ottQMsVgXl5ux3BbMrWG1LeX4hLES9Ip7djmDJBuSsmT6XMKGLuOre5GokgFCLCXFsAwz_F3GSSSOKcRRb13-nuBdPnG919SYbS0E0mvD0eSzbmWHdUci5br8QsZQWwJryf9u0RGx3ZdMer2BXT0Qj4bJI1D4s1CK4T7z7jxk1PHeXvZ7w0GOdutJ5VG2jfgPjqfq07RxyBvSRJ_j549t30pmVMite6vnqbvv9673wX_-pN3VLwqR1QXRgaeyZYN3LoUCS1bmBw5kaw6tVCBGPADUz6VRvyUgiA";
fn rsa_jwk_json() -> String {
// A JWKS entry as Google publishes it: kty/n/e/alg/kid.
format!(
r#"{{"kty":"RSA","alg":"RS256","use":"sig","kid":"rsa-test-1","n":"{RSA_N}","e":"AQAB"}}"#
)
}
#[test]
fn rsa_jwk_round_trip_and_honours_explicit_kid() {
let jwk = Jwk::parse(&rsa_jwk_json()).unwrap();
assert_eq!(jwk.kid(), "rsa-test-1");
assert_eq!(jwk.verifying_key().algorithm(), AsymmetricAlgorithm::Rs256);
// Re-emit and re-parse: kty/n/e survive, no curve/x/y leak in.
let json = jwk.to_json();
assert!(json.contains(r#""kty":"RSA""#));
assert!(json.contains(r#""n":"#));
assert!(json.contains(r#""e":"AQAB""#));
assert!(!json.contains(r#""crv""#), "RSA has no crv: {json}");
assert!(!json.contains(r#""x":"#), "RSA has no x: {json}");
let reparsed = Jwk::parse(&json).unwrap();
assert_eq!(reparsed.kid(), "rsa-test-1");
}
#[test]
fn jwks_verify_selects_rsa_by_kid() {
// A set holding the RSA key plus an unrelated Ed25519 key: the RS256
// token must select the RSA key by `kid` and verify.
let ed = keypair(AsymmetricAlgorithm::EdDsa);
let json = format!(
r#"{{"keys":[{},{}]}}"#,
rsa_jwk_json(),
Jwk::new(ed.to_verifying_key()).to_json(),
);
let set = JwkSet::parse(&json).unwrap();
assert_eq!(set.keys().len(), 2);
let (header, claims) = set.verify(RS256_TOKEN).unwrap();
assert_eq!(header.alg(), JwtAlgorithm::RS256);
assert_eq!(header.kid(), Some("rsa-test-1"));
assert_eq!(claims.sub(), Some("1234567890"));
assert_eq!(claims.iss(), Some("https://accounts.google.com"));
}
#[test]
fn jwks_rejects_rsa_token_with_unknown_kid() {
// The RSA key is present under a different kid; selection must fail.
let json = format!(
r#"{{"keys":[{{"kty":"RSA","alg":"RS256","kid":"other","n":"{RSA_N}","e":"AQAB"}}]}}"#,
);
let set = JwkSet::parse(&json).unwrap();
assert!(set.verify(RS256_TOKEN).is_err());
}
#[test]
fn jwks_missing_keys_array_errors() {
let err = JwkSet::parse(r#"{"foo":1}"#).unwrap_err();
assert!(err.is_missing_member());
}
#[test]
fn jwk_malformed_coordinate_errors() {
let err = Jwk::parse(r#"{"kty":"OKP","crv":"Ed25519","x":"!!!"}"#).unwrap_err();
assert!(err.is_invalid_key_material());
}
#[test]
fn jwks_parse_rejects_duplicate_kid() {
// RFC 7517 §4.5: two keys sharing a kid must be rejected, not
// silently shadowed by first-match selection.
let ed = keypair(AsymmetricAlgorithm::EdDsa);
let single = JwkSet::from_keys([ed.to_verifying_key()]).to_json();
let inner = single
.trim_start_matches(r#"{"keys":["#)
.trim_end_matches("]}");
let dup = format!(r#"{{"keys":[{inner},{inner}]}}"#);
let err = JwkSet::parse(&dup).unwrap_err();
assert!(err.is_duplicate_kid(), "expected DuplicateKid, got: {err}");
}
// --- JWKS verify-by-kid ---
#[test]
fn jwks_verify_selects_by_kid() {
let ed = keypair(AsymmetricAlgorithm::EdDsa);
let ec = keypair(AsymmetricAlgorithm::Es256);
let set = JwkSet::from_keys([ed.to_verifying_key(), ec.to_verifying_key()]);
let token = JwtEncoder::new().subject("u1").sign_asymmetric(&ed);
let (header, claims) = set.verify(&token).unwrap();
assert_eq!(header.kid(), Some(ed.kid()));
assert_eq!(claims.sub(), Some("u1"));
}
#[test]
fn jwks_verify_unknown_kid_fails() {
let ed = keypair(AsymmetricAlgorithm::EdDsa);
let other = keypair(AsymmetricAlgorithm::EdDsa);
let set = JwkSet::from_keys([other.to_verifying_key()]);
let token = JwtEncoder::new().sign_asymmetric(&ed);
assert!(set.verify(&token).is_err());
}
// --- Key ring rotation ---
#[test]
fn key_ring_signs_with_active_and_verifies() {
let ring = KeyRing::new(keypair(AsymmetricAlgorithm::EdDsa));
let token = ring.sign(&JwtEncoder::new().subject("u"));
let (_, claims) = ring.verify(&token).unwrap();
assert_eq!(claims.sub(), Some("u"));
}
#[test]
fn key_ring_rotation_keeps_old_tokens_valid() {
let k1 = keypair(AsymmetricAlgorithm::EdDsa);
let k1_kid = k1.kid().to_owned();
let mut ring = KeyRing::new(k1);
// Token signed under the original active key.
let old_token = ring.sign(&JwtEncoder::new().subject("old"));
// Rotate to a new active key.
let k2 = keypair(AsymmetricAlgorithm::Es256);
let k2_kid = k2.kid().to_owned();
ring.rotate(k2);
// New tokens use the new key; old tokens still verify.
let new_token = ring.sign(&JwtEncoder::new().subject("new"));
assert_eq!(ring.verify(&old_token).unwrap().1.sub(), Some("old"));
assert_eq!(ring.verify(&new_token).unwrap().1.sub(), Some("new"));
// JWKS publishes both keys.
let jwks = ring.jwks();
assert!(jwks.find(&k1_kid).is_some());
assert!(jwks.find(&k2_kid).is_some());
// Dropping the old key past its grace window invalidates old tokens.
ring.retain_verification_keys(|kid| kid != k1_kid);
assert!(ring.verify(&old_token).is_err());
assert!(ring.verify(&new_token).is_ok());
}
#[test]
fn key_ring_debug_omits_private_key() {
let ring = KeyRing::new(keypair(AsymmetricAlgorithm::EdDsa));
let dbg = format!("{ring:?}");
let raw = ring.active().private_bytes();
let hex = crate::encoding::hex_encode(&raw);
assert!(!dbg.contains(&hex));
}
#[test]
fn skips_keys_not_usable_for_verification() {
// RFC 7517 §4.2/§4.3: the publisher's stated intent must be honoured.
// An encryption-only key previously verified signatures happily.
let enc_only = r#"{"keys":[{"kty":"OKP","crv":"Ed25519","use":"enc",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","kid":"e1"}]}"#;
assert_eq!(JwkSet::parse(enc_only).unwrap().keys().len(), 0);
let no_verify = r#"{"keys":[{"kty":"OKP","crv":"Ed25519","key_ops":["encrypt"],
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","kid":"e2"}]}"#;
assert_eq!(JwkSet::parse(no_verify).unwrap().keys().len(), 0);
// An explicit signing key, and one with no `use` at all, both load.
let sig = r#"{"keys":[{"kty":"OKP","crv":"Ed25519","use":"sig",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","kid":"s1"},
{"kty":"OKP","crv":"Ed25519",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo","kid":"s2"}]}"#;
assert_eq!(JwkSet::parse(sig).unwrap().keys().len(), 2);
}
}