use crate::error::KeyError;
use crate::generate::generate;
use crate::{Algorithm, Claims};
use base64::Engine;
use jsonwebtoken::{DecodingKey, EncodingKey, Header};
use p256::elliptic_curve::SecretKey;
use p256::elliptic_curve::pkcs8::EncodePrivateKey;
use rsa::BigUint;
use rsa::pkcs1::EncodeRsaPrivateKey;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::OnceLock;
use std::{collections::HashSet, fmt, path::Path as StdPath};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "camelCase")]
pub enum KeyOperation {
Sign,
Verify,
Decrypt,
Encrypt,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "kty")]
pub enum KeyMaterial {
EC {
#[serde(rename = "crv")]
curve: EllipticCurve,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
x: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
y: Vec<u8>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_base64url_optional",
deserialize_with = "deserialize_base64url_optional"
)]
d: Option<Vec<u8>>,
},
RSA {
#[serde(flatten)]
public: RsaPublicKey,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
private: Option<RsaPrivateKey>,
},
#[serde(rename = "oct")]
OCT {
#[serde(
rename = "k",
serialize_with = "serialize_base64url",
deserialize_with = "deserialize_base64url"
)]
secret: Vec<u8>,
},
OKP {
#[serde(rename = "crv")]
curve: EllipticCurve,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
x: Vec<u8>,
#[serde(
rename = "d",
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_base64url_optional",
deserialize_with = "deserialize_base64url_optional"
)]
d: Option<Vec<u8>>,
},
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub enum EllipticCurve {
#[serde(rename = "P-256")]
P256,
#[serde(rename = "P-384")]
P384,
#[serde(rename = "Ed25519")]
Ed25519,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct RsaPublicKey {
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub n: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub e: Vec<u8>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct RsaPrivateKey {
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub d: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub p: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub q: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub dp: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub dq: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub qi: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oth: Option<Vec<RsaAdditionalPrime>>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct RsaAdditionalPrime {
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub r: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub d: Vec<u8>,
#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
pub t: Vec<u8>,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(remote = "Self")]
#[non_exhaustive]
pub struct Jwk {
#[serde(rename = "alg")]
pub algorithm: Algorithm,
#[serde(rename = "key_ops", default = "sign_verify")]
pub operations: HashSet<KeyOperation>,
#[serde(flatten)]
pub material: KeyMaterial,
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<crate::KeyId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<crate::Scope>,
}
fn sign_verify() -> HashSet<KeyOperation> {
[KeyOperation::Sign, KeyOperation::Verify].into()
}
const MIN_OCT_SECRET_BYTES: usize = 32;
impl Jwk {
pub fn new(algorithm: Algorithm, material: KeyMaterial) -> Self {
Self {
algorithm,
operations: sign_verify(),
material,
kid: None,
scope: None,
}
}
pub fn import(self) -> crate::Result<Key> {
if let Some(scope) = &self.scope {
scope.validate()?;
}
if let KeyMaterial::OCT { secret } = &self.material
&& secret.len() < MIN_OCT_SECRET_BYTES
{
return Err(KeyError::SecretTooShort(MIN_OCT_SECRET_BYTES).into());
}
Ok(Key {
jwk: self,
decode: Default::default(),
encode: Default::default(),
})
}
}
impl<'de> Deserialize<'de> for Jwk {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let mut value = serde_json::Value::deserialize(deserializer)?;
if let Some(obj) = value.as_object_mut()
&& !obj.contains_key("kty")
{
obj.insert("kty".to_string(), serde_json::Value::String("oct".to_string()));
}
Self::deserialize(value).map_err(serde::de::Error::custom)
}
}
impl Serialize for Jwk {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Self::serialize(self, serializer)
}
}
#[derive(Clone)]
pub struct Key {
jwk: Jwk,
decode: OnceLock<DecodingKey>,
encode: OnceLock<EncodingKey>,
}
impl std::ops::Deref for Key {
type Target = Jwk;
fn deref(&self) -> &Self::Target {
&self.jwk
}
}
impl TryFrom<Jwk> for Key {
type Error = crate::Error;
fn try_from(jwk: Jwk) -> crate::Result<Self> {
jwk.import()
}
}
impl From<&Key> for Jwk {
fn from(key: &Key) -> Self {
key.export()
}
}
impl<'de> Deserialize<'de> for Key {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let jwk = <Jwk as Deserialize>::deserialize(deserializer)?;
Key::try_from(jwk).map_err(serde::de::Error::custom)
}
}
impl Serialize for Key {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Serialize::serialize(&Jwk::from(self), serializer)
}
}
impl fmt::Debug for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Key")
.field("algorithm", &self.algorithm)
.field("operations", &self.operations)
.field("kid", &self.kid)
.field("scope", &self.scope)
.finish()
}
}
impl Key {
pub fn export(&self) -> Jwk {
self.jwk.clone()
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> crate::Result<Self> {
let s = s.trim();
if s.starts_with('{') {
Ok(serde_json::from_str(s)?)
} else {
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s)?;
let json = String::from_utf8(decoded)?;
Ok(serde_json::from_str(&json)?)
}
}
pub fn from_file<P: AsRef<StdPath>>(path: P) -> crate::Result<Self> {
let contents = std::fs::read_to_string(&path)?;
Self::from_str(&contents)
}
#[cfg(feature = "tokio")]
pub async fn from_file_async<P: AsRef<StdPath>>(path: P) -> crate::Result<Self> {
let contents = tokio::fs::read_to_string(path).await?;
Self::from_str(&contents)
}
pub fn to_str(&self) -> crate::Result<String> {
let json = serde_json::to_string(self)?;
Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json.as_bytes()))
}
pub fn to_file<P: AsRef<StdPath>>(&self, path: P) -> crate::Result<()> {
let encoded = self.to_str()?;
std::fs::write(path, encoded)?;
Ok(())
}
pub fn to_public(&self) -> crate::Result<Self> {
if !self.operations.contains(&KeyOperation::Verify) {
return Err(KeyError::VerifyUnsupported.into());
}
let material = match self.material {
KeyMaterial::RSA { ref public, .. } => KeyMaterial::RSA {
public: public.clone(),
private: None,
},
KeyMaterial::EC {
ref x,
ref y,
ref curve,
..
} => KeyMaterial::EC {
x: x.clone(),
y: y.clone(),
curve: curve.clone(),
d: None,
},
KeyMaterial::OCT { .. } => return Err(KeyError::NoPublicKey.into()),
KeyMaterial::OKP { ref x, ref curve, .. } => KeyMaterial::OKP {
x: x.clone(),
curve: curve.clone(),
d: None,
},
};
Ok(Self {
jwk: Jwk {
algorithm: self.algorithm,
operations: [KeyOperation::Verify].into(),
material,
kid: self.kid.clone(),
scope: self.scope.clone(),
},
decode: Default::default(),
encode: Default::default(),
})
}
pub(crate) fn has_signing_material(&self) -> bool {
match &self.material {
KeyMaterial::OCT { .. } => true,
KeyMaterial::EC { d, .. } => d.is_some(),
KeyMaterial::OKP { d, .. } => d.is_some(),
KeyMaterial::RSA { private, .. } => private.is_some(),
}
}
fn to_decoding_key(&self) -> crate::Result<&DecodingKey> {
if let Some(key) = self.decode.get() {
return Ok(key);
}
let decoding_key = match self.material {
KeyMaterial::OCT { ref secret } => match self.algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => DecodingKey::from_secret(secret),
_ => return Err(KeyError::InvalidAlgorithm.into()),
},
KeyMaterial::EC {
ref curve,
ref x,
ref y,
..
} => match curve {
EllipticCurve::P256 => {
if self.algorithm != Algorithm::ES256 {
return Err(KeyError::InvalidAlgorithmForCurve("P-256").into());
}
if x.len() != 32 || y.len() != 32 {
return Err(KeyError::InvalidCoordinateLength("P-256").into());
}
DecodingKey::from_ec_components(
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x).as_ref(),
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y).as_ref(),
)?
}
EllipticCurve::P384 => {
if self.algorithm != Algorithm::ES384 {
return Err(KeyError::InvalidAlgorithmForCurve("P-384").into());
}
if x.len() != 48 || y.len() != 48 {
return Err(KeyError::InvalidCoordinateLength("P-384").into());
}
DecodingKey::from_ec_components(
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x).as_ref(),
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y).as_ref(),
)?
}
_ => return Err(KeyError::InvalidCurve("EC").into()),
},
KeyMaterial::OKP { ref curve, ref x, .. } => match curve {
EllipticCurve::Ed25519 => {
if self.algorithm != Algorithm::EdDSA {
return Err(KeyError::InvalidAlgorithmForCurve("Ed25519").into());
}
DecodingKey::from_ed_components(
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x).as_ref(),
)?
}
_ => return Err(KeyError::InvalidCurve("OKP").into()),
},
KeyMaterial::RSA { ref public, .. } => {
DecodingKey::from_rsa_raw_components(public.n.as_ref(), public.e.as_ref())
}
};
Ok(self.decode.get_or_init(|| decoding_key))
}
fn to_encoding_key(&self) -> crate::Result<&EncodingKey> {
if let Some(key) = self.encode.get() {
return Ok(key);
}
let encoding_key = match self.material {
KeyMaterial::OCT { ref secret } => match self.algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => EncodingKey::from_secret(secret),
_ => return Err(KeyError::InvalidAlgorithm.into()),
},
KeyMaterial::EC { ref curve, ref d, .. } => {
let d = d.as_ref().ok_or(KeyError::MissingPrivateKey)?;
match curve {
EllipticCurve::P256 => {
let secret_key = SecretKey::<p256::NistP256>::from_slice(d)?;
let doc = secret_key.to_pkcs8_der()?;
EncodingKey::from_ec_der(doc.as_bytes())
}
EllipticCurve::P384 => {
let secret_key = SecretKey::<p384::NistP384>::from_slice(d)?;
let doc = secret_key.to_pkcs8_der()?;
EncodingKey::from_ec_der(doc.as_bytes())
}
_ => return Err(KeyError::InvalidCurve("EC").into()),
}
}
KeyMaterial::OKP {
ref curve,
ref d,
ref x,
} => {
let d = d.as_ref().ok_or(KeyError::MissingPrivateKey)?;
let key_pair =
aws_lc_rs::signature::Ed25519KeyPair::from_seed_and_public_key(d.as_slice(), x.as_slice())?;
match curve {
EllipticCurve::Ed25519 => EncodingKey::from_ed_der(key_pair.to_pkcs8()?.as_ref()),
_ => return Err(KeyError::InvalidCurve("OKP").into()),
}
}
KeyMaterial::RSA {
ref public,
ref private,
} => {
let n = BigUint::from_bytes_be(&public.n);
let e = BigUint::from_bytes_be(&public.e);
let private = private.as_ref().ok_or(KeyError::MissingPrivateKey)?;
let d = BigUint::from_bytes_be(&private.d);
let p = BigUint::from_bytes_be(&private.p);
let q = BigUint::from_bytes_be(&private.q);
let rsa = rsa::RsaPrivateKey::from_components(n, e, d, vec![p, q]);
let pem = rsa?.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF);
EncodingKey::from_rsa_pem(pem?.as_bytes())?
}
};
Ok(self.encode.get_or_init(|| encoding_key))
}
pub fn verify(&self, token: &str) -> crate::Result<Claims> {
if !self.operations.contains(&KeyOperation::Verify) {
return Err(KeyError::VerifyUnsupported.into());
}
let decode = self.to_decoding_key()?;
let mut validation = jsonwebtoken::Validation::new(self.algorithm.into());
validation.required_spec_claims = Default::default(); validation.validate_exp = false;
let token = jsonwebtoken::decode::<Claims>(token, decode, &validation)?;
if let Some(exp) = token.claims.expires
&& exp < std::time::SystemTime::now()
{
return Err(crate::Error::TokenExpired);
}
token.claims.validate()?;
self.validate_scope(&token.claims)?;
Ok(token.claims)
}
pub fn sign(&self, payload: &Claims) -> crate::Result<String> {
if !self.operations.contains(&KeyOperation::Sign) {
return Err(KeyError::SignUnsupported.into());
}
payload.validate()?;
self.validate_scope(payload)?;
let encode = self.to_encoding_key()?;
let mut header = Header::new(self.algorithm.into());
header.kid = self.kid.as_ref().map(|k| k.to_string());
let token = jsonwebtoken::encode(&header, &payload, encode)?;
Ok(token)
}
#[doc(hidden)]
#[deprecated(note = "renamed to Key::sign")]
pub fn encode(&self, payload: &Claims) -> crate::Result<String> {
self.sign(payload)
}
#[doc(hidden)]
#[deprecated(note = "renamed to Key::verify")]
pub fn decode(&self, token: &str) -> crate::Result<Claims> {
self.verify(token)
}
pub fn generate(algorithm: Algorithm, id: Option<crate::KeyId>) -> crate::Result<Self> {
generate(algorithm, id)
}
pub fn with_scope(mut self, scope: crate::Scope) -> crate::Result<Self> {
scope.validate()?;
self.jwk.scope = Some(scope);
Ok(self)
}
pub fn with_operations(mut self, operations: impl IntoIterator<Item = KeyOperation>) -> Self {
self.jwk.operations = operations.into_iter().collect();
self
}
fn validate_scope(&self, claims: &Claims) -> crate::Result<()> {
if let Some(scope) = &self.scope {
scope.validate()?;
if !scope.allows(claims) {
return Err(crate::Error::ScopeExceeded);
}
}
Ok(())
}
}
fn serialize_base64url<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
serializer.serialize_str(&encoded)
}
fn serialize_base64url_optional<S>(bytes: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match bytes {
Some(b) => serialize_base64url(b, serializer),
None => serializer.serialize_none(),
}
}
fn deserialize_base64url<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&s)
.or_else(|_| {
base64::engine::general_purpose::URL_SAFE.decode(&s)
})
.map_err(serde::de::Error::custom)
}
fn deserialize_base64url_optional<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(s) => {
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&s)
.or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(&s))
.map_err(serde::de::Error::custom)?;
Ok(Some(decoded))
}
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, SystemTime};
fn create_test_key() -> Key {
let mut jwk = Jwk::new(
Algorithm::HS256,
KeyMaterial::OCT {
secret: b"test-secret-that-is-long-enough-for-hmac-sha256".to_vec(),
},
);
jwk.kid = Some(crate::KeyId::decode("test-key-1").unwrap());
jwk.import().unwrap()
}
fn create_test_claims() -> Claims {
Claims {
root: "test-path".to_string(),
publish: vec!["test-pub".into()],
subscribe: vec!["test-sub".into()],
expires: Some(SystemTime::now() + Duration::from_secs(3600)),
issued: Some(SystemTime::now()),
}
}
#[test]
fn test_key_from_str_valid() {
let key = create_test_key();
let json = key.to_str().unwrap();
let loaded_key = Key::from_str(&json).unwrap();
assert_eq!(loaded_key.algorithm, key.algorithm);
assert_eq!(loaded_key.operations, key.operations);
match (&loaded_key.material, &key.material) {
(KeyMaterial::OCT { secret: loaded_secret }, KeyMaterial::OCT { secret }) => {
assert_eq!(loaded_secret, secret);
}
_ => panic!("Expected OCT key"),
}
assert_eq!(loaded_key.kid, key.kid);
}
#[test]
fn test_key_oct_backwards_compatibility() {
let json = r#"{"alg":"HS256","key_ops":["sign","verify"],"k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68"}"#;
let key = Key::from_str(json);
assert!(key.is_ok());
let key = key.unwrap();
if let KeyMaterial::OCT { secret, .. } = &key.material {
let base64_key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(secret);
assert_eq!(base64_key, "Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68");
} else {
panic!("Expected OCT key");
}
let key_str = key.to_str().unwrap();
let loaded = Key::from_str(&key_str).unwrap();
assert_eq!(loaded.algorithm, Algorithm::HS256);
assert!(loaded.operations.contains(&KeyOperation::Sign));
assert!(loaded.operations.contains(&KeyOperation::Verify));
assert!(matches!(loaded.material, KeyMaterial::OCT { .. }));
}
#[test]
fn test_key_without_key_ops_defaults_sign_verify() {
let json = r#"{"kty":"oct","alg":"HS256","k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68","kid":"no-ops"}"#;
let key = Key::from_str(json).unwrap();
assert_eq!(key.operations, sign_verify());
let claims = create_test_claims();
let token = key.sign(&claims).unwrap();
let verified = key.verify(&token).unwrap();
assert_eq!(verified.root, claims.root);
}
#[test]
fn test_key_without_key_ops_round_trip() {
let json = r#"{"kty":"oct","alg":"HS256","k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68"}"#;
let key = Key::from_str(json).unwrap();
let serialized = serde_json::to_string(&key).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
let ops = parsed["key_ops"].as_array().unwrap();
assert_eq!(ops.len(), 2);
let reloaded = Key::from_str(&serialized).unwrap();
assert_eq!(reloaded.operations, sign_verify());
assert_eq!(reloaded.algorithm, key.algorithm);
}
#[test]
fn test_key_oct_secret_required_and_min_length() {
assert!(Key::from_str(r#"{"kty":"oct","alg":"HS256"}"#).is_err());
assert!(Key::from_str(r#"{"kty":"oct","alg":"HS256","k":""}"#).is_err());
assert!(Key::from_str(r#"{"kty":"oct","alg":"HS256","k":"AAAAAAAAAAAAAAAAAAAAAA"}"#).is_err());
let short = Jwk::new(Algorithm::HS256, KeyMaterial::OCT { secret: vec![0; 16] });
assert!(short.import().is_err());
let key =
Key::from_str(r#"{"kty":"oct","alg":"HS256","k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68"}"#).unwrap();
let KeyMaterial::OCT { ref secret } = key.material else {
panic!("Expected OCT key");
};
assert_eq!(secret.len(), 32);
}
#[test]
fn test_key_without_key_ops_to_public() {
let json = r#"{"kty":"OKP","alg":"EdDSA","crv":"Ed25519","x":"UiU9fT_SdBBpkFtJPRCY0gX1jK_Dr9syYLFuEz4QUM4","d":"lm-L_PV3ksuQ-KrFBgFMDJqAZC3_Z6Z5UC4ZQY5OoDQ","kid":"defaulted"}"#;
let key = Key::from_str(json).unwrap();
assert_eq!(key.operations, sign_verify());
let public = key.to_public().unwrap();
assert_eq!(public.operations, [KeyOperation::Verify].into());
}
#[test]
fn test_key_from_str_invalid_json() {
let result = Key::from_str("invalid json");
assert!(result.is_err());
}
#[test]
fn test_key_to_str() {
let key = create_test_key();
let encoded = key.to_str().unwrap();
assert!(!encoded.contains('{'));
let loaded = Key::from_str(&encoded).unwrap();
assert_eq!(loaded.algorithm, Algorithm::HS256);
assert_eq!(loaded.kid, key.kid);
assert!(loaded.operations.contains(&KeyOperation::Sign));
assert!(loaded.operations.contains(&KeyOperation::Verify));
}
#[test]
fn test_key_sign_success() {
let key = create_test_key();
let claims = create_test_claims();
let token = key.sign(&claims).unwrap();
assert!(!token.is_empty());
assert_eq!(token.matches('.').count(), 2); }
#[test]
fn test_key_scope_enforced_when_signing_and_verifying() {
let unrestricted = create_test_key();
let scoped = unrestricted
.clone()
.with_scope(crate::Scope {
root: "test-path".into(),
publish: vec!["allowed".into()],
subscribe: vec![],
})
.unwrap();
let allowed = Claims {
root: "test-path".into(),
publish: vec!["allowed/room".into()],
..Default::default()
};
let denied = Claims {
root: "test-path".into(),
publish: vec!["other".into()],
..Default::default()
};
assert!(scoped.sign(&allowed).is_ok());
assert!(matches!(scoped.sign(&denied), Err(crate::Error::ScopeExceeded)));
let forged = unrestricted.sign(&denied).unwrap();
assert!(matches!(scoped.verify(&forged), Err(crate::Error::ScopeExceeded)));
}
#[test]
fn test_key_derived_material_never_stale() {
let claims = Claims {
root: "test-path".into(),
publish: vec!["test-pub".into()],
..Default::default()
};
let key = create_test_key();
let token = key.sign(&claims).unwrap();
assert!(key.encode.get().is_some());
let mut jwk = Jwk::from(&key);
jwk.algorithm = Algorithm::HS384;
let derived = Key::try_from(jwk).unwrap();
assert!(derived.encode.get().is_none());
let derived_token = derived.sign(&claims).unwrap();
assert_ne!(token, derived_token);
let cold = Key::from_str(&derived.to_str().unwrap()).unwrap();
assert_eq!(derived_token, cold.sign(&claims).unwrap());
assert!(cold.verify(&derived_token).is_ok());
assert!(key.verify(&derived_token).is_err());
}
#[test]
fn test_key_scope_requires_validation() {
let key = create_test_key();
assert!(key.scope.is_none());
let useless = crate::Scope::default();
assert!(matches!(
key.clone().with_scope(useless.clone()),
Err(crate::Error::UselessScope)
));
let mut jwk = Jwk::from(&key);
jwk.scope = Some(useless);
assert!(matches!(Key::try_from(jwk), Err(crate::Error::UselessScope)));
let json = r#"{"alg":"HS256","key_ops":["sign"],"k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68","scope":{}}"#;
assert!(Key::from_str(json).is_err());
}
#[test]
fn test_key_sign_no_permission() {
let key = create_test_key().with_operations([KeyOperation::Verify]);
let claims = create_test_claims();
let result = key.sign(&claims);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("key does not support signing"));
}
#[test]
fn test_key_sign_invalid_claims() {
let key = create_test_key();
let invalid_claims = Claims {
root: "test-path".to_string(),
publish: vec![],
subscribe: vec![],
expires: None,
issued: None,
};
let result = key.sign(&invalid_claims);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("no publish or subscribe allowed; token is useless")
);
}
#[test]
fn test_key_verify_success() {
let key = create_test_key();
let claims = create_test_claims();
let token = key.sign(&claims).unwrap();
let verified_claims = key.verify(&token).unwrap();
assert_eq!(verified_claims.root, claims.root);
assert_eq!(verified_claims.publish, claims.publish);
assert_eq!(verified_claims.subscribe, claims.subscribe);
}
#[test]
fn test_key_verify_no_permission() {
let key = create_test_key().with_operations([KeyOperation::Sign]);
let result = key.verify("some.jwt.token");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("key does not support verification")
);
}
#[test]
fn test_key_verify_invalid_token() {
let key = create_test_key();
let result = key.verify("invalid-token");
assert!(result.is_err());
}
#[test]
fn test_key_verify_path_mismatch() {
let key = create_test_key();
let claims = create_test_claims();
let token = key.sign(&claims).unwrap();
let result = key.verify(&token);
assert!(result.is_ok());
}
#[test]
fn test_key_verify_expired_token() {
let key = create_test_key();
let mut claims = create_test_claims();
claims.expires = Some(SystemTime::now() - Duration::from_secs(3600)); let token = key.sign(&claims).unwrap();
let result = key.verify(&token);
assert!(result.is_err());
}
#[test]
fn test_key_verify_token_without_exp() {
let key = create_test_key();
let claims = Claims {
root: "test-path".to_string(),
publish: vec!["".to_string()],
subscribe: vec!["".to_string()],
expires: None,
issued: None,
};
let token = key.sign(&claims).unwrap();
let verified_claims = key.verify(&token).unwrap();
assert_eq!(verified_claims.root, claims.root);
assert_eq!(verified_claims.publish, claims.publish);
assert_eq!(verified_claims.subscribe, claims.subscribe);
assert_eq!(verified_claims.expires, None);
}
#[test]
fn test_key_round_trip() {
let key = create_test_key();
let original_claims = Claims {
root: "test-path".to_string(),
publish: vec!["test-pub".into()],
subscribe: vec!["test-sub".into()],
expires: Some(SystemTime::now() + Duration::from_secs(3600)),
issued: Some(SystemTime::now()),
};
let token = key.sign(&original_claims).unwrap();
let verified_claims = key.verify(&token).unwrap();
assert_eq!(verified_claims.root, original_claims.root);
assert_eq!(verified_claims.publish, original_claims.publish);
assert_eq!(verified_claims.subscribe, original_claims.subscribe);
}
#[test]
fn test_key_generate_hs256() {
let key = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::HS256);
assert_eq!(key.kid, Some(crate::KeyId::decode("test-id").unwrap()));
assert_eq!(key.operations, [KeyOperation::Sign, KeyOperation::Verify].into());
match &key.material {
KeyMaterial::OCT { secret } => assert_eq!(secret.len(), 32),
_ => panic!("Expected OCT key"),
}
}
#[test]
fn test_key_generate_hs384() {
let key = Key::generate(Algorithm::HS384, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::HS384);
match &key.material {
KeyMaterial::OCT { secret } => assert_eq!(secret.len(), 48),
_ => panic!("Expected OCT key"),
}
}
#[test]
fn test_key_generate_hs512() {
let key = Key::generate(Algorithm::HS512, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::HS512);
match &key.material {
KeyMaterial::OCT { secret } => assert_eq!(secret.len(), 64),
_ => panic!("Expected OCT key"),
}
}
#[test]
fn test_key_generate_rs512() {
let key = Key::generate(Algorithm::RS512, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::RS512);
assert!(matches!(key.material, KeyMaterial::RSA { .. }));
match &key.material {
KeyMaterial::RSA { public, private } => {
assert!(private.is_some());
assert_eq!(public.n.len(), 256);
assert_eq!(public.e.len(), 3);
}
_ => panic!("Expected RSA key"),
}
}
#[test]
fn test_key_generate_es256() {
let key = Key::generate(Algorithm::ES256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::ES256);
assert!(matches!(key.material, KeyMaterial::EC { .. }))
}
#[test]
fn test_key_generate_ps512() {
let key = Key::generate(Algorithm::PS512, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::PS512);
assert!(matches!(key.material, KeyMaterial::RSA { .. }));
}
#[test]
fn test_key_generate_eddsa() {
let key = Key::generate(Algorithm::EdDSA, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::EdDSA);
assert!(matches!(key.material, KeyMaterial::OKP { .. }));
}
#[test]
fn test_key_generate_without_id() {
let key = Key::generate(Algorithm::HS256, None);
assert!(key.is_ok());
let key = key.unwrap();
assert_eq!(key.algorithm, Algorithm::HS256);
assert_eq!(key.kid, None);
assert_eq!(key.operations, [KeyOperation::Sign, KeyOperation::Verify].into());
}
#[test]
fn test_public_key_conversion_hmac() {
let key = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()))
.expect("HMAC key generation failed");
assert!(key.to_public().is_err());
}
#[test]
fn test_public_key_conversion_rsa() {
let key = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
let public_key = key.to_public().unwrap();
assert_eq!(key.kid, public_key.kid);
assert_eq!(public_key.operations, [KeyOperation::Verify].into());
assert!(public_key.encode.get().is_none());
assert!(public_key.decode.get().is_none());
assert!(matches!(public_key.material, KeyMaterial::RSA { .. }));
if let KeyMaterial::RSA { public, private } = &public_key.material {
assert!(private.is_none());
if let KeyMaterial::RSA { public: src_public, .. } = &key.material {
assert_eq!(public.e, src_public.e);
assert_eq!(public.n, src_public.n);
} else {
unreachable!("Expected RSA key")
}
} else {
unreachable!("Expected RSA key");
}
}
#[test]
fn test_public_key_conversion_es() {
let key = Key::generate(Algorithm::ES256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
let public_key = key.to_public().unwrap();
assert_eq!(key.kid, public_key.kid);
assert_eq!(public_key.operations, [KeyOperation::Verify].into());
assert!(public_key.encode.get().is_none());
assert!(public_key.decode.get().is_none());
assert!(matches!(public_key.material, KeyMaterial::EC { .. }));
if let KeyMaterial::EC { x, y, d, curve } = &public_key.material {
assert!(d.is_none());
if let KeyMaterial::EC {
x: src_x,
y: src_y,
curve: src_curve,
..
} = &key.material
{
assert_eq!(x, src_x);
assert_eq!(y, src_y);
assert_eq!(curve, src_curve);
} else {
unreachable!("Expected EC key")
}
} else {
unreachable!("Expected EC key");
}
}
#[test]
fn test_public_key_conversion_ed() {
let key = Key::generate(Algorithm::EdDSA, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
let public_key = key.to_public().unwrap();
assert_eq!(key.kid, public_key.kid);
assert_eq!(public_key.operations, [KeyOperation::Verify].into());
assert!(public_key.encode.get().is_none());
assert!(public_key.decode.get().is_none());
assert!(matches!(public_key.material, KeyMaterial::OKP { .. }));
if let KeyMaterial::OKP { x, d, curve } = &public_key.material {
assert!(d.is_none());
if let KeyMaterial::OKP {
x: src_x,
curve: src_curve,
..
} = &key.material
{
assert_eq!(x, src_x);
assert_eq!(curve, src_curve);
} else {
unreachable!("Expected OKP key")
}
} else {
unreachable!("Expected OKP key");
}
}
#[test]
fn test_key_generate_sign_verify_cycle() {
let key = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
let claims = create_test_claims();
let token = key.sign(&claims).unwrap();
let verified_claims = key.verify(&token).unwrap();
assert_eq!(verified_claims.root, claims.root);
assert_eq!(verified_claims.publish, claims.publish);
assert_eq!(verified_claims.subscribe, claims.subscribe);
}
#[test]
fn test_key_debug_no_secret() {
let key = create_test_key();
let debug_str = format!("{key:?}");
assert!(debug_str.contains("algorithm: HS256"));
assert!(debug_str.contains("operations"));
assert!(debug_str.contains("kid: Some(KeyId(\"test-key-1\"))"));
assert!(!debug_str.contains("secret")); }
#[test]
fn test_key_operations_enum() {
let sign_op = KeyOperation::Sign;
let verify_op = KeyOperation::Verify;
let decrypt_op = KeyOperation::Decrypt;
let encrypt_op = KeyOperation::Encrypt;
assert_eq!(sign_op, KeyOperation::Sign);
assert_eq!(verify_op, KeyOperation::Verify);
assert_eq!(decrypt_op, KeyOperation::Decrypt);
assert_eq!(encrypt_op, KeyOperation::Encrypt);
assert_ne!(sign_op, verify_op);
assert_ne!(decrypt_op, encrypt_op);
}
#[test]
fn test_key_operations_serde() {
let operations = [KeyOperation::Sign, KeyOperation::Verify];
let json = serde_json::to_string(&operations).unwrap();
assert!(json.contains("\"sign\""));
assert!(json.contains("\"verify\""));
let deserialized: Vec<KeyOperation> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, operations);
}
#[test]
fn test_key_serde() {
let key = create_test_key();
let json = serde_json::to_string(&key).unwrap();
let deserialized: Key = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.algorithm, key.algorithm);
assert_eq!(deserialized.operations, key.operations);
assert_eq!(deserialized.kid, key.kid);
if let (
KeyMaterial::OCT {
secret: original_secret,
},
KeyMaterial::OCT {
secret: deserialized_secret,
},
) = (&key.material, &deserialized.material)
{
assert_eq!(deserialized_secret, original_secret);
} else {
panic!("Expected both keys to be OCT variant");
}
}
#[test]
fn test_key_clone() {
let key = create_test_key();
let cloned = key.clone();
assert_eq!(cloned.algorithm, key.algorithm);
assert_eq!(cloned.operations, key.operations);
assert_eq!(cloned.kid, key.kid);
if let (
KeyMaterial::OCT {
secret: original_secret,
},
KeyMaterial::OCT { secret: cloned_secret },
) = (&key.material, &cloned.material)
{
assert_eq!(cloned_secret, original_secret);
} else {
panic!("Expected both keys to be OCT variant");
}
}
#[test]
fn test_hmac_algorithms() {
let key_256 = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
let key_384 = Key::generate(Algorithm::HS384, Some(crate::KeyId::decode("test-id").unwrap()));
let key_512 = Key::generate(Algorithm::HS512, Some(crate::KeyId::decode("test-id").unwrap()));
let claims = create_test_claims();
for key in [key_256, key_384, key_512] {
assert!(key.is_ok());
let key = key.unwrap();
let token = key.sign(&claims).unwrap();
let verified_claims = key.verify(&token).unwrap();
assert_eq!(verified_claims.root, claims.root);
}
}
#[test]
fn test_rsa_pkcs1_asymmetric_algorithms() {
let key_rs256 = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
let key_rs384 = Key::generate(Algorithm::RS384, Some(crate::KeyId::decode("test-id").unwrap()));
let key_rs512 = Key::generate(Algorithm::RS512, Some(crate::KeyId::decode("test-id").unwrap()));
for key in [key_rs256, key_rs384, key_rs512] {
test_asymmetric_key(key);
}
}
#[test]
fn test_rsa_pss_asymmetric_algorithms() {
let key_ps256 = Key::generate(Algorithm::PS256, Some(crate::KeyId::decode("test-id").unwrap()));
let key_ps384 = Key::generate(Algorithm::PS384, Some(crate::KeyId::decode("test-id").unwrap()));
let key_ps512 = Key::generate(Algorithm::PS512, Some(crate::KeyId::decode("test-id").unwrap()));
for key in [key_ps256, key_ps384, key_ps512] {
test_asymmetric_key(key);
}
}
#[test]
fn test_ec_asymmetric_algorithms() {
let key_es256 = Key::generate(Algorithm::ES256, Some(crate::KeyId::decode("test-id").unwrap()));
let key_es384 = Key::generate(Algorithm::ES384, Some(crate::KeyId::decode("test-id").unwrap()));
for key in [key_es256, key_es384] {
test_asymmetric_key(key);
}
}
#[test]
fn test_ed_asymmetric_algorithms() {
let key_eddsa = Key::generate(Algorithm::EdDSA, Some(crate::KeyId::decode("test-id").unwrap()));
test_asymmetric_key(key_eddsa);
}
fn test_asymmetric_key(key: crate::Result<Key>) {
assert!(key.is_ok());
let key = key.unwrap();
let claims = create_test_claims();
let token = key.sign(&claims).unwrap();
let private_verified_claims = key.verify(&token).unwrap();
assert_eq!(
private_verified_claims.root, claims.root,
"validation using private key"
);
let public_verified_claims = key.to_public().unwrap().verify(&token).unwrap();
assert_eq!(public_verified_claims.root, claims.root, "validation using public key");
}
#[test]
fn test_cross_algorithm_verification_fails() {
let key_256 = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key_256.is_ok());
let key_256 = key_256.unwrap();
let key_384 = Key::generate(Algorithm::HS384, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key_384.is_ok());
let key_384 = key_384.unwrap();
let claims = create_test_claims();
let token = key_256.sign(&claims).unwrap();
let result = key_384.verify(&token);
assert!(result.is_err());
}
#[test]
fn test_asymmetric_cross_algorithm_verification_fails() {
let key_rs256 = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key_rs256.is_ok());
let key_rs256 = key_rs256.unwrap();
let key_ps256 = Key::generate(Algorithm::PS256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key_ps256.is_ok());
let key_ps256 = key_ps256.unwrap();
let claims = create_test_claims();
let token = key_rs256.sign(&claims).unwrap();
let private_result = key_ps256.verify(&token);
let public_result = key_ps256.to_public().unwrap().verify(&token);
assert!(private_result.is_err());
assert!(public_result.is_err());
}
#[test]
fn test_rsa_pkcs1_public_key_conversion() {
let key = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert!(key.operations.contains(&KeyOperation::Sign));
assert!(key.operations.contains(&KeyOperation::Verify));
let public_key = key.to_public().unwrap();
assert!(!public_key.operations.contains(&KeyOperation::Sign));
assert!(public_key.operations.contains(&KeyOperation::Verify));
match &key.material {
KeyMaterial::RSA { public, private } => {
assert!(private.is_some());
assert_eq!(public.n.len(), 256);
assert_eq!(public.e.len(), 3);
match &public_key.material {
KeyMaterial::RSA {
public: guest_public,
private: public_private,
} => {
assert!(public_private.is_none());
assert_eq!(public.n, guest_public.n);
assert_eq!(public.e, guest_public.e);
}
_ => panic!("Expected public key to be an RSA key"),
}
}
_ => panic!("Expected private key to be an RSA key"),
}
}
#[test]
fn test_rsa_pss_public_key_conversion() {
let key = Key::generate(Algorithm::PS384, Some(crate::KeyId::decode("test-id").unwrap()));
assert!(key.is_ok());
let key = key.unwrap();
assert!(key.operations.contains(&KeyOperation::Sign));
assert!(key.operations.contains(&KeyOperation::Verify));
let public_key = key.to_public().unwrap();
assert!(!public_key.operations.contains(&KeyOperation::Sign));
assert!(public_key.operations.contains(&KeyOperation::Verify));
match &key.material {
KeyMaterial::RSA { public, private } => {
assert!(private.is_some());
assert_eq!(public.n.len(), 256);
assert_eq!(public.e.len(), 3);
match &public_key.material {
KeyMaterial::RSA {
public: guest_public,
private: public_private,
} => {
assert!(public_private.is_none());
assert_eq!(public.n, guest_public.n);
assert_eq!(public.e, guest_public.e);
}
_ => panic!("Expected public key to be an RSA key"),
}
}
_ => panic!("Expected private key to be an RSA key"),
}
}
#[test]
fn test_base64url_serialization() {
let key = create_test_key();
let json = serde_json::to_string(&key).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let k_value = parsed["k"].as_str().unwrap();
assert!(!k_value.contains('='));
assert!(!k_value.contains('+'));
assert!(!k_value.contains('/'));
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(k_value)
.unwrap();
if let KeyMaterial::OCT {
secret: original_secret,
} = &key.material
{
assert_eq!(decoded, *original_secret);
} else {
panic!("Expected both keys to be OCT variant");
}
}
#[test]
fn test_backwards_compatibility_unpadded_base64url() {
let unpadded_json = r#"{"kty":"oct","alg":"HS256","key_ops":["sign","verify"],"k":"dGVzdC1zZWNyZXQtdGhhdC1pcy1sb25nLWVub3VnaC1mb3ItaG1hYy1zaGEyNTY","kid":"test-key-1"}"#;
let key: Key = serde_json::from_str(unpadded_json).unwrap();
assert_eq!(key.algorithm, Algorithm::HS256);
assert_eq!(key.kid, Some(crate::KeyId::decode("test-key-1").unwrap()));
if let KeyMaterial::OCT { secret } = &key.material {
assert_eq!(secret, b"test-secret-that-is-long-enough-for-hmac-sha256");
} else {
panic!("Expected key to be OCT variant");
}
}
#[test]
fn test_backwards_compatibility_padded_base64url() {
let padded_json = r#"{"kty":"oct","alg":"HS256","key_ops":["sign","verify"],"k":"dGVzdC1zZWNyZXQtdGhhdC1pcy1sb25nLWVub3VnaC1mb3ItaG1hYy1zaGEyNTY=","kid":"test-key-1"}"#;
let key: Key = serde_json::from_str(padded_json).unwrap();
assert_eq!(key.algorithm, Algorithm::HS256);
assert_eq!(key.kid, Some(crate::KeyId::decode("test-key-1").unwrap()));
if let KeyMaterial::OCT { secret } = &key.material {
assert_eq!(secret, b"test-secret-that-is-long-enough-for-hmac-sha256");
} else {
panic!("Expected key to be OCT variant");
}
}
const JS_HS256_KEY: &str = r#"{"kty":"oct","alg":"HS256","k":"xm6xsSkfFqzPU3KfcbAcF2_h0OkStxQ_nNqVPYl0ync","kid":"js-test-key","key_ops":["sign","verify"],"guest":[],"guest_sub":[],"guest_pub":[]}"#;
const JS_HS256_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImpzLXRlc3Qta2V5In0.eyJyb290IjoibGl2ZSIsInB1dCI6WyJjYW1lcmExIl0sImdldCI6WyJjYW1lcmExIiwiY2FtZXJhMiJdLCJpYXQiOjE3NzUxNzY3NTR9.tHNQtHh_HCIKxXOexDCM7AkjqWzbULLZzjEckfOGRfY";
const JS_EDDSA_PRIVATE_KEY: &str = r#"{"kty":"OKP","alg":"EdDSA","crv":"Ed25519","x":"UiU9fT_SdBBpkFtJPRCY0gX1jK_Dr9syYLFuEz4QUM4","d":"lm-L_PV3ksuQ-KrFBgFMDJqAZC3_Z6Z5UC4ZQY5OoDQ","kid":"js-eddsa-key","key_ops":["sign","verify"],"guest":[],"guest_sub":[],"guest_pub":[]}"#;
const JS_EDDSA_PUBLIC_KEY: &str = r#"{"kty":"OKP","alg":"EdDSA","crv":"Ed25519","x":"UiU9fT_SdBBpkFtJPRCY0gX1jK_Dr9syYLFuEz4QUM4","kid":"js-eddsa-key","guest":[],"guest_sub":[],"guest_pub":[],"key_ops":["verify"]}"#;
const JS_EDDSA_TOKEN: &str = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImpzLWVkZHNhLWtleSJ9.eyJyb290Ijoic3RyZWFtIiwicHV0IjpbInZpZGVvIl0sImlhdCI6MTc3NTE3Njc1Nn0.l9rUMHjPSXWKSXRP3mmeMgTAywtqpdqQehhViWaPrKxax1Y2D9KRIYTixYz-b6PI-AoHQYusHWeeLu_HRw2cAg";
#[test]
fn test_js_hs256_key_load() {
let key = Key::from_str(JS_HS256_KEY).unwrap();
assert_eq!(key.algorithm, Algorithm::HS256);
assert_eq!(key.kid, Some(crate::KeyId::decode("js-test-key").unwrap()));
}
#[test]
fn test_js_hs256_token_verify() {
let key = Key::from_str(JS_HS256_KEY).unwrap();
let claims = key.verify(JS_HS256_TOKEN).unwrap();
assert_eq!(claims.root, "live");
assert_eq!(claims.publish, vec!["camera1"]);
assert_eq!(claims.subscribe, vec!["camera1", "camera2"]);
}
#[test]
fn test_js_hs256_sign_and_roundtrip() {
let key = Key::from_str(JS_HS256_KEY).unwrap();
let claims = Claims {
root: "rust-test".to_string(),
publish: vec!["pub1".into()],
subscribe: vec!["sub1".into()],
..Default::default()
};
let token = key.sign(&claims).unwrap();
let verified = key.verify(&token).unwrap();
assert_eq!(verified.root, "rust-test");
assert_eq!(verified.publish, vec!["pub1"]);
}
#[test]
fn test_js_eddsa_key_load() {
let private_key = Key::from_str(JS_EDDSA_PRIVATE_KEY).unwrap();
assert_eq!(private_key.algorithm, Algorithm::EdDSA);
assert!(matches!(private_key.material, KeyMaterial::OKP { .. }));
let public_key = Key::from_str(JS_EDDSA_PUBLIC_KEY).unwrap();
assert_eq!(public_key.algorithm, Algorithm::EdDSA);
}
#[test]
fn test_js_eddsa_token_verify_with_private_key() {
let key = Key::from_str(JS_EDDSA_PRIVATE_KEY).unwrap();
let claims = key.verify(JS_EDDSA_TOKEN).unwrap();
assert_eq!(claims.root, "stream");
assert_eq!(claims.publish, vec!["video"]);
}
#[test]
fn test_js_eddsa_token_verify_with_public_key() {
let key = Key::from_str(JS_EDDSA_PUBLIC_KEY).unwrap();
let claims = key.verify(JS_EDDSA_TOKEN).unwrap();
assert_eq!(claims.root, "stream");
assert_eq!(claims.publish, vec!["video"]);
}
#[test]
fn test_js_token_wrong_key_fails() {
let wrong_key = Key::generate(Algorithm::HS256, None).unwrap();
let result = wrong_key.verify(JS_HS256_TOKEN);
assert!(result.is_err());
}
#[test]
fn test_js_eddsa_token_wrong_key_fails() {
let wrong_key = Key::from_str(JS_HS256_KEY).unwrap();
let result = wrong_key.verify(JS_EDDSA_TOKEN);
assert!(result.is_err());
}
#[test]
fn test_file_io_base64url() {
let key = create_test_key();
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join("test_jwk.key");
key.to_file(&temp_path).unwrap();
let contents = std::fs::read_to_string(&temp_path).unwrap();
assert!(!contents.contains('{'));
assert!(!contents.contains('}'));
assert!(!contents.contains('"'));
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&contents)
.unwrap();
let json_str = String::from_utf8(decoded).unwrap();
let _: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let loaded_key = Key::from_file(&temp_path).unwrap();
assert_eq!(loaded_key.algorithm, key.algorithm);
assert_eq!(loaded_key.operations, key.operations);
assert_eq!(loaded_key.kid, key.kid);
if let (
KeyMaterial::OCT {
secret: original_secret,
},
KeyMaterial::OCT { secret: loaded_secret },
) = (&key.material, &loaded_key.material)
{
assert_eq!(loaded_secret, original_secret);
} else {
panic!("Expected both keys to be OCT variant");
}
std::fs::remove_file(temp_path).ok();
}
#[test]
fn test_file_io_raw_json() {
let key = create_test_key();
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join("test_jwk_raw_json.key");
let json = serde_json::to_string(&key).unwrap();
std::fs::write(&temp_path, &json).unwrap();
assert!(json.starts_with('{'));
let loaded_key = Key::from_file(&temp_path).unwrap();
assert_eq!(loaded_key.algorithm, key.algorithm);
assert_eq!(loaded_key.operations, key.operations);
assert_eq!(loaded_key.kid, key.kid);
if let (
KeyMaterial::OCT {
secret: original_secret,
},
KeyMaterial::OCT { secret: loaded_secret },
) = (&key.material, &loaded_key.material)
{
assert_eq!(loaded_secret, original_secret);
} else {
panic!("Expected both keys to be OCT variant");
}
std::fs::remove_file(temp_path).ok();
}
}