pub mod jwks;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation};
use serde_json::Value;
pub const DEFAULT_LEEWAY_SECS: u64 = 30;
pub const MAX_LEEWAY_SECS: u64 = 300;
pub const DEFAULT_MAX_TOKEN_BYTES: usize = 8_192;
pub fn validate_jwks_url(url: &str) -> Result<(), String> {
if url.starts_with("https://") {
Ok(())
} else {
Err(format!(
"must be HTTPS — keys fetched over plaintext are keys an on-path \
attacker chose (got '{url}')"
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RejectReason {
Missing,
Oversized,
Malformed,
AlgRejected,
UnknownKid,
BadSignature,
Expired,
NotYetValid,
IssuerMismatch,
AudienceMismatch,
MissingClaim,
KeysUnavailable,
}
impl RejectReason {
pub fn as_str(self) -> &'static str {
match self {
Self::Missing => "missing",
Self::Oversized => "oversized",
Self::Malformed => "malformed",
Self::AlgRejected => "alg_rejected",
Self::UnknownKid => "unknown_kid",
Self::BadSignature => "bad_signature",
Self::Expired => "expired",
Self::NotYetValid => "not_yet_valid",
Self::IssuerMismatch => "issuer_mismatch",
Self::AudienceMismatch => "audience_mismatch",
Self::MissingClaim => "missing_claim",
Self::KeysUnavailable => "keys_unavailable",
}
}
pub fn wire_description(self) -> Option<&'static str> {
matches!(self, Self::Expired).then_some("token expired")
}
}
pub struct StaticKey {
pub kid: Option<String>,
pub algorithm: Algorithm,
pub key: DecodingKey,
}
pub struct Verifier {
pub static_keys: Vec<StaticKey>,
pub jwks_url: Option<String>,
pub algorithms: Vec<Algorithm>,
pub issuer: Vec<String>,
pub audience: Vec<String>,
pub leeway_secs: u64,
pub require_exp: bool,
pub max_token_bytes: usize,
pub validations: std::sync::OnceLock<Vec<(Algorithm, Validation)>>,
}
impl Verifier {
pub async fn verify(&self, token: &str) -> Result<Value, RejectReason> {
if token.is_empty() {
return Err(RejectReason::Missing);
}
if token.len() > self.max_token_bytes {
return Err(RejectReason::Oversized);
}
let header = jsonwebtoken::decode_header(token).map_err(|_| RejectReason::Malformed)?;
if !self.algorithms.contains(&header.alg) {
return Err(RejectReason::AlgRejected);
}
let validation = self.validation(header.alg);
let kid = header.kid.as_deref();
let mut saw_candidate = false;
let mut last: Option<RejectReason> = None;
for exact_kid in [true, false] {
for candidate in self.static_keys.iter().filter(|k| {
k.algorithm == header.alg
&& if exact_kid {
kid.is_some() && k.kid.as_deref() == kid
} else {
k.kid.is_none()
}
}) {
saw_candidate = true;
match try_key(token, &candidate.key, validation) {
Ok(claims) => return Ok(claims),
Err(RejectReason::BadSignature) => last = Some(RejectReason::BadSignature),
Err(other) => return Err(other),
}
}
}
if let Some(url) = &self.jwks_url {
let keys = jwks::decoding_keys(url, kid, header.alg).await?;
for key in &keys {
saw_candidate = true;
match try_key(token, key, validation) {
Ok(claims) => return Ok(claims),
Err(RejectReason::BadSignature) => last = Some(RejectReason::BadSignature),
Err(other) => return Err(other),
}
}
}
Err(if saw_candidate {
last.unwrap_or(RejectReason::BadSignature)
} else {
RejectReason::UnknownKid
})
}
fn validation(&self, alg: Algorithm) -> &Validation {
let validations = self.validations.get_or_init(|| {
self.algorithms
.iter()
.map(|&a| (a, self.build_validation(a)))
.collect()
});
validations
.iter()
.find_map(|(a, v)| (*a == alg).then_some(v))
.expect("algorithm allowlist membership checked before key routing")
}
fn build_validation(&self, alg: Algorithm) -> Validation {
let mut v = Validation::new(alg);
v.leeway = self.leeway_secs;
v.validate_exp = true;
v.validate_nbf = true;
if self.require_exp {
v.set_required_spec_claims(&["exp"]);
} else {
v.required_spec_claims.clear();
}
if self.issuer.is_empty() {
v.iss = None;
} else {
v.set_issuer(&self.issuer);
}
if self.audience.is_empty() {
v.validate_aud = false;
} else {
v.set_audience(&self.audience);
}
v
}
}
fn try_key(token: &str, key: &DecodingKey, validation: &Validation) -> Result<Value, RejectReason> {
use jsonwebtoken::errors::ErrorKind;
match jsonwebtoken::decode::<Value>(token, key, validation) {
Ok(data) => Ok(data.claims),
Err(e) => Err(match e.kind() {
ErrorKind::ExpiredSignature => RejectReason::Expired,
ErrorKind::ImmatureSignature => RejectReason::NotYetValid,
ErrorKind::InvalidIssuer => RejectReason::IssuerMismatch,
ErrorKind::InvalidAudience => RejectReason::AudienceMismatch,
ErrorKind::MissingRequiredClaim(_) => RejectReason::MissingClaim,
ErrorKind::InvalidSignature => RejectReason::BadSignature,
_ => RejectReason::BadSignature,
}),
}
}
pub const ALGORITHM_NAMES: &[&str] = &[
"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256",
"ES384", "EdDSA",
];
pub fn parse_algorithm(name: &str) -> Result<Algorithm, String> {
Ok(match name {
"HS256" => Algorithm::HS256,
"HS384" => Algorithm::HS384,
"HS512" => Algorithm::HS512,
"RS256" => Algorithm::RS256,
"RS384" => Algorithm::RS384,
"RS512" => Algorithm::RS512,
"PS256" => Algorithm::PS256,
"PS384" => Algorithm::PS384,
"PS512" => Algorithm::PS512,
"ES256" => Algorithm::ES256,
"ES384" => Algorithm::ES384,
"EdDSA" => Algorithm::EdDSA,
other => {
return Err(format!(
"algorithm '{other}' is not supported — one of {}",
ALGORITHM_NAMES.join(", ")
));
}
})
}
pub fn secret_bytes(secret: &str, key_encoding: Option<&str>) -> Result<Vec<u8>, String> {
use crate::engine::operators::{Codec, decode_bytes};
match key_encoding {
None | Some("utf8") => Ok(secret.as_bytes().to_vec()),
Some("base64") => decode_bytes(Codec::Base64, secret)
.map_err(|e| format!("key does not decode as base64: {e}")),
Some("hex") => {
decode_bytes(Codec::Hex, secret).map_err(|e| format!("key does not decode as hex: {e}"))
}
Some(other) => Err(format!(
"key_encoding '{other}' is not supported — utf8 (default), base64, hex"
)),
}
}
fn hs_min_len(alg: Algorithm) -> usize {
match alg {
Algorithm::HS256 => 32,
Algorithm::HS384 => 48,
_ => 64,
}
}
pub fn decoding_key(
algorithm: Algorithm,
material: &str,
key_encoding: Option<&str>,
) -> Result<DecodingKey, String> {
use jsonwebtoken::AlgorithmFamily;
match algorithm.family() {
AlgorithmFamily::Hmac => {
let bytes = secret_bytes(material, key_encoding)?;
if bytes.len() < hs_min_len(algorithm) {
return Err(format!(
"HS secret is {} bytes; RFC 7518 requires at least the hash length \
({} bytes) — a shorter secret weakens the MAC",
bytes.len(),
hs_min_len(algorithm)
));
}
Ok(DecodingKey::from_secret(&bytes))
}
AlgorithmFamily::Rsa => DecodingKey::from_rsa_pem(material.as_bytes())
.map_err(|e| format!("not a usable RSA public key PEM: {e}")),
AlgorithmFamily::Ec => DecodingKey::from_ec_pem(material.as_bytes())
.map_err(|e| format!("not a usable EC public key PEM: {e}")),
AlgorithmFamily::Ed => DecodingKey::from_ed_pem(material.as_bytes())
.map_err(|e| format!("not a usable Ed25519 public key PEM: {e}")),
}
}
pub fn encoding_key(
algorithm: Algorithm,
material: &str,
key_encoding: Option<&str>,
) -> Result<EncodingKey, String> {
use jsonwebtoken::AlgorithmFamily;
match algorithm.family() {
AlgorithmFamily::Hmac => {
let bytes = secret_bytes(material, key_encoding)?;
if bytes.len() < hs_min_len(algorithm) {
return Err(format!(
"HS secret is {} bytes; RFC 7518 requires at least the hash length \
({} bytes)",
bytes.len(),
hs_min_len(algorithm)
));
}
Ok(EncodingKey::from_secret(&bytes))
}
AlgorithmFamily::Rsa => EncodingKey::from_rsa_pem(material.as_bytes())
.map_err(|e| format!("not a usable RSA private key PEM: {e}")),
AlgorithmFamily::Ec => EncodingKey::from_ec_pem(material.as_bytes())
.map_err(|e| format!("not a usable EC private key PEM: {e}")),
AlgorithmFamily::Ed => EncodingKey::from_ed_pem(material.as_bytes())
.map_err(|e| format!("not a usable Ed25519 private key PEM: {e}")),
}
}
pub fn sign(
algorithm: Algorithm,
key: &EncodingKey,
kid: Option<String>,
claims: &Value,
) -> Result<String, String> {
let mut header = jsonwebtoken::Header::new(algorithm);
header.kid = kid;
jsonwebtoken::encode(&header, claims, key).map_err(|e| format!("signing failed: {e}"))
}
#[cfg(test)]
pub mod testkeys {
use std::sync::LazyLock;
pub struct Keypair {
pub private: String,
pub public: String,
}
fn generate(alg: &'static rcgen::SignatureAlgorithm) -> Keypair {
let key = rcgen::KeyPair::generate_for(alg).expect("test keypair generation");
Keypair {
private: key.serialize_pem(),
public: key.public_key_pem(),
}
}
pub static RSA: LazyLock<Keypair> = LazyLock::new(|| generate(&rcgen::PKCS_RSA_SHA256));
pub static EC: LazyLock<Keypair> = LazyLock::new(|| generate(&rcgen::PKCS_ECDSA_P256_SHA256));
pub static ED: LazyLock<Keypair> = LazyLock::new(|| generate(&rcgen::PKCS_ED25519));
}