1use serde::{Deserialize, Serialize};
2
3use crate::types::KeyId;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[non_exhaustive]
7pub struct WellKnownPasetoDocument {
8 pub issuer: String,
9 pub version: String,
10 pub keys: Vec<WellKnownPasetoKey>,
11 pub cache_ttl_seconds: u64,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[non_exhaustive]
16pub struct WellKnownPasetoKey {
17 pub kid: KeyId,
18 pub public_key_hex: String,
19 pub status: WellKnownKeyStatus,
20 #[serde(with = "time::serde::rfc3339")]
21 pub created_at: time::OffsetDateTime,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26#[non_exhaustive]
27pub enum WellKnownKeyStatus {
28 Active,
29 Retiring,
30 Revoked,
31}
32
33#[cfg(test)]
34#[allow(clippy::unwrap_used)]
35mod tests {
36 use super::*;
37
38 const SAMPLE_JSON: &str = r#"{
39 "issuer": "accounts.ppoppo.com",
40 "version": "v4.public",
41 "keys": [
42 {
43 "kid": "key-001",
44 "public_key_hex": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
45 "status": "active",
46 "created_at": "2026-01-01T00:00:00Z"
47 },
48 {
49 "kid": "key-002",
50 "public_key_hex": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
51 "status": "retiring",
52 "created_at": "2025-06-01T00:00:00Z"
53 }
54 ],
55 "cache_ttl_seconds": 3600
56 }"#;
57
58 #[test]
59 fn deserialize_well_known_document() {
60 let doc: WellKnownPasetoDocument = serde_json::from_str(SAMPLE_JSON).unwrap();
61
62 assert_eq!(doc.issuer, "accounts.ppoppo.com");
63 assert_eq!(doc.version, "v4.public");
64 assert_eq!(doc.keys.len(), 2);
65 assert_eq!(doc.cache_ttl_seconds, 3600);
66 }
67
68 #[test]
69 fn deserialize_key_fields() {
70 let doc: WellKnownPasetoDocument = serde_json::from_str(SAMPLE_JSON).unwrap();
71
72 let key = &doc.keys[0];
73 assert_eq!(key.kid.to_string(), "key-001");
74 assert_eq!(key.status, WellKnownKeyStatus::Active);
75
76 let retiring = &doc.keys[1];
77 assert_eq!(retiring.status, WellKnownKeyStatus::Retiring);
78 }
79
80 #[test]
81 fn serde_roundtrip() {
82 let doc: WellKnownPasetoDocument = serde_json::from_str(SAMPLE_JSON).unwrap();
83 let json = serde_json::to_string(&doc).unwrap();
84 let doc2: WellKnownPasetoDocument = serde_json::from_str(&json).unwrap();
85 assert_eq!(doc, doc2);
86 }
87
88 #[test]
89 fn deserialize_revoked_status() {
90 let json = r#"{
91 "kid": "key-003",
92 "public_key_hex": "cc",
93 "status": "revoked",
94 "created_at": "2024-01-01T00:00:00Z"
95 }"#;
96 let key: WellKnownPasetoKey = serde_json::from_str(json).unwrap();
97 assert_eq!(key.status, WellKnownKeyStatus::Revoked);
98 }
99
100 #[cfg(feature = "token")]
101 #[test]
102 fn convert_well_known_key_to_public_key() {
103 use crate::token::PublicKey;
104
105 let doc: WellKnownPasetoDocument = serde_json::from_str(SAMPLE_JSON).unwrap();
106 let key = &doc.keys[0];
107
108 let result = PublicKey::try_from(key);
109 assert!(result.is_ok());
110 assert_eq!(result.unwrap().as_bytes().len(), 32);
111 }
112}