Skip to main content

moq_auth/
key.rs

1use crate::error::KeyError;
2use crate::generate::generate;
3use crate::{Algorithm, Claims};
4use base64::Engine;
5use jsonwebtoken::{DecodingKey, EncodingKey, Header};
6use p256::elliptic_curve::SecretKey;
7use p256::elliptic_curve::pkcs8::EncodePrivateKey;
8use rsa::BigUint;
9use rsa::pkcs1::EncodeRsaPrivateKey;
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use std::sync::OnceLock;
12use std::{collections::HashSet, fmt, path::Path as StdPath};
13
14/// Cryptographic operations that a key can perform.
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[serde(rename_all = "camelCase")]
17pub enum KeyOperation {
18	Sign,
19	Verify,
20	Decrypt,
21	Encrypt,
22}
23
24/// <https://datatracker.ietf.org/doc/html/rfc7518#section-6>
25#[derive(Clone, Serialize, Deserialize)]
26#[serde(tag = "kty")]
27pub enum KeyMaterial {
28	/// <https://datatracker.ietf.org/doc/html/rfc7518#section-6.2>
29	EC {
30		#[serde(rename = "crv")]
31		curve: EllipticCurve,
32		/// The X-coordinate of an EC key
33		#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
34		x: Vec<u8>,
35		/// The Y-coordinate of an EC key
36		#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
37		y: Vec<u8>,
38		/// The private value of an EC key
39		#[serde(
40			default,
41			skip_serializing_if = "Option::is_none",
42			serialize_with = "serialize_base64url_optional",
43			deserialize_with = "deserialize_base64url_optional"
44		)]
45		d: Option<Vec<u8>>,
46	},
47	/// <https://datatracker.ietf.org/doc/html/rfc7518#section-6.3>
48	RSA {
49		#[serde(flatten)]
50		public: RsaPublicKey,
51		#[serde(flatten, skip_serializing_if = "Option::is_none")]
52		private: Option<RsaPrivateKey>,
53	},
54	/// <https://datatracker.ietf.org/doc/html/rfc7518#section-6.4>
55	#[serde(rename = "oct")]
56	OCT {
57		/// The secret key as base64url (unpadded). Must be at least 32 bytes once decoded.
58		#[serde(
59			rename = "k",
60			serialize_with = "serialize_base64url",
61			deserialize_with = "deserialize_base64url"
62		)]
63		secret: Vec<u8>,
64	},
65	/// <https://datatracker.ietf.org/doc/html/rfc8037#section-2>
66	OKP {
67		#[serde(rename = "crv")]
68		curve: EllipticCurve,
69		#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
70		x: Vec<u8>,
71		#[serde(
72			rename = "d",
73			default,
74			skip_serializing_if = "Option::is_none",
75			serialize_with = "serialize_base64url_optional",
76			deserialize_with = "deserialize_base64url_optional"
77		)]
78		d: Option<Vec<u8>>,
79	},
80}
81
82/// Supported elliptic curves for EC and OKP key types.
83///
84/// See <https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1>
85#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
86pub enum EllipticCurve {
87	#[serde(rename = "P-256")]
88	P256,
89	#[serde(rename = "P-384")]
90	P384,
91	// jsonwebtoken doesn't support the ES512 algorithm, so we can't implement this
92	// #[serde(rename = "P-521")]
93	// P521,
94	#[serde(rename = "Ed25519")]
95	Ed25519,
96}
97
98/// RSA public key parameters.
99///
100/// See <https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1>
101#[derive(Clone, Serialize, Deserialize)]
102pub struct RsaPublicKey {
103	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
104	pub n: Vec<u8>,
105	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
106	pub e: Vec<u8>,
107}
108
109/// RSA private key parameters.
110///
111/// See <https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2>
112#[derive(Clone, Serialize, Deserialize)]
113pub struct RsaPrivateKey {
114	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
115	pub d: Vec<u8>,
116	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
117	pub p: Vec<u8>,
118	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
119	pub q: Vec<u8>,
120	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
121	pub dp: Vec<u8>,
122	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
123	pub dq: Vec<u8>,
124	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
125	pub qi: Vec<u8>,
126	#[serde(skip_serializing_if = "Option::is_none")]
127	pub oth: Option<Vec<RsaAdditionalPrime>>,
128}
129
130/// Additional prime information for multi-prime RSA keys.
131#[derive(Clone, Serialize, Deserialize)]
132pub struct RsaAdditionalPrime {
133	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
134	pub r: Vec<u8>,
135	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
136	pub d: Vec<u8>,
137	#[serde(serialize_with = "serialize_base64url", deserialize_with = "deserialize_base64url")]
138	pub t: Vec<u8>,
139}
140
141/// JWK, almost to spec (<https://datatracker.ietf.org/doc/html/rfc7517>) but not quite the same
142/// because it's annoying to implement.
143///
144/// This is the serialized form of a key, with plain fields you can build and edit. It is not
145/// usable on its own: call [`import`](Self::import) to validate it and get a usable [`Key`], and
146/// [`Key::export`] to go back the other way. What that key may do is whatever `key_ops` allows,
147/// so a verify-only JWK imports fine and simply cannot sign. `kty` is required; a missing value
148/// is refused rather than defaulted to `oct`.
149#[derive(Clone, Serialize, Deserialize)]
150#[non_exhaustive]
151pub struct Jwk {
152	/// The algorithm used by the key.
153	#[serde(rename = "alg")]
154	pub algorithm: Algorithm,
155
156	/// The permitted operations, defaulting to sign and verify when `key_ops` is absent
157	/// (optional per RFC 7517 section 4.3).
158	#[serde(rename = "key_ops", default = "sign_verify")]
159	pub operations: HashSet<KeyOperation>,
160
161	/// The key material.
162	#[serde(flatten)]
163	pub material: KeyMaterial,
164
165	/// The key ID, useful for rotating keys.
166	#[serde(skip_serializing_if = "Option::is_none")]
167	pub kid: Option<crate::KeyId>,
168
169	/// Optional authorization limits for tokens signed by this key.
170	#[serde(default, skip_serializing_if = "Option::is_none")]
171	pub scope: Option<crate::Scope>,
172}
173
174fn sign_verify() -> HashSet<KeyOperation> {
175	[KeyOperation::Sign, KeyOperation::Verify].into()
176}
177
178/// Matches the minimum `@moq/auth` enforces, so a key that loads in one loads in the other.
179const MIN_OCT_SECRET_BYTES: usize = 32;
180
181impl Jwk {
182	/// A key that can both sign and verify, with no key ID or scope.
183	///
184	/// Set the remaining fields on the returned value. The struct is `#[non_exhaustive]`, so
185	/// building it this way keeps working as JWK parameters are added.
186	pub fn new(algorithm: Algorithm, material: KeyMaterial) -> Self {
187		Self {
188			algorithm,
189			operations: sign_verify(),
190			material,
191			kid: None,
192			scope: None,
193		}
194	}
195
196	/// Validate the parameters and import this as a usable [`Key`].
197	///
198	/// The inverse of [`Key::export`]. Named rather than only a `TryFrom` impl so the conversion
199	/// is discoverable from here, and `import`/`export` rather than `validate` because the
200	/// `validate` methods elsewhere in this crate check without converting.
201	pub fn import(self) -> crate::Result<Key> {
202		if let Some(scope) = &self.scope {
203			scope.validate()?;
204		}
205
206		if let KeyMaterial::OCT { secret } = &self.material
207			&& secret.len() < MIN_OCT_SECRET_BYTES
208		{
209			return Err(KeyError::SecretTooShort(MIN_OCT_SECRET_BYTES).into());
210		}
211
212		Ok(Key {
213			jwk: self,
214			decode: Default::default(),
215			encode: Default::default(),
216		})
217	}
218}
219
220/// A validated key, ready to sign and verify tokens.
221///
222/// The fields are fixed at construction: derived crypto material is cached on first use, so a key
223/// that could be mutated would sign with stale material. Build one from a [`Jwk`], from
224/// [`Key::generate`], or by parsing with [`Key::from_str`], then use the builders to derive a new
225/// key rather than editing an existing one.
226#[derive(Clone)]
227pub struct Key {
228	jwk: Jwk,
229
230	// Cached for performance reasons, unfortunately.
231	decode: OnceLock<DecodingKey>,
232	encode: OnceLock<EncodingKey>,
233}
234
235/// Read-only access to the underlying [`Jwk`] fields (`key.algorithm`, `key.kid`, ...).
236///
237/// Deliberately no `DerefMut`: handing out `&mut Jwk` would let a caller change the algorithm or
238/// key material behind the cached crypto material, which is the bug this split exists to prevent.
239impl std::ops::Deref for Key {
240	type Target = Jwk;
241
242	fn deref(&self) -> &Self::Target {
243		&self.jwk
244	}
245}
246
247impl TryFrom<Jwk> for Key {
248	type Error = crate::Error;
249
250	fn try_from(jwk: Jwk) -> crate::Result<Self> {
251		jwk.import()
252	}
253}
254
255impl From<&Key> for Jwk {
256	fn from(key: &Key) -> Self {
257		key.export()
258	}
259}
260
261impl<'de> Deserialize<'de> for Key {
262	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
263	where
264		D: Deserializer<'de>,
265	{
266		let jwk = Jwk::deserialize(deserializer)?;
267		Key::try_from(jwk).map_err(serde::de::Error::custom)
268	}
269}
270
271impl Serialize for Key {
272	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
273	where
274		S: Serializer,
275	{
276		Serialize::serialize(&Jwk::from(self), serializer)
277	}
278}
279
280impl fmt::Debug for Key {
281	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282		f.debug_struct("Key")
283			.field("algorithm", &self.algorithm)
284			.field("operations", &self.operations)
285			.field("kid", &self.kid)
286			.field("scope", &self.scope)
287			.finish()
288	}
289}
290
291impl Key {
292	/// The serializable [`Jwk`] behind this key, cloned so editing it can't reach the original.
293	///
294	/// The inverse of [`Jwk::import`]. Use it to derive a variant: export, edit, import again.
295	/// Reading a single field needs no clone, since a [`Key`] derefs to its [`Jwk`].
296	pub fn export(&self) -> Jwk {
297		self.jwk.clone()
298	}
299
300	/// Parse a key from a string, auto-detecting JSON or base64url encoding.
301	#[allow(clippy::should_implement_trait)]
302	pub fn from_str(s: &str) -> crate::Result<Self> {
303		let s = s.trim();
304		if s.starts_with('{') {
305			Ok(serde_json::from_str(s)?)
306		} else {
307			let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s)?;
308			let json = String::from_utf8(decoded)?;
309			Ok(serde_json::from_str(&json)?)
310		}
311	}
312
313	/// Load a key from a file, auto-detecting JSON or base64url encoding.
314	pub fn from_file<P: AsRef<StdPath>>(path: P) -> crate::Result<Self> {
315		let contents = std::fs::read_to_string(&path)?;
316		Self::from_str(&contents)
317	}
318
319	/// Async version of [`from_file`](Self::from_file), using `tokio::fs`.
320	#[cfg(feature = "tokio")]
321	pub async fn from_file_async<P: AsRef<StdPath>>(path: P) -> crate::Result<Self> {
322		let contents = tokio::fs::read_to_string(path).await?;
323		Self::from_str(&contents)
324	}
325
326	/// Encode the key as base64url-encoded JSON.
327	pub fn to_str(&self) -> crate::Result<String> {
328		let json = serde_json::to_string(self)?;
329		Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json.as_bytes()))
330	}
331
332	/// Write the key to a file as base64url-encoded JSON.
333	///
334	/// A key carrying private material is written owner-only (mode `0600` on Unix), including when
335	/// it overwrites a file that was more permissive.
336	pub fn to_file<P: AsRef<StdPath>>(&self, path: P) -> crate::Result<()> {
337		let encoded = self.to_str()?;
338		crate::fs::write(path.as_ref(), &encoded, self.is_private())?;
339		Ok(())
340	}
341
342	/// Derive a verify-only copy of this key, dropping the private material.
343	///
344	/// Fails for symmetric (`oct`) keys, which have no public half, and for a key that cannot
345	/// verify in the first place.
346	pub fn to_public(&self) -> crate::Result<Self> {
347		if !self.operations.contains(&KeyOperation::Verify) {
348			return Err(KeyError::VerifyUnsupported.into());
349		}
350
351		let material = match self.material {
352			KeyMaterial::RSA { ref public, .. } => KeyMaterial::RSA {
353				public: public.clone(),
354				private: None,
355			},
356			KeyMaterial::EC {
357				ref x,
358				ref y,
359				ref curve,
360				..
361			} => KeyMaterial::EC {
362				x: x.clone(),
363				y: y.clone(),
364				curve: curve.clone(),
365				d: None,
366			},
367			KeyMaterial::OCT { .. } => return Err(KeyError::NoPublicKey.into()),
368			KeyMaterial::OKP { ref x, ref curve, .. } => KeyMaterial::OKP {
369				x: x.clone(),
370				curve: curve.clone(),
371				d: None,
372			},
373		};
374
375		Ok(Self {
376			jwk: Jwk {
377				algorithm: self.algorithm,
378				operations: [KeyOperation::Verify].into(),
379				material,
380				kid: self.kid.clone(),
381				scope: self.scope.clone(),
382			},
383			decode: Default::default(),
384			encode: Default::default(),
385		})
386	}
387
388	/// Whether the key carries private material: the half signing needs, and the half that must
389	/// not leak to another user on disk.
390	///
391	/// `key_ops` says what a key is *permitted* to do, which a public JWK can still advertise, so
392	/// this asks about the material rather than the declared operations.
393	pub(crate) fn is_private(&self) -> bool {
394		match &self.material {
395			KeyMaterial::OCT { .. } => true,
396			KeyMaterial::EC { d, .. } | KeyMaterial::OKP { d, .. } => d.is_some(),
397			KeyMaterial::RSA { private, .. } => private.is_some(),
398		}
399	}
400
401	fn to_decoding_key(&self) -> crate::Result<&DecodingKey> {
402		if let Some(key) = self.decode.get() {
403			return Ok(key);
404		}
405
406		let decoding_key = match self.material {
407			KeyMaterial::OCT { ref secret } => match self.algorithm {
408				Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => DecodingKey::from_secret(secret),
409				_ => return Err(KeyError::InvalidAlgorithm.into()),
410			},
411			KeyMaterial::EC {
412				ref curve,
413				ref x,
414				ref y,
415				..
416			} => match curve {
417				EllipticCurve::P256 => {
418					if self.algorithm != Algorithm::ES256 {
419						return Err(KeyError::InvalidAlgorithmForCurve("P-256").into());
420					}
421					if x.len() != 32 || y.len() != 32 {
422						return Err(KeyError::InvalidCoordinateLength("P-256").into());
423					}
424
425					DecodingKey::from_ec_components(
426						base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x).as_ref(),
427						base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y).as_ref(),
428					)?
429				}
430				EllipticCurve::P384 => {
431					if self.algorithm != Algorithm::ES384 {
432						return Err(KeyError::InvalidAlgorithmForCurve("P-384").into());
433					}
434					if x.len() != 48 || y.len() != 48 {
435						return Err(KeyError::InvalidCoordinateLength("P-384").into());
436					}
437
438					DecodingKey::from_ec_components(
439						base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x).as_ref(),
440						base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y).as_ref(),
441					)?
442				}
443				_ => return Err(KeyError::InvalidCurve("EC").into()),
444			},
445			KeyMaterial::OKP { ref curve, ref x, .. } => match curve {
446				EllipticCurve::Ed25519 => {
447					if self.algorithm != Algorithm::EdDSA {
448						return Err(KeyError::InvalidAlgorithmForCurve("Ed25519").into());
449					}
450
451					DecodingKey::from_ed_components(
452						base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x).as_ref(),
453					)?
454				}
455				_ => return Err(KeyError::InvalidCurve("OKP").into()),
456			},
457			KeyMaterial::RSA { ref public, .. } => {
458				DecodingKey::from_rsa_raw_components(public.n.as_ref(), public.e.as_ref())
459			}
460		};
461
462		Ok(self.decode.get_or_init(|| decoding_key))
463	}
464
465	fn to_encoding_key(&self) -> crate::Result<&EncodingKey> {
466		if let Some(key) = self.encode.get() {
467			return Ok(key);
468		}
469
470		let encoding_key = match self.material {
471			KeyMaterial::OCT { ref secret } => match self.algorithm {
472				Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => EncodingKey::from_secret(secret),
473				_ => return Err(KeyError::InvalidAlgorithm.into()),
474			},
475			KeyMaterial::EC { ref curve, ref d, .. } => {
476				let d = d.as_ref().ok_or(KeyError::MissingPrivateKey)?;
477
478				match curve {
479					EllipticCurve::P256 => {
480						let secret_key = SecretKey::<p256::NistP256>::from_slice(d)?;
481						let doc = secret_key.to_pkcs8_der()?;
482						EncodingKey::from_ec_der(doc.as_bytes())
483					}
484					EllipticCurve::P384 => {
485						let secret_key = SecretKey::<p384::NistP384>::from_slice(d)?;
486						let doc = secret_key.to_pkcs8_der()?;
487						EncodingKey::from_ec_der(doc.as_bytes())
488					}
489					_ => return Err(KeyError::InvalidCurve("EC").into()),
490				}
491			}
492			KeyMaterial::OKP {
493				ref curve,
494				ref d,
495				ref x,
496			} => {
497				let d = d.as_ref().ok_or(KeyError::MissingPrivateKey)?;
498
499				let key_pair =
500					aws_lc_rs::signature::Ed25519KeyPair::from_seed_and_public_key(d.as_slice(), x.as_slice())?;
501
502				match curve {
503					EllipticCurve::Ed25519 => EncodingKey::from_ed_der(key_pair.to_pkcs8()?.as_ref()),
504					_ => return Err(KeyError::InvalidCurve("OKP").into()),
505				}
506			}
507			KeyMaterial::RSA {
508				ref public,
509				ref private,
510			} => {
511				let n = BigUint::from_bytes_be(&public.n);
512				let e = BigUint::from_bytes_be(&public.e);
513				let private = private.as_ref().ok_or(KeyError::MissingPrivateKey)?;
514				let d = BigUint::from_bytes_be(&private.d);
515				let p = BigUint::from_bytes_be(&private.p);
516				let q = BigUint::from_bytes_be(&private.q);
517
518				let rsa = rsa::RsaPrivateKey::from_components(n, e, d, vec![p, q]);
519				let pem = rsa?.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF);
520
521				EncodingKey::from_rsa_pem(pem?.as_bytes())?
522			}
523		};
524
525		Ok(self.encode.get_or_init(|| encoding_key))
526	}
527
528	/// Verify a token's signature with this key and return its claims.
529	///
530	/// Rejects an expired token (the `exp` claim), one not yet valid (`nbf`), and one
531	/// that grants nothing.
532	/// Scoping the claims to a connection path is a separate step; see
533	/// [`Claims::authorize`].
534	pub fn verify(&self, token: &str) -> crate::Result<Claims> {
535		if !self.operations.contains(&KeyOperation::Verify) {
536			return Err(KeyError::VerifyUnsupported.into());
537		}
538
539		let decode = self.to_decoding_key()?;
540
541		let mut validation = jsonwebtoken::Validation::new(self.algorithm.into());
542		validation.required_spec_claims = Default::default(); // Don't require exp, but still validate it if present
543		validation.validate_exp = false; // We validate exp ourselves to handle null values
544
545		let token = jsonwebtoken::decode::<Claims>(token, decode, &validation)?;
546
547		validate_times(&token.claims, std::time::SystemTime::now())?;
548		token.claims.validate()?;
549		self.validate_scope(&token.claims)?;
550
551		Ok(token.claims)
552	}
553
554	/// Sign the claims with this key, returning the encoded token.
555	pub fn sign(&self, payload: &Claims) -> crate::Result<String> {
556		if !self.operations.contains(&KeyOperation::Sign) {
557			return Err(KeyError::SignUnsupported.into());
558		}
559
560		payload.validate()?;
561		self.validate_scope(payload)?;
562
563		let encode = self.to_encoding_key()?;
564
565		let mut header = Header::new(self.algorithm.into());
566		header.kid = self.kid.as_ref().map(|k| k.to_string());
567		let token = jsonwebtoken::encode(&header, &payload, encode)?;
568		Ok(token)
569	}
570
571	/// Generate a key pair for the given algorithm, returning the private and public keys.
572	pub fn generate(algorithm: Algorithm, id: Option<crate::KeyId>) -> crate::Result<Self> {
573		generate(algorithm, id)
574	}
575
576	/// Derive a key with an authorization scope attached, capping what its tokens may grant.
577	///
578	/// The scope is validated here, and it is the only way to set one, so a key can never carry a
579	/// scope that permits nothing.
580	pub fn with_scope(mut self, scope: crate::Scope) -> crate::Result<Self> {
581		scope.validate()?;
582		self.jwk.scope = Some(scope);
583		Ok(self)
584	}
585
586	/// Derive a key restricted to the given operations.
587	pub fn with_operations(mut self, operations: impl IntoIterator<Item = KeyOperation>) -> Self {
588		self.jwk.operations = operations.into_iter().collect();
589		self
590	}
591
592	fn validate_scope(&self, claims: &Claims) -> crate::Result<()> {
593		if let Some(scope) = &self.scope {
594			scope.validate()?;
595			if !scope.allows(claims) {
596				return Err(crate::Error::ScopeExceeded);
597			}
598		}
599		Ok(())
600	}
601}
602
603/// Refuse claims expired at `now` (`exp <= now`) or not yet valid (`nbf > now`), as `jose` does.
604fn validate_times(claims: &Claims, now: std::time::SystemTime) -> crate::Result<()> {
605	if claims.expires.is_some_and(|exp| exp <= now) {
606		return Err(crate::Error::TokenExpired);
607	}
608	if claims.not_before.is_some_and(|nbf| nbf > now) {
609		return Err(crate::Error::TokenNotYetValid);
610	}
611	Ok(())
612}
613
614/// Serialize bytes as base64url without padding
615fn serialize_base64url<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
616where
617	S: Serializer,
618{
619	let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
620	serializer.serialize_str(&encoded)
621}
622
623fn serialize_base64url_optional<S>(bytes: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
624where
625	S: Serializer,
626{
627	match bytes {
628		Some(b) => serialize_base64url(b, serializer),
629		None => serializer.serialize_none(),
630	}
631}
632
633/// Deserialize base64url string to bytes, supporting both padded and unpadded formats for backwards compatibility
634fn deserialize_base64url<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
635where
636	D: Deserializer<'de>,
637{
638	let s = String::deserialize(deserializer)?;
639
640	// Try to decode as unpadded base64url first (preferred format)
641	base64::engine::general_purpose::URL_SAFE_NO_PAD
642		.decode(&s)
643		.or_else(|_| {
644			// Fall back to padded base64url for backwards compatibility
645			base64::engine::general_purpose::URL_SAFE.decode(&s)
646		})
647		.map_err(serde::de::Error::custom)
648}
649
650fn deserialize_base64url_optional<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
651where
652	D: Deserializer<'de>,
653{
654	let s: Option<String> = Option::deserialize(deserializer)?;
655	match s {
656		Some(s) => {
657			let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
658				.decode(&s)
659				.or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(&s))
660				.map_err(serde::de::Error::custom)?;
661			Ok(Some(decoded))
662		}
663		None => Ok(None),
664	}
665}
666
667#[cfg(test)]
668mod tests {
669	use super::*;
670
671	fn patterns(texts: &[&str]) -> crate::Patterns {
672		texts.iter().map(|text| text.parse().unwrap()).collect()
673	}
674	use std::time::{Duration, SystemTime};
675
676	fn create_test_key() -> Key {
677		let mut jwk = Jwk::new(
678			Algorithm::HS256,
679			KeyMaterial::OCT {
680				secret: b"test-secret-that-is-long-enough-for-hmac-sha256".to_vec(),
681			},
682		);
683		jwk.kid = Some(crate::KeyId::decode("test-key-1").unwrap());
684		jwk.import().unwrap()
685	}
686
687	fn create_test_claims() -> Claims {
688		Claims {
689			root: "test-path".to_string(),
690			publish: patterns(&["test-pub/**"]),
691			subscribe: patterns(&["test-sub/**"]),
692			expires: Some(SystemTime::now() + Duration::from_secs(3600)),
693			issued: Some(SystemTime::now()),
694			not_before: None,
695		}
696	}
697
698	#[test]
699	fn test_key_from_str_valid() {
700		let key = create_test_key();
701		let json = key.to_str().unwrap();
702		let loaded_key = Key::from_str(&json).unwrap();
703
704		assert_eq!(loaded_key.algorithm, key.algorithm);
705		assert_eq!(loaded_key.operations, key.operations);
706		match (&loaded_key.material, &key.material) {
707			(KeyMaterial::OCT { secret: loaded_secret }, KeyMaterial::OCT { secret }) => {
708				assert_eq!(loaded_secret, secret);
709			}
710			_ => panic!("Expected OCT key"),
711		}
712		assert_eq!(loaded_key.kid, key.kid);
713	}
714
715	/// An oct JWK without `kty` is refused; JS already did, and so does this crate.
716	#[test]
717	fn test_key_oct_without_kty_is_refused() {
718		let json = r#"{"alg":"HS256","key_ops":["sign","verify"],"k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68"}"#;
719		assert!(Key::from_str(json).is_err());
720	}
721
722	#[test]
723	fn test_key_without_key_ops_defaults_sign_verify() {
724		let json = r#"{"kty":"oct","alg":"HS256","k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68","kid":"no-ops"}"#;
725		let key = Key::from_str(json).unwrap();
726
727		assert_eq!(key.operations, sign_verify());
728
729		let claims = create_test_claims();
730		let token = key.sign(&claims).unwrap();
731		let verified = key.verify(&token).unwrap();
732		assert_eq!(verified.root, claims.root);
733	}
734
735	#[test]
736	fn test_key_without_key_ops_round_trip() {
737		let json = r#"{"kty":"oct","alg":"HS256","k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68"}"#;
738		let key = Key::from_str(json).unwrap();
739
740		let serialized = serde_json::to_string(&key).unwrap();
741		let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
742		let ops = parsed["key_ops"].as_array().unwrap();
743		assert_eq!(ops.len(), 2);
744
745		let reloaded = Key::from_str(&serialized).unwrap();
746		assert_eq!(reloaded.operations, sign_verify());
747		assert_eq!(reloaded.algorithm, key.algorithm);
748	}
749
750	// Defaulting key_ops must not let a weak or truncated file parse into a working key.
751	#[test]
752	fn test_key_oct_secret_required_and_min_length() {
753		// Missing k entirely, the truncated-file case.
754		assert!(Key::from_str(r#"{"kty":"oct","alg":"HS256"}"#).is_err());
755		assert!(Key::from_str(r#"{"kty":"oct","alg":"HS256","k":""}"#).is_err());
756
757		// 16-byte secret, below the 32-byte minimum.
758		assert!(Key::from_str(r#"{"kty":"oct","alg":"HS256","k":"AAAAAAAAAAAAAAAAAAAAAA"}"#).is_err());
759
760		// A short secret is rejected however the Jwk was built, not just when deserialized.
761		let short = Jwk::new(Algorithm::HS256, KeyMaterial::OCT { secret: vec![0; 16] });
762		assert!(short.import().is_err());
763
764		// Exactly 32 bytes.
765		let key =
766			Key::from_str(r#"{"kty":"oct","alg":"HS256","k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68"}"#).unwrap();
767		let KeyMaterial::OCT { ref secret } = key.material else {
768			panic!("Expected OCT key");
769		};
770		assert_eq!(secret.len(), 32);
771	}
772
773	#[test]
774	fn test_key_without_key_ops_to_public() {
775		let json = r#"{"kty":"OKP","alg":"EdDSA","crv":"Ed25519","x":"UiU9fT_SdBBpkFtJPRCY0gX1jK_Dr9syYLFuEz4QUM4","d":"lm-L_PV3ksuQ-KrFBgFMDJqAZC3_Z6Z5UC4ZQY5OoDQ","kid":"defaulted"}"#;
776		let key = Key::from_str(json).unwrap();
777		assert_eq!(key.operations, sign_verify());
778
779		let public = key.to_public().unwrap();
780		assert_eq!(public.operations, [KeyOperation::Verify].into());
781	}
782
783	#[test]
784	fn test_key_from_str_invalid_json() {
785		let result = Key::from_str("invalid json");
786		assert!(result.is_err());
787	}
788
789	#[test]
790	fn test_key_to_str() {
791		let key = create_test_key();
792		let encoded = key.to_str().unwrap();
793
794		// Should be base64url, not raw JSON
795		assert!(!encoded.contains('{'));
796
797		// Round-trip through from_str
798		let loaded = Key::from_str(&encoded).unwrap();
799		assert_eq!(loaded.algorithm, Algorithm::HS256);
800		assert_eq!(loaded.kid, key.kid);
801		assert!(loaded.operations.contains(&KeyOperation::Sign));
802		assert!(loaded.operations.contains(&KeyOperation::Verify));
803	}
804
805	#[test]
806	fn test_key_sign_success() {
807		let key = create_test_key();
808		let claims = create_test_claims();
809		let token = key.sign(&claims).unwrap();
810
811		assert!(!token.is_empty());
812		assert_eq!(token.matches('.').count(), 2); // JWT format: header.payload.signature
813	}
814
815	#[test]
816	fn test_key_scope_enforced_when_signing_and_verifying() {
817		let unrestricted = create_test_key();
818		let scoped = unrestricted
819			.clone()
820			.with_scope(crate::Scope {
821				root: "test-path".into(),
822				publish: patterns(&["allowed/**"]),
823				subscribe: patterns(&[]),
824			})
825			.unwrap();
826		let allowed = Claims {
827			root: "test-path".into(),
828			publish: patterns(&["allowed/room/**"]),
829			..Default::default()
830		};
831		let denied = Claims {
832			root: "test-path".into(),
833			publish: patterns(&["other/**"]),
834			..Default::default()
835		};
836
837		assert!(scoped.sign(&allowed).is_ok());
838		assert!(matches!(scoped.sign(&denied), Err(crate::Error::ScopeExceeded)));
839
840		let forged = unrestricted.sign(&denied).unwrap();
841		assert!(matches!(scoped.verify(&forged), Err(crate::Error::ScopeExceeded)));
842	}
843
844	/// A key's crypto material is derived once and cached, so the fields it was derived from must
845	/// stay fixed. Changing the algorithm means building a new key, which derives fresh material.
846	#[test]
847	fn test_key_derived_material_never_stale() {
848		let claims = Claims {
849			root: "test-path".into(),
850			publish: patterns(&["test-pub/**"]),
851			..Default::default()
852		};
853
854		// Sign once so the encode/decode caches are populated.
855		let key = create_test_key();
856		let token = key.sign(&claims).unwrap();
857		assert!(key.encode.get().is_some());
858
859		// The only way to change the algorithm is to build another key, which starts with an empty
860		// cache and therefore signs with material matching the header it writes.
861		let mut jwk = Jwk::from(&key);
862		jwk.algorithm = Algorithm::HS384;
863		let derived = Key::try_from(jwk).unwrap();
864		assert!(derived.encode.get().is_none());
865
866		let derived_token = derived.sign(&claims).unwrap();
867		assert_ne!(token, derived_token);
868
869		// The derived key agrees with one parsed cold from the same JWK, and the original key
870		// rejects the token it did not sign.
871		let cold = Key::from_str(&derived.to_str().unwrap()).unwrap();
872		assert_eq!(derived_token, cold.sign(&claims).unwrap());
873		assert!(cold.verify(&derived_token).is_ok());
874		assert!(key.verify(&derived_token).is_err());
875	}
876
877	/// A scope can only be attached through the validating builder, and the serde path validates
878	/// too, so a key can never carry a scope that grants nothing.
879	#[test]
880	fn test_key_scope_requires_validation() {
881		let key = create_test_key();
882		assert!(key.scope.is_none());
883
884		let useless = crate::Scope::default();
885		assert!(matches!(
886			key.clone().with_scope(useless.clone()),
887			Err(crate::Error::UselessScope)
888		));
889
890		let mut jwk = Jwk::from(&key);
891		jwk.scope = Some(useless);
892		assert!(matches!(Key::try_from(jwk), Err(crate::Error::UselessScope)));
893
894		let json = r#"{"kty":"oct","alg":"HS256","key_ops":["sign"],"k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68","scope":{}}"#;
895		assert!(Key::from_str(json).is_err());
896	}
897
898	#[test]
899	fn test_key_sign_no_permission() {
900		let key = create_test_key().with_operations([KeyOperation::Verify]);
901		let claims = create_test_claims();
902
903		let result = key.sign(&claims);
904		assert!(result.is_err());
905		assert!(result.unwrap_err().to_string().contains("key does not support signing"));
906	}
907
908	#[test]
909	fn test_key_sign_invalid_claims() {
910		let key = create_test_key();
911		let invalid_claims = Claims {
912			root: "test-path".to_string(),
913			publish: patterns(&[]),
914			subscribe: patterns(&[]),
915			expires: None,
916			issued: None,
917			not_before: None,
918		};
919
920		let result = key.sign(&invalid_claims);
921		assert!(result.is_err());
922		assert!(
923			result
924				.unwrap_err()
925				.to_string()
926				.contains("no publish or subscribe allowed; token is useless")
927		);
928	}
929
930	#[test]
931	fn test_key_verify_success() {
932		let key = create_test_key();
933		let claims = create_test_claims();
934		let token = key.sign(&claims).unwrap();
935
936		let verified_claims = key.verify(&token).unwrap();
937		assert_eq!(verified_claims.root, claims.root);
938		assert_eq!(verified_claims.publish, claims.publish);
939		assert_eq!(verified_claims.subscribe, claims.subscribe);
940	}
941
942	#[test]
943	fn test_key_verify_no_permission() {
944		let key = create_test_key().with_operations([KeyOperation::Sign]);
945
946		let result = key.verify("some.jwt.token");
947		assert!(result.is_err());
948		assert!(
949			result
950				.unwrap_err()
951				.to_string()
952				.contains("key does not support verification")
953		);
954	}
955
956	#[test]
957	fn test_key_verify_invalid_token() {
958		let key = create_test_key();
959		let result = key.verify("invalid-token");
960		assert!(result.is_err());
961	}
962
963	#[test]
964	fn test_key_verify_path_mismatch() {
965		let key = create_test_key();
966		let claims = create_test_claims();
967		let token = key.sign(&claims).unwrap();
968
969		// This test was expecting a path mismatch error, but now decode succeeds
970		let result = key.verify(&token);
971		assert!(result.is_ok());
972	}
973
974	/// Sign an arbitrary payload with `key`, bypassing [`Claims`], as another issuer might.
975	fn sign_raw(key: &Key, payload: serde_json::Value) -> String {
976		let header = Header::new(key.algorithm.into());
977		jsonwebtoken::encode(&header, &payload, key.to_encoding_key().unwrap()).unwrap()
978	}
979
980	/// An issuer's bookkeeping is read and dropped; anything else is refused by name,
981	/// since it might narrow the grant, and a misspelled `root` would widen it to
982	/// everything.
983	#[test]
984	fn test_key_verify_only_registered_claims() {
985		let key = create_test_key();
986		let now = SystemTime::now()
987			.duration_since(SystemTime::UNIX_EPOCH)
988			.unwrap()
989			.as_secs();
990
991		let token = sign_raw(
992			&key,
993			serde_json::json!({"root": "room", "publish": ["**"], "iss": "api", "sub": "alice", "jti": "1", "iat": now}),
994		);
995		assert_eq!(key.verify(&token).unwrap().root, "room");
996
997		for (claim, payload) in [
998			("rooot", serde_json::json!({"rooot": "room/123", "publish": ["**"]})),
999			(
1000				"user_id",
1001				serde_json::json!({"root": "room", "publish": ["**"], "user_id": 7}),
1002			),
1003			(
1004				"cluster",
1005				serde_json::json!({"root": "room", "put": [""], "cluster": true}),
1006			),
1007		] {
1008			let err = key.verify(&sign_raw(&key, payload)).unwrap_err().to_string();
1009			assert!(err.contains(&format!("`{claim}`")), "{claim}: {err}");
1010		}
1011
1012		// No audience is configured, so there is nothing to check one against.
1013		let token = sign_raw(
1014			&key,
1015			serde_json::json!({"root": "room", "publish": ["**"], "aud": "relay"}),
1016		);
1017		assert!(key.verify(&token).is_err());
1018	}
1019
1020	#[test]
1021	fn test_key_verify_enforces_not_before() {
1022		let key = create_test_key();
1023		let at = |offset: i64| {
1024			let now = SystemTime::now()
1025				.duration_since(SystemTime::UNIX_EPOCH)
1026				.unwrap()
1027				.as_secs() as i64;
1028			sign_raw(
1029				&key,
1030				serde_json::json!({"root": "room", "publish": ["**"], "nbf": now + offset}),
1031			)
1032		};
1033		assert!(key.verify(&at(-60)).is_ok());
1034		assert!(matches!(key.verify(&at(3600)), Err(crate::Error::TokenNotYetValid)));
1035	}
1036
1037	/// `exp` is refused at the instant itself and `nbf` accepted at it, matching `jose`.
1038	#[test]
1039	fn validate_times_at_the_boundary() {
1040		let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
1041		let second = Duration::from_secs(1);
1042
1043		let at = |expires: Option<SystemTime>, not_before: Option<SystemTime>| {
1044			let mut claims = create_test_claims();
1045			claims.expires = expires;
1046			claims.not_before = not_before;
1047			validate_times(&claims, now)
1048		};
1049
1050		assert!(at(Some(now + second), None).is_ok());
1051		assert!(matches!(at(Some(now), None), Err(crate::Error::TokenExpired)));
1052		assert!(matches!(at(Some(now - second), None), Err(crate::Error::TokenExpired)));
1053
1054		assert!(at(None, Some(now)).is_ok());
1055		assert!(at(None, Some(now - second)).is_ok());
1056		assert!(matches!(
1057			at(None, Some(now + second)),
1058			Err(crate::Error::TokenNotYetValid)
1059		));
1060	}
1061
1062	#[test]
1063	fn test_key_verify_expired_token() {
1064		let key = create_test_key();
1065		let mut claims = create_test_claims();
1066		claims.expires = Some(SystemTime::now() - Duration::from_secs(3600)); // 1 hour ago
1067		let token = key.sign(&claims).unwrap();
1068
1069		let result = key.verify(&token);
1070		assert!(result.is_err());
1071	}
1072
1073	#[test]
1074	fn test_key_verify_token_without_exp() {
1075		let key = create_test_key();
1076		let claims = Claims {
1077			root: "test-path".to_string(),
1078			publish: patterns(&["**"]),
1079			subscribe: patterns(&["**"]),
1080			expires: None,
1081			issued: None,
1082			not_before: None,
1083		};
1084		let token = key.sign(&claims).unwrap();
1085
1086		let verified_claims = key.verify(&token).unwrap();
1087		assert_eq!(verified_claims.root, claims.root);
1088		assert_eq!(verified_claims.publish, claims.publish);
1089		assert_eq!(verified_claims.subscribe, claims.subscribe);
1090		assert_eq!(verified_claims.expires, None);
1091	}
1092
1093	#[test]
1094	fn test_key_round_trip() {
1095		let key = create_test_key();
1096		let original_claims = Claims {
1097			root: "test-path".to_string(),
1098			publish: patterns(&["test-pub/**"]),
1099			subscribe: patterns(&["test-sub/**"]),
1100			expires: Some(SystemTime::now() + Duration::from_secs(3600)),
1101			issued: Some(SystemTime::now()),
1102			not_before: None,
1103		};
1104
1105		let token = key.sign(&original_claims).unwrap();
1106		let verified_claims = key.verify(&token).unwrap();
1107
1108		assert_eq!(verified_claims.root, original_claims.root);
1109		assert_eq!(verified_claims.publish, original_claims.publish);
1110		assert_eq!(verified_claims.subscribe, original_claims.subscribe);
1111	}
1112
1113	#[test]
1114	fn test_key_generate_hs256() {
1115		let key = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
1116		assert!(key.is_ok());
1117		let key = key.unwrap();
1118
1119		assert_eq!(key.algorithm, Algorithm::HS256);
1120		assert_eq!(key.kid, Some(crate::KeyId::decode("test-id").unwrap()));
1121		assert_eq!(key.operations, [KeyOperation::Sign, KeyOperation::Verify].into());
1122
1123		match &key.material {
1124			KeyMaterial::OCT { secret } => assert_eq!(secret.len(), 32),
1125			_ => panic!("Expected OCT key"),
1126		}
1127	}
1128
1129	#[test]
1130	fn test_key_generate_hs384() {
1131		let key = Key::generate(Algorithm::HS384, Some(crate::KeyId::decode("test-id").unwrap()));
1132		assert!(key.is_ok());
1133		let key = key.unwrap();
1134
1135		assert_eq!(key.algorithm, Algorithm::HS384);
1136
1137		match &key.material {
1138			KeyMaterial::OCT { secret } => assert_eq!(secret.len(), 48),
1139			_ => panic!("Expected OCT key"),
1140		}
1141	}
1142
1143	#[test]
1144	fn test_key_generate_hs512() {
1145		let key = Key::generate(Algorithm::HS512, Some(crate::KeyId::decode("test-id").unwrap()));
1146		assert!(key.is_ok());
1147		let key = key.unwrap();
1148
1149		assert_eq!(key.algorithm, Algorithm::HS512);
1150
1151		match &key.material {
1152			KeyMaterial::OCT { secret } => assert_eq!(secret.len(), 64),
1153			_ => panic!("Expected OCT key"),
1154		}
1155	}
1156
1157	#[test]
1158	fn test_key_generate_rs512() {
1159		let key = Key::generate(Algorithm::RS512, Some(crate::KeyId::decode("test-id").unwrap()));
1160		assert!(key.is_ok());
1161		let key = key.unwrap();
1162
1163		assert_eq!(key.algorithm, Algorithm::RS512);
1164		assert!(matches!(key.material, KeyMaterial::RSA { .. }));
1165		match &key.material {
1166			KeyMaterial::RSA { public, private } => {
1167				assert!(private.is_some());
1168				assert_eq!(public.n.len(), 256);
1169				assert_eq!(public.e.len(), 3);
1170			}
1171			_ => panic!("Expected RSA key"),
1172		}
1173	}
1174
1175	#[test]
1176	fn test_key_generate_es256() {
1177		let key = Key::generate(Algorithm::ES256, Some(crate::KeyId::decode("test-id").unwrap()));
1178		assert!(key.is_ok());
1179		let key = key.unwrap();
1180
1181		assert_eq!(key.algorithm, Algorithm::ES256);
1182		assert!(matches!(key.material, KeyMaterial::EC { .. }))
1183	}
1184
1185	#[test]
1186	fn test_key_generate_ps512() {
1187		let key = Key::generate(Algorithm::PS512, Some(crate::KeyId::decode("test-id").unwrap()));
1188		assert!(key.is_ok());
1189		let key = key.unwrap();
1190
1191		assert_eq!(key.algorithm, Algorithm::PS512);
1192		assert!(matches!(key.material, KeyMaterial::RSA { .. }));
1193	}
1194
1195	#[test]
1196	fn test_key_generate_eddsa() {
1197		let key = Key::generate(Algorithm::EdDSA, Some(crate::KeyId::decode("test-id").unwrap()));
1198		assert!(key.is_ok());
1199		let key = key.unwrap();
1200
1201		assert_eq!(key.algorithm, Algorithm::EdDSA);
1202		assert!(matches!(key.material, KeyMaterial::OKP { .. }));
1203	}
1204
1205	#[test]
1206	fn test_key_generate_without_id() {
1207		let key = Key::generate(Algorithm::HS256, None);
1208		assert!(key.is_ok());
1209		let key = key.unwrap();
1210
1211		assert_eq!(key.algorithm, Algorithm::HS256);
1212		assert_eq!(key.kid, None);
1213		assert_eq!(key.operations, [KeyOperation::Sign, KeyOperation::Verify].into());
1214	}
1215
1216	#[test]
1217	fn test_public_key_conversion_hmac() {
1218		let key = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()))
1219			.expect("HMAC key generation failed");
1220
1221		assert!(key.to_public().is_err());
1222	}
1223
1224	#[test]
1225	fn test_public_key_conversion_rsa() {
1226		let key = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
1227		assert!(key.is_ok());
1228		let key = key.unwrap();
1229
1230		let public_key = key.to_public().unwrap();
1231		assert_eq!(key.kid, public_key.kid);
1232		assert_eq!(public_key.operations, [KeyOperation::Verify].into());
1233		assert!(public_key.encode.get().is_none());
1234		assert!(public_key.decode.get().is_none());
1235		assert!(matches!(public_key.material, KeyMaterial::RSA { .. }));
1236
1237		if let KeyMaterial::RSA { public, private } = &public_key.material {
1238			assert!(private.is_none());
1239
1240			if let KeyMaterial::RSA { public: src_public, .. } = &key.material {
1241				assert_eq!(public.e, src_public.e);
1242				assert_eq!(public.n, src_public.n);
1243			} else {
1244				unreachable!("Expected RSA key")
1245			}
1246		} else {
1247			unreachable!("Expected RSA key");
1248		}
1249	}
1250
1251	#[test]
1252	fn test_public_key_conversion_es() {
1253		let key = Key::generate(Algorithm::ES256, Some(crate::KeyId::decode("test-id").unwrap()));
1254		assert!(key.is_ok());
1255		let key = key.unwrap();
1256
1257		let public_key = key.to_public().unwrap();
1258		assert_eq!(key.kid, public_key.kid);
1259		assert_eq!(public_key.operations, [KeyOperation::Verify].into());
1260		assert!(public_key.encode.get().is_none());
1261		assert!(public_key.decode.get().is_none());
1262		assert!(matches!(public_key.material, KeyMaterial::EC { .. }));
1263
1264		if let KeyMaterial::EC { x, y, d, curve } = &public_key.material {
1265			assert!(d.is_none());
1266
1267			if let KeyMaterial::EC {
1268				x: src_x,
1269				y: src_y,
1270				curve: src_curve,
1271				..
1272			} = &key.material
1273			{
1274				assert_eq!(x, src_x);
1275				assert_eq!(y, src_y);
1276				assert_eq!(curve, src_curve);
1277			} else {
1278				unreachable!("Expected EC key")
1279			}
1280		} else {
1281			unreachable!("Expected EC key");
1282		}
1283	}
1284
1285	#[test]
1286	fn test_public_key_conversion_ed() {
1287		let key = Key::generate(Algorithm::EdDSA, Some(crate::KeyId::decode("test-id").unwrap()));
1288		assert!(key.is_ok());
1289		let key = key.unwrap();
1290
1291		let public_key = key.to_public().unwrap();
1292		assert_eq!(key.kid, public_key.kid);
1293		assert_eq!(public_key.operations, [KeyOperation::Verify].into());
1294		assert!(public_key.encode.get().is_none());
1295		assert!(public_key.decode.get().is_none());
1296		assert!(matches!(public_key.material, KeyMaterial::OKP { .. }));
1297
1298		if let KeyMaterial::OKP { x, d, curve } = &public_key.material {
1299			assert!(d.is_none());
1300
1301			if let KeyMaterial::OKP {
1302				x: src_x,
1303				curve: src_curve,
1304				..
1305			} = &key.material
1306			{
1307				assert_eq!(x, src_x);
1308				assert_eq!(curve, src_curve);
1309			} else {
1310				unreachable!("Expected OKP key")
1311			}
1312		} else {
1313			unreachable!("Expected OKP key");
1314		}
1315	}
1316
1317	#[test]
1318	fn test_key_generate_sign_verify_cycle() {
1319		let key = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
1320		assert!(key.is_ok());
1321		let key = key.unwrap();
1322
1323		let claims = create_test_claims();
1324
1325		let token = key.sign(&claims).unwrap();
1326		let verified_claims = key.verify(&token).unwrap();
1327
1328		assert_eq!(verified_claims.root, claims.root);
1329		assert_eq!(verified_claims.publish, claims.publish);
1330		assert_eq!(verified_claims.subscribe, claims.subscribe);
1331	}
1332
1333	#[test]
1334	fn test_key_debug_no_secret() {
1335		let key = create_test_key();
1336		let debug_str = format!("{key:?}");
1337
1338		assert!(debug_str.contains("algorithm: HS256"));
1339		assert!(debug_str.contains("operations"));
1340		assert!(debug_str.contains("kid: Some(KeyId(\"test-key-1\"))"));
1341		assert!(!debug_str.contains("secret")); // Should not contain secret
1342	}
1343
1344	#[test]
1345	fn test_key_operations_enum() {
1346		let sign_op = KeyOperation::Sign;
1347		let verify_op = KeyOperation::Verify;
1348		let decrypt_op = KeyOperation::Decrypt;
1349		let encrypt_op = KeyOperation::Encrypt;
1350
1351		assert_eq!(sign_op, KeyOperation::Sign);
1352		assert_eq!(verify_op, KeyOperation::Verify);
1353		assert_eq!(decrypt_op, KeyOperation::Decrypt);
1354		assert_eq!(encrypt_op, KeyOperation::Encrypt);
1355
1356		assert_ne!(sign_op, verify_op);
1357		assert_ne!(decrypt_op, encrypt_op);
1358	}
1359
1360	#[test]
1361	fn test_key_operations_serde() {
1362		let operations = [KeyOperation::Sign, KeyOperation::Verify];
1363		let json = serde_json::to_string(&operations).unwrap();
1364		assert!(json.contains("\"sign\""));
1365		assert!(json.contains("\"verify\""));
1366
1367		let deserialized: Vec<KeyOperation> = serde_json::from_str(&json).unwrap();
1368		assert_eq!(deserialized, operations);
1369	}
1370
1371	#[test]
1372	fn test_key_serde() {
1373		let key = create_test_key();
1374		let json = serde_json::to_string(&key).unwrap();
1375		let deserialized: Key = serde_json::from_str(&json).unwrap();
1376
1377		assert_eq!(deserialized.algorithm, key.algorithm);
1378		assert_eq!(deserialized.operations, key.operations);
1379		assert_eq!(deserialized.kid, key.kid);
1380
1381		if let (
1382			KeyMaterial::OCT {
1383				secret: original_secret,
1384			},
1385			KeyMaterial::OCT {
1386				secret: deserialized_secret,
1387			},
1388		) = (&key.material, &deserialized.material)
1389		{
1390			assert_eq!(deserialized_secret, original_secret);
1391		} else {
1392			panic!("Expected both keys to be OCT variant");
1393		}
1394	}
1395
1396	#[test]
1397	fn test_key_clone() {
1398		let key = create_test_key();
1399		let cloned = key.clone();
1400
1401		assert_eq!(cloned.algorithm, key.algorithm);
1402		assert_eq!(cloned.operations, key.operations);
1403		assert_eq!(cloned.kid, key.kid);
1404
1405		if let (
1406			KeyMaterial::OCT {
1407				secret: original_secret,
1408			},
1409			KeyMaterial::OCT { secret: cloned_secret },
1410		) = (&key.material, &cloned.material)
1411		{
1412			assert_eq!(cloned_secret, original_secret);
1413		} else {
1414			panic!("Expected both keys to be OCT variant");
1415		}
1416	}
1417
1418	#[test]
1419	fn test_hmac_algorithms() {
1420		let key_256 = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
1421		let key_384 = Key::generate(Algorithm::HS384, Some(crate::KeyId::decode("test-id").unwrap()));
1422		let key_512 = Key::generate(Algorithm::HS512, Some(crate::KeyId::decode("test-id").unwrap()));
1423
1424		let claims = create_test_claims();
1425
1426		// Test that each algorithm can sign and verify
1427		for key in [key_256, key_384, key_512] {
1428			assert!(key.is_ok());
1429			let key = key.unwrap();
1430
1431			let token = key.sign(&claims).unwrap();
1432			let verified_claims = key.verify(&token).unwrap();
1433			assert_eq!(verified_claims.root, claims.root);
1434		}
1435	}
1436
1437	#[test]
1438	fn test_rsa_pkcs1_asymmetric_algorithms() {
1439		let key_rs256 = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
1440		let key_rs384 = Key::generate(Algorithm::RS384, Some(crate::KeyId::decode("test-id").unwrap()));
1441		let key_rs512 = Key::generate(Algorithm::RS512, Some(crate::KeyId::decode("test-id").unwrap()));
1442
1443		for key in [key_rs256, key_rs384, key_rs512] {
1444			test_asymmetric_key(key);
1445		}
1446	}
1447
1448	#[test]
1449	fn test_rsa_pss_asymmetric_algorithms() {
1450		let key_ps256 = Key::generate(Algorithm::PS256, Some(crate::KeyId::decode("test-id").unwrap()));
1451		let key_ps384 = Key::generate(Algorithm::PS384, Some(crate::KeyId::decode("test-id").unwrap()));
1452		let key_ps512 = Key::generate(Algorithm::PS512, Some(crate::KeyId::decode("test-id").unwrap()));
1453
1454		for key in [key_ps256, key_ps384, key_ps512] {
1455			test_asymmetric_key(key);
1456		}
1457	}
1458
1459	#[test]
1460	fn test_ec_asymmetric_algorithms() {
1461		let key_es256 = Key::generate(Algorithm::ES256, Some(crate::KeyId::decode("test-id").unwrap()));
1462		let key_es384 = Key::generate(Algorithm::ES384, Some(crate::KeyId::decode("test-id").unwrap()));
1463
1464		for key in [key_es256, key_es384] {
1465			test_asymmetric_key(key);
1466		}
1467	}
1468
1469	#[test]
1470	fn test_ed_asymmetric_algorithms() {
1471		let key_eddsa = Key::generate(Algorithm::EdDSA, Some(crate::KeyId::decode("test-id").unwrap()));
1472
1473		test_asymmetric_key(key_eddsa);
1474	}
1475
1476	fn test_asymmetric_key(key: crate::Result<Key>) {
1477		assert!(key.is_ok());
1478		let key = key.unwrap();
1479
1480		let claims = create_test_claims();
1481		let token = key.sign(&claims).unwrap();
1482
1483		let private_verified_claims = key.verify(&token).unwrap();
1484		assert_eq!(
1485			private_verified_claims.root, claims.root,
1486			"validation using private key"
1487		);
1488
1489		let public_verified_claims = key.to_public().unwrap().verify(&token).unwrap();
1490		assert_eq!(public_verified_claims.root, claims.root, "validation using public key");
1491	}
1492
1493	#[test]
1494	fn test_cross_algorithm_verification_fails() {
1495		let key_256 = Key::generate(Algorithm::HS256, Some(crate::KeyId::decode("test-id").unwrap()));
1496		assert!(key_256.is_ok());
1497		let key_256 = key_256.unwrap();
1498
1499		let key_384 = Key::generate(Algorithm::HS384, Some(crate::KeyId::decode("test-id").unwrap()));
1500		assert!(key_384.is_ok());
1501		let key_384 = key_384.unwrap();
1502
1503		let claims = create_test_claims();
1504		let token = key_256.sign(&claims).unwrap();
1505
1506		// Different algorithm should fail verification
1507		let result = key_384.verify(&token);
1508		assert!(result.is_err());
1509	}
1510
1511	#[test]
1512	fn test_asymmetric_cross_algorithm_verification_fails() {
1513		let key_rs256 = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
1514		assert!(key_rs256.is_ok());
1515		let key_rs256 = key_rs256.unwrap();
1516
1517		let key_ps256 = Key::generate(Algorithm::PS256, Some(crate::KeyId::decode("test-id").unwrap()));
1518		assert!(key_ps256.is_ok());
1519		let key_ps256 = key_ps256.unwrap();
1520
1521		let claims = create_test_claims();
1522		let token = key_rs256.sign(&claims).unwrap();
1523
1524		// Different algorithm should fail verification
1525		let private_result = key_ps256.verify(&token);
1526		let public_result = key_ps256.to_public().unwrap().verify(&token);
1527		assert!(private_result.is_err());
1528		assert!(public_result.is_err());
1529	}
1530
1531	#[test]
1532	fn test_rsa_pkcs1_public_key_conversion() {
1533		let key = Key::generate(Algorithm::RS256, Some(crate::KeyId::decode("test-id").unwrap()));
1534		assert!(key.is_ok());
1535		let key = key.unwrap();
1536
1537		assert!(key.operations.contains(&KeyOperation::Sign));
1538		assert!(key.operations.contains(&KeyOperation::Verify));
1539
1540		let public_key = key.to_public().unwrap();
1541		assert!(!public_key.operations.contains(&KeyOperation::Sign));
1542		assert!(public_key.operations.contains(&KeyOperation::Verify));
1543
1544		match &key.material {
1545			KeyMaterial::RSA { public, private } => {
1546				assert!(private.is_some());
1547				assert_eq!(public.n.len(), 256);
1548				assert_eq!(public.e.len(), 3);
1549
1550				match &public_key.material {
1551					KeyMaterial::RSA {
1552						public: guest_public,
1553						private: public_private,
1554					} => {
1555						assert!(public_private.is_none());
1556						assert_eq!(public.n, guest_public.n);
1557						assert_eq!(public.e, guest_public.e);
1558					}
1559					_ => panic!("Expected public key to be an RSA key"),
1560				}
1561			}
1562			_ => panic!("Expected private key to be an RSA key"),
1563		}
1564	}
1565
1566	#[test]
1567	fn test_rsa_pss_public_key_conversion() {
1568		let key = Key::generate(Algorithm::PS384, Some(crate::KeyId::decode("test-id").unwrap()));
1569		assert!(key.is_ok());
1570		let key = key.unwrap();
1571
1572		assert!(key.operations.contains(&KeyOperation::Sign));
1573		assert!(key.operations.contains(&KeyOperation::Verify));
1574
1575		let public_key = key.to_public().unwrap();
1576		assert!(!public_key.operations.contains(&KeyOperation::Sign));
1577		assert!(public_key.operations.contains(&KeyOperation::Verify));
1578
1579		match &key.material {
1580			KeyMaterial::RSA { public, private } => {
1581				assert!(private.is_some());
1582				assert_eq!(public.n.len(), 256);
1583				assert_eq!(public.e.len(), 3);
1584
1585				match &public_key.material {
1586					KeyMaterial::RSA {
1587						public: guest_public,
1588						private: public_private,
1589					} => {
1590						assert!(public_private.is_none());
1591						assert_eq!(public.n, guest_public.n);
1592						assert_eq!(public.e, guest_public.e);
1593					}
1594					_ => panic!("Expected public key to be an RSA key"),
1595				}
1596			}
1597			_ => panic!("Expected private key to be an RSA key"),
1598		}
1599	}
1600
1601	#[test]
1602	fn test_base64url_serialization() {
1603		let key = create_test_key();
1604		let json = serde_json::to_string(&key).unwrap();
1605
1606		// Check that the secret is base64url encoded without padding
1607		let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1608		let k_value = parsed["k"].as_str().unwrap();
1609
1610		// Base64url should not contain padding characters
1611		assert!(!k_value.contains('='));
1612		assert!(!k_value.contains('+'));
1613		assert!(!k_value.contains('/'));
1614
1615		// Verify it decodes correctly
1616		let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
1617			.decode(k_value)
1618			.unwrap();
1619
1620		if let KeyMaterial::OCT {
1621			secret: original_secret,
1622		} = &key.material
1623		{
1624			assert_eq!(decoded, *original_secret);
1625		} else {
1626			panic!("Expected both keys to be OCT variant");
1627		}
1628	}
1629
1630	#[test]
1631	fn test_backwards_compatibility_unpadded_base64url() {
1632		// Create a JSON with unpadded base64url (new format)
1633		let unpadded_json = r#"{"kty":"oct","alg":"HS256","key_ops":["sign","verify"],"k":"dGVzdC1zZWNyZXQtdGhhdC1pcy1sb25nLWVub3VnaC1mb3ItaG1hYy1zaGEyNTY","kid":"test-key-1"}"#;
1634
1635		// Should be able to deserialize new format
1636		let key: Key = serde_json::from_str(unpadded_json).unwrap();
1637		assert_eq!(key.algorithm, Algorithm::HS256);
1638		assert_eq!(key.kid, Some(crate::KeyId::decode("test-key-1").unwrap()));
1639
1640		if let KeyMaterial::OCT { secret } = &key.material {
1641			assert_eq!(secret, b"test-secret-that-is-long-enough-for-hmac-sha256");
1642		} else {
1643			panic!("Expected key to be OCT variant");
1644		}
1645	}
1646
1647	#[test]
1648	fn test_backwards_compatibility_padded_base64url() {
1649		// Create a JSON with padded base64url (old format) - same secret but with padding
1650		let padded_json = r#"{"kty":"oct","alg":"HS256","key_ops":["sign","verify"],"k":"dGVzdC1zZWNyZXQtdGhhdC1pcy1sb25nLWVub3VnaC1mb3ItaG1hYy1zaGEyNTY=","kid":"test-key-1"}"#;
1651
1652		// Should be able to deserialize old format for backwards compatibility
1653		let key: Key = serde_json::from_str(padded_json).unwrap();
1654		assert_eq!(key.algorithm, Algorithm::HS256);
1655		assert_eq!(key.kid, Some(crate::KeyId::decode("test-key-1").unwrap()));
1656
1657		if let KeyMaterial::OCT { secret } = &key.material {
1658			assert_eq!(secret, b"test-secret-that-is-long-enough-for-hmac-sha256");
1659		} else {
1660			panic!("Expected key to be OCT variant");
1661		}
1662	}
1663
1664	// Tests that Rust can load keys generated by the JS @moq/auth package
1665	// and verify tokens signed by JS.
1666	//
1667	// Generated with: bun -e 'import { generate } from "./js/auth/src/generate.ts"; ...'
1668	// See js/auth/src/interop.test.ts for the JS-side counterpart.
1669
1670	/// JS-generated HS256 key (from @moq/auth generate("HS256", "js-test-key"))
1671	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":[]}"#;
1672
1673	/// JS-generated HS256 token (from @moq/auth sign(key, {root:"live", publish:["camera1"], subscribe:["camera1","camera2"]}))
1674	const JS_HS256_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImpzLXRlc3Qta2V5In0.eyJyb290IjoibGl2ZSIsInB1Ymxpc2giOlsiY2FtZXJhMSJdLCJzdWJzY3JpYmUiOlsiY2FtZXJhMSIsImNhbWVyYTIiXSwiaWF0IjoxNzc1MTc2NzU0fQ.DxrRkpYDd7Cc215qstY4RfnB7hRvc8RG61YWO4UwyWg";
1675
1676	/// A token @moq/token minted before patterns, with `put` and `get` prefix lists.
1677	const JS_HS256_LEGACY_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImpzLXRlc3Qta2V5In0.eyJyb290IjoibGl2ZSIsInB1dCI6WyJjYW1lcmExIl0sImdldCI6WyJjYW1lcmExIiwiY2FtZXJhMiJdLCJpYXQiOjE3NzUxNzY3NTR9.tHNQtHh_HCIKxXOexDCM7AkjqWzbULLZzjEckfOGRfY";
1678
1679	/// JS-generated EdDSA private key (from @moq/auth generate("EdDSA", "js-eddsa-key"))
1680	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":[]}"#;
1681
1682	/// JS-generated EdDSA public key (from @moq/auth toPublicKey(key))
1683	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"]}"#;
1684
1685	/// JS-generated EdDSA token (from @moq/auth sign(key, {root:"stream", publish:["video/**"]}))
1686	const JS_EDDSA_TOKEN: &str = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImpzLWVkZHNhLWtleSJ9.eyJyb290Ijoic3RyZWFtIiwicHVibGlzaCI6WyJ2aWRlby8qKiJdLCJpYXQiOjE3NzUxNzY3NTZ9.Vq00rznemlwxsGLdAR8EJs1J8cgXhaiuEiCysmCxnWbDemIFxS6kTNv3kp6LKnokp-mpHiRpO24Nv7b47jcIAQ";
1687
1688	#[test]
1689	fn test_js_hs256_key_load() {
1690		let key = Key::from_str(JS_HS256_KEY).unwrap();
1691		assert_eq!(key.algorithm, Algorithm::HS256);
1692		assert_eq!(key.kid, Some(crate::KeyId::decode("js-test-key").unwrap()));
1693	}
1694
1695	#[test]
1696	fn test_js_hs256_token_verify() {
1697		let key = Key::from_str(JS_HS256_KEY).unwrap();
1698		let claims = key.verify(JS_HS256_TOKEN).unwrap();
1699		assert_eq!(claims.root, "live");
1700		assert_eq!(claims.publish, patterns(&["camera1"]));
1701		assert_eq!(claims.subscribe, patterns(&["camera1", "camera2"]));
1702	}
1703
1704	#[test]
1705	fn test_js_legacy_prefix_token_verifies_as_subtrees() {
1706		let key = Key::from_str(JS_HS256_KEY).unwrap();
1707		let claims = key.verify(JS_HS256_LEGACY_TOKEN).unwrap();
1708		assert_eq!(claims.root, "live");
1709		assert_eq!(claims.publish, patterns(&["camera1/**"]));
1710		assert_eq!(claims.subscribe, patterns(&["camera1/**", "camera2/**"]));
1711	}
1712
1713	#[test]
1714	fn test_legacy_scoped_key_signs_within_its_prefixes() {
1715		// A key minted by moq-token-cli with `--root demo --put room`.
1716		let json = r#"{"kty":"oct","alg":"HS256","key_ops":["sign","verify"],"k":"Fp8kipWUJeUFqeSqWym_tRC_tyI8z-QpqopIGrbrD68","scope":{"root":"demo","put":["room"]}}"#;
1717		let key = Key::from_str(json).unwrap();
1718		assert_eq!(key.scope.as_ref().unwrap().publish, patterns(&["room/**"]));
1719
1720		let inside = Claims {
1721			root: "demo/room".into(),
1722			publish: patterns(&["alice"]),
1723			..Default::default()
1724		};
1725		let outside = Claims {
1726			root: "demo".into(),
1727			publish: patterns(&["lobby/**"]),
1728			..Default::default()
1729		};
1730		assert!(key.verify(&key.sign(&inside).unwrap()).is_ok());
1731		assert!(matches!(key.sign(&outside), Err(crate::Error::ScopeExceeded)));
1732
1733		// Written back the way it was read, so the old CLI still loads it.
1734		assert!(
1735			serde_json::to_string(&key)
1736				.unwrap()
1737				.contains(r#""scope":{"root":"demo","put":["room"]}"#)
1738		);
1739	}
1740
1741	#[test]
1742	fn test_js_hs256_sign_and_roundtrip() {
1743		let key = Key::from_str(JS_HS256_KEY).unwrap();
1744		let claims = Claims {
1745			root: "rust-test".to_string(),
1746			publish: patterns(&["pub1/**"]),
1747			subscribe: patterns(&["sub1/**"]),
1748			..Default::default()
1749		};
1750		let token = key.sign(&claims).unwrap();
1751		let verified = key.verify(&token).unwrap();
1752		assert_eq!(verified.root, "rust-test");
1753		assert_eq!(verified.publish, patterns(&["pub1/**"]));
1754	}
1755
1756	#[test]
1757	fn test_js_eddsa_key_load() {
1758		let private_key = Key::from_str(JS_EDDSA_PRIVATE_KEY).unwrap();
1759		assert_eq!(private_key.algorithm, Algorithm::EdDSA);
1760		assert!(matches!(private_key.material, KeyMaterial::OKP { .. }));
1761
1762		let public_key = Key::from_str(JS_EDDSA_PUBLIC_KEY).unwrap();
1763		assert_eq!(public_key.algorithm, Algorithm::EdDSA);
1764	}
1765
1766	#[test]
1767	fn test_js_eddsa_token_verify_with_private_key() {
1768		let key = Key::from_str(JS_EDDSA_PRIVATE_KEY).unwrap();
1769		let claims = key.verify(JS_EDDSA_TOKEN).unwrap();
1770		assert_eq!(claims.root, "stream");
1771		assert_eq!(claims.publish, patterns(&["video/**"]));
1772	}
1773
1774	#[test]
1775	fn test_js_eddsa_token_verify_with_public_key() {
1776		let key = Key::from_str(JS_EDDSA_PUBLIC_KEY).unwrap();
1777		let claims = key.verify(JS_EDDSA_TOKEN).unwrap();
1778		assert_eq!(claims.root, "stream");
1779		assert_eq!(claims.publish, patterns(&["video/**"]));
1780	}
1781
1782	#[test]
1783	fn test_js_token_wrong_key_fails() {
1784		// Generate a different HS256 key
1785		let wrong_key = Key::generate(Algorithm::HS256, None).unwrap();
1786		let result = wrong_key.verify(JS_HS256_TOKEN);
1787		assert!(result.is_err());
1788	}
1789
1790	#[test]
1791	fn test_js_eddsa_token_wrong_key_fails() {
1792		// Try verifying EdDSA token with the HS256 key
1793		let wrong_key = Key::from_str(JS_HS256_KEY).unwrap();
1794		let result = wrong_key.verify(JS_EDDSA_TOKEN);
1795		assert!(result.is_err());
1796	}
1797
1798	#[test]
1799	fn test_file_io_base64url() {
1800		let key = create_test_key();
1801		let temp_dir = std::env::temp_dir();
1802		let temp_path = temp_dir.join("test_jwk.key");
1803
1804		// Write key to file as base64url
1805		key.to_file(&temp_path).unwrap();
1806
1807		// Read file contents
1808		let contents = std::fs::read_to_string(&temp_path).unwrap();
1809
1810		// Should be base64url encoded
1811		assert!(!contents.contains('{'));
1812		assert!(!contents.contains('}'));
1813		assert!(!contents.contains('"'));
1814
1815		// Decode and verify it's valid JSON
1816		let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
1817			.decode(&contents)
1818			.unwrap();
1819		let json_str = String::from_utf8(decoded).unwrap();
1820		let _: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1821
1822		// Read key back from file
1823		let loaded_key = Key::from_file(&temp_path).unwrap();
1824		assert_eq!(loaded_key.algorithm, key.algorithm);
1825		assert_eq!(loaded_key.operations, key.operations);
1826		assert_eq!(loaded_key.kid, key.kid);
1827
1828		if let (
1829			KeyMaterial::OCT {
1830				secret: original_secret,
1831			},
1832			KeyMaterial::OCT { secret: loaded_secret },
1833		) = (&key.material, &loaded_key.material)
1834		{
1835			assert_eq!(loaded_secret, original_secret);
1836		} else {
1837			panic!("Expected both keys to be OCT variant");
1838		}
1839
1840		// Clean up
1841		std::fs::remove_file(temp_path).ok();
1842	}
1843
1844	#[test]
1845	fn test_file_io_raw_json() {
1846		let key = create_test_key();
1847		let temp_dir = std::env::temp_dir();
1848		let temp_path = temp_dir.join("test_jwk_raw_json.key");
1849
1850		// Write key as raw JSON (backwards compat format)
1851		let json = serde_json::to_string(&key).unwrap();
1852		std::fs::write(&temp_path, &json).unwrap();
1853
1854		// Verify it looks like JSON
1855		assert!(json.starts_with('{'));
1856
1857		// Load via from_file (should auto-detect JSON)
1858		let loaded_key = Key::from_file(&temp_path).unwrap();
1859		assert_eq!(loaded_key.algorithm, key.algorithm);
1860		assert_eq!(loaded_key.operations, key.operations);
1861		assert_eq!(loaded_key.kid, key.kid);
1862
1863		if let (
1864			KeyMaterial::OCT {
1865				secret: original_secret,
1866			},
1867			KeyMaterial::OCT { secret: loaded_secret },
1868		) = (&key.material, &loaded_key.material)
1869		{
1870			assert_eq!(loaded_secret, original_secret);
1871		} else {
1872			panic!("Expected both keys to be OCT variant");
1873		}
1874
1875		// Clean up
1876		std::fs::remove_file(temp_path).ok();
1877	}
1878
1879	#[cfg(unix)]
1880	mod permissions {
1881		use super::*;
1882		use std::os::unix::fs::PermissionsExt;
1883
1884		fn temp_path(name: &str) -> std::path::PathBuf {
1885			let unique = SystemTime::now()
1886				.duration_since(SystemTime::UNIX_EPOCH)
1887				.unwrap()
1888				.as_nanos();
1889			std::env::temp_dir().join(format!("test_perms_{name}_{unique}.jwk"))
1890		}
1891
1892		fn mode(path: &std::path::Path) -> u32 {
1893			std::fs::metadata(path).unwrap().permissions().mode() & 0o777
1894		}
1895
1896		#[test]
1897		fn private_key_is_owner_only() {
1898			let path = temp_path("private");
1899			create_test_key().to_file(&path).unwrap();
1900			assert_eq!(mode(&path), 0o600);
1901			std::fs::remove_file(&path).ok();
1902		}
1903
1904		#[test]
1905		fn private_key_tightens_existing_file() {
1906			let path = temp_path("existing");
1907			std::fs::write(&path, "stale").unwrap();
1908			std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
1909
1910			create_test_key().to_file(&path).unwrap();
1911			assert_eq!(mode(&path), 0o600);
1912
1913			// The old contents are gone, not just hidden behind the new mode.
1914			let contents = std::fs::read_to_string(&path).unwrap();
1915			assert!(!contents.contains("stale"));
1916			std::fs::remove_file(&path).ok();
1917		}
1918
1919		#[test]
1920		fn public_key_keeps_default_permissions() {
1921			let path = temp_path("public");
1922			std::fs::write(&path, "").unwrap();
1923			std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
1924
1925			let public = Key::generate(Algorithm::ES256, None).unwrap().to_public().unwrap();
1926			public.to_file(&path).unwrap();
1927			assert_eq!(mode(&path), 0o644);
1928			std::fs::remove_file(&path).ok();
1929		}
1930	}
1931}