use core::fmt;
use crate::error::{Error, Result};
macro_rules! alg_enum {
(
$(#[$meta:meta])*
$name:ident {
$( $(#[$vmeta:meta])* $variant:ident => $s:literal, )*
}
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum $name {
$( $(#[$vmeta])* $variant, )*
Unknown(String),
}
impl $name {
pub const KNOWN: &'static [$name] = &[$($name::$variant),*];
pub fn as_str(&self) -> &str {
match self {
$( Self::$variant => $s, )*
Self::Unknown(s) => s,
}
}
fn lookup(name: &str) -> Option<Self> {
match name {
$( $s => Some(Self::$variant), )*
_ => None,
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
pub fn ensure_known(&self) -> Result<&Self> {
match self {
Self::Unknown(_) => Err(Error::UnsupportedAlgorithm),
_ => Ok(self),
}
}
}
impl From<&str> for $name {
fn from(name: &str) -> Self {
Self::lookup(name).unwrap_or_else(|| Self::Unknown(name.to_owned()))
}
}
impl From<String> for $name {
fn from(name: String) -> Self {
Self::lookup(&name).unwrap_or(Self::Unknown(name))
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
};
}
alg_enum! {
SigningAlg {
HS256 => "HS256",
HS384 => "HS384",
HS512 => "HS512",
RS256 => "RS256",
RS384 => "RS384",
RS512 => "RS512",
ES256 => "ES256",
ES384 => "ES384",
ES512 => "ES512",
ES256K => "ES256K",
PS256 => "PS256",
PS384 => "PS384",
PS512 => "PS512",
EdDSA => "EdDSA",
Ed25519 => "Ed25519",
None => "none",
}
}
alg_enum! {
KeyMgmtAlg {
RsaOaep256 => "RSA-OAEP-256",
A128KW => "A128KW",
A192KW => "A192KW",
A256KW => "A256KW",
Dir => "dir",
EcdhEs => "ECDH-ES",
EcdhEsA128KW => "ECDH-ES+A128KW",
EcdhEsA192KW => "ECDH-ES+A192KW",
EcdhEsA256KW => "ECDH-ES+A256KW",
A128GCMKW => "A128GCMKW",
A192GCMKW => "A192GCMKW",
A256GCMKW => "A256GCMKW",
Pbes2Hs256A128KW => "PBES2-HS256+A128KW",
Pbes2Hs384A192KW => "PBES2-HS384+A192KW",
Pbes2Hs512A256KW => "PBES2-HS512+A256KW",
}
}
alg_enum! {
ContentEncAlg {
A128CbcHs256 => "A128CBC-HS256",
A192CbcHs384 => "A192CBC-HS384",
A256CbcHs512 => "A256CBC-HS512",
A128GCM => "A128GCM",
A192GCM => "A192GCM",
A256GCM => "A256GCM",
}
}
#[cfg(test)]
mod tests {
use super::*;
const JWS_NAMES: &[&str] = &[
"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "ES256K",
"PS256", "PS384", "PS512", "EdDSA", "Ed25519", "none",
];
const JWE_ALG_NAMES: &[&str] = &[
"RSA-OAEP-256",
"A128KW",
"A192KW",
"A256KW",
"dir",
"ECDH-ES",
"ECDH-ES+A128KW",
"ECDH-ES+A192KW",
"ECDH-ES+A256KW",
"A128GCMKW",
"A192GCMKW",
"A256GCMKW",
"PBES2-HS256+A128KW",
"PBES2-HS384+A192KW",
"PBES2-HS512+A256KW",
];
const JWE_ENC_NAMES: &[&str] = &[
"A128CBC-HS256",
"A192CBC-HS384",
"A256CBC-HS512",
"A128GCM",
"A192GCM",
"A256GCM",
];
fn assert_names<T>(names: &[&str], known: &[T])
where
T: PartialEq + fmt::Debug + fmt::Display + for<'a> From<&'a str> + From<String>,
{
for name in names {
let parsed = T::from(*name);
assert!(known.contains(&parsed), "{name} must be a known variant");
assert_eq!(T::from((*name).to_owned()), parsed, "{name}");
assert_eq!(parsed.to_string(), *name);
}
let known_names: Vec<String> = known.iter().map(ToString::to_string).collect();
assert_eq!(known_names, names, "KNOWN must match the registry list");
}
#[test]
fn jws_alg_names_match_the_iana_registry() {
assert_names(JWS_NAMES, SigningAlg::KNOWN);
for alg in SigningAlg::KNOWN {
assert_eq!(SigningAlg::from(alg.as_str()), *alg);
}
}
#[test]
fn jwe_alg_names_match_the_iana_registry() {
assert_names(JWE_ALG_NAMES, KeyMgmtAlg::KNOWN);
for alg in KeyMgmtAlg::KNOWN {
assert_eq!(KeyMgmtAlg::from(alg.as_str()), *alg);
}
}
#[test]
fn jwe_enc_names_match_the_iana_registry() {
assert_names(JWE_ENC_NAMES, ContentEncAlg::KNOWN);
for enc in ContentEncAlg::KNOWN {
assert_eq!(ContentEncAlg::from(enc.as_str()), *enc);
}
}
#[test]
fn new_identifiers_have_their_variants() {
assert_eq!(SigningAlg::from("ES256K"), SigningAlg::ES256K);
assert_eq!(SigningAlg::from("Ed25519"), SigningAlg::Ed25519);
assert_eq!(SigningAlg::from("none"), SigningAlg::None);
assert_eq!(KeyMgmtAlg::from("A192GCMKW"), KeyMgmtAlg::A192GCMKW);
assert_eq!(
KeyMgmtAlg::from("PBES2-HS512+A256KW"),
KeyMgmtAlg::Pbes2Hs512A256KW
);
}
#[test]
fn rsa_oaep_sha1_is_unknown_and_unsupported() {
let alg = KeyMgmtAlg::from("RSA-OAEP");
assert_eq!(alg, KeyMgmtAlg::Unknown("RSA-OAEP".into()));
assert_eq!(alg.as_str(), "RSA-OAEP");
assert_eq!(alg.ensure_known(), Err(Error::UnsupportedAlgorithm));
}
#[test]
fn rsa1_5_is_unknown_and_unsupported() {
let alg = KeyMgmtAlg::from("RSA1_5");
assert_eq!(alg, KeyMgmtAlg::Unknown("RSA1_5".into()));
assert_eq!(alg.as_str(), "RSA1_5");
assert_eq!(alg.ensure_known(), Err(Error::UnsupportedAlgorithm));
}
const NOT_JWS: &[&str] = &[
"hs256",
"Hs256",
"NONE",
"None",
"eddsa",
"ED25519",
"Ed448",
"ML-DSA-44",
"ES256k",
" HS256",
"HS256 ",
"HS256\0",
"HS256",
"",
"RSA-OAEP-256",
"A128GCM",
];
const NOT_JWE_ALG: &[&str] = &[
"RSA-OAEP",
"RSA1_5",
"rsa-oaep-256",
"RSA-OAEP-384",
"RSA_1_5",
"Dir",
"DIR",
"HPKE-0",
"ECDH-ES+A128GCMKW",
"pbes2-hs256+a128kw",
"",
"HS256",
"none",
];
const NOT_JWE_ENC: &[&str] = &["a128gcm", "A128CBC", "A128CBC-HS512", "", "A256KW", "dir"];
fn assert_unknown_round_trips<T>(values: &[&str], unknown: fn(String) -> T)
where
T: PartialEq + fmt::Debug + fmt::Display + for<'a> From<&'a str> + From<String>,
{
for value in values {
let parsed = T::from(*value);
assert_eq!(parsed, unknown((*value).to_owned()), "{value:?}");
assert_eq!(T::from((*value).to_owned()), parsed, "{value:?}");
assert_eq!(parsed.to_string(), *value);
}
}
#[test]
fn unknown_jws_alg_round_trips_exactly() {
assert_unknown_round_trips(NOT_JWS, SigningAlg::Unknown);
}
#[test]
fn unknown_jwe_alg_round_trips_exactly() {
assert_unknown_round_trips(NOT_JWE_ALG, KeyMgmtAlg::Unknown);
}
#[test]
fn unknown_jwe_enc_round_trips_exactly() {
assert_unknown_round_trips(NOT_JWE_ENC, ContentEncAlg::Unknown);
}
#[test]
fn ensure_known_rejects_unknown_with_unsupported_algorithm() {
let marker = "pj-SECRET-marker-alg";
let cases = [
SigningAlg::from(marker).ensure_known().err(),
KeyMgmtAlg::from(marker).ensure_known().err(),
ContentEncAlg::from(marker).ensure_known().err(),
];
for err in cases {
let err = err.unwrap_or_else(|| unreachable!("unknown must fail"));
assert_eq!(err, Error::UnsupportedAlgorithm);
assert!(!format!("{err:?}").contains(marker));
assert!(!err.to_string().contains(marker));
}
}
#[test]
fn ensure_known_accepts_every_known_value() {
for alg in SigningAlg::KNOWN {
assert_eq!(alg.ensure_known().ok(), Some(alg));
assert!(alg.is_known());
}
for alg in KeyMgmtAlg::KNOWN {
assert_eq!(alg.ensure_known().ok(), Some(alg));
assert!(alg.is_known());
}
for enc in ContentEncAlg::KNOWN {
assert_eq!(enc.ensure_known().ok(), Some(enc));
assert!(enc.is_known());
}
assert!(!SigningAlg::from("HS1").is_known());
}
#[test]
fn non_canonical_unknown_fails_closed() {
let forged = SigningAlg::Unknown("HS256".into());
assert_ne!(forged, SigningAlg::HS256);
assert_eq!(forged.ensure_known(), Err(Error::UnsupportedAlgorithm));
}
}