1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12use crate::badge::BadgeStatus;
13use crate::fingerprint::CertFingerprint;
14use crate::types::AnsName;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[non_exhaustive]
21pub enum VerificationTier {
22 BadgeOnly,
24 StatusTokenVerified,
27 FullScitt,
30}
31
32impl VerificationTier {
33 pub fn is_scitt(&self) -> bool {
36 matches!(self, Self::StatusTokenVerified | Self::FullScitt)
37 }
38
39 pub fn has_receipt(&self) -> bool {
42 matches!(self, Self::FullScitt)
43 }
44}
45
46impl std::fmt::Display for VerificationTier {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::BadgeOnly => write!(f, "BadgeOnly"),
50 Self::StatusTokenVerified => write!(f, "StatusTokenVerified"),
51 Self::FullScitt => write!(f, "FullScitt"),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[non_exhaustive]
62pub enum CertType {
63 #[serde(rename = "X509-DV-SERVER")]
65 X509DvServer,
66 #[serde(rename = "X509-OV-CLIENT")]
68 X509OvClient,
69}
70
71impl std::str::FromStr for CertType {
72 type Err = String;
73
74 fn from_str(s: &str) -> Result<Self, Self::Err> {
75 match s {
76 "X509-DV-SERVER" => Ok(Self::X509DvServer),
77 "X509-OV-CLIENT" => Ok(Self::X509OvClient),
78 other => Err(format!("unknown cert_type: {other}")),
79 }
80 }
81}
82
83impl std::fmt::Display for CertType {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::X509DvServer => write!(f, "X509-DV-SERVER"),
87 Self::X509OvClient => write!(f, "X509-OV-CLIENT"),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
97#[non_exhaustive]
98pub struct CertEntry {
99 pub fingerprint: CertFingerprint,
101 pub cert_type: CertType,
103}
104
105impl CertEntry {
106 pub fn new(fingerprint: CertFingerprint, cert_type: CertType) -> Self {
108 Self {
109 fingerprint,
110 cert_type,
111 }
112 }
113}
114
115impl StatusTokenPayload {
116 #[allow(clippy::too_many_arguments)]
118 pub fn new(
119 agent_id: Uuid,
120 status: BadgeStatus,
121 iat: i64,
122 exp: i64,
123 ans_name: AnsName,
124 valid_identity_certs: Vec<CertEntry>,
125 valid_server_certs: Vec<CertEntry>,
126 metadata_hashes: BTreeMap<String, String>,
127 ) -> Self {
128 Self {
129 agent_id,
130 status,
131 iat,
132 exp,
133 ans_name,
134 valid_identity_certs,
135 valid_server_certs,
136 metadata_hashes,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
151#[non_exhaustive]
152pub struct StatusTokenPayload {
153 pub agent_id: Uuid,
155 pub status: BadgeStatus,
157 pub iat: i64,
159 pub exp: i64,
161 pub ans_name: AnsName,
165 pub valid_identity_certs: Vec<CertEntry>,
167 pub valid_server_certs: Vec<CertEntry>,
169 pub metadata_hashes: BTreeMap<String, String>,
172}
173
174#[allow(clippy::unwrap_used, clippy::expect_used)]
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn verification_tier_is_scitt() {
181 assert!(!VerificationTier::BadgeOnly.is_scitt());
182 assert!(VerificationTier::StatusTokenVerified.is_scitt());
183 assert!(VerificationTier::FullScitt.is_scitt());
184 }
185
186 #[test]
187 fn verification_tier_has_receipt() {
188 assert!(!VerificationTier::BadgeOnly.has_receipt());
189 assert!(!VerificationTier::StatusTokenVerified.has_receipt());
190 assert!(VerificationTier::FullScitt.has_receipt());
191 }
192
193 #[test]
194 fn verification_tier_display() {
195 assert_eq!(VerificationTier::BadgeOnly.to_string(), "BadgeOnly");
196 assert_eq!(
197 VerificationTier::StatusTokenVerified.to_string(),
198 "StatusTokenVerified"
199 );
200 assert_eq!(VerificationTier::FullScitt.to_string(), "FullScitt");
201 }
202
203 #[test]
204 fn verification_tier_serde_roundtrip() {
205 for tier in [
206 VerificationTier::BadgeOnly,
207 VerificationTier::StatusTokenVerified,
208 VerificationTier::FullScitt,
209 ] {
210 let json = serde_json::to_string(&tier).unwrap();
211 let deserialized: VerificationTier = serde_json::from_str(&json).unwrap();
212 assert_eq!(tier, deserialized);
213 }
214 }
215
216 #[test]
217 fn verification_tier_equality_and_hash() {
218 use std::collections::HashSet;
219 let mut set = HashSet::new();
220 set.insert(VerificationTier::BadgeOnly);
221 set.insert(VerificationTier::StatusTokenVerified);
222 set.insert(VerificationTier::FullScitt);
223 set.insert(VerificationTier::BadgeOnly); assert_eq!(set.len(), 3);
225 }
226
227 #[test]
228 fn verification_tier_clone_and_copy() {
229 let tier = VerificationTier::FullScitt;
230 let cloned = tier;
231 assert_eq!(tier, cloned); }
233
234 #[test]
235 fn cert_entry_new() {
236 let fp = CertFingerprint::from_bytes([
237 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2,
238 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4,
239 0xe5, 0xf6, 0xa1, 0xb2,
240 ]);
241 let entry = CertEntry::new(fp.clone(), CertType::X509DvServer);
242 assert_eq!(entry.fingerprint, fp);
243 assert_eq!(entry.cert_type, CertType::X509DvServer);
244 }
245
246 #[test]
247 fn status_token_payload_serde_roundtrip() {
248 let fp = CertFingerprint::from_bytes([
249 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2,
250 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4,
251 0xe5, 0xf6, 0xa1, 0xb2,
252 ]);
253
254 let payload = StatusTokenPayload {
255 agent_id: Uuid::nil(),
256 status: BadgeStatus::Active,
257 iat: 1_700_000_000,
258 exp: 1_700_003_600,
259 ans_name: AnsName::parse("ans://v1.0.0.agent.example.com").unwrap(),
260 valid_identity_certs: vec![CertEntry::new(fp.clone(), CertType::X509OvClient)],
261 valid_server_certs: vec![CertEntry::new(fp, CertType::X509DvServer)],
262 metadata_hashes: BTreeMap::from([("key".to_string(), "value".to_string())]),
263 };
264
265 let json = serde_json::to_string(&payload).unwrap();
266 let deserialized: StatusTokenPayload = serde_json::from_str(&json).unwrap();
267
268 assert_eq!(deserialized.agent_id, payload.agent_id);
269 assert_eq!(deserialized.status, payload.status);
270 assert_eq!(deserialized.iat, payload.iat);
271 assert_eq!(deserialized.exp, payload.exp);
272 assert_eq!(deserialized.ans_name, payload.ans_name);
273 assert_eq!(deserialized.valid_identity_certs.len(), 1);
274 assert_eq!(deserialized.valid_server_certs.len(), 1);
275 assert_eq!(deserialized.metadata_hashes.len(), 1);
276 }
277
278 #[test]
279 fn status_token_payload_empty_cert_arrays() {
280 let payload = StatusTokenPayload {
281 agent_id: Uuid::nil(),
282 status: BadgeStatus::Warning,
283 iat: 0,
284 exp: 3600,
285 ans_name: AnsName::parse("ans://v0.1.0.test.example.com").unwrap(),
286 valid_identity_certs: vec![],
287 valid_server_certs: vec![],
288 metadata_hashes: BTreeMap::new(),
289 };
290
291 let json = serde_json::to_string(&payload).unwrap();
292 let deserialized: StatusTokenPayload = serde_json::from_str(&json).unwrap();
293 assert!(deserialized.valid_identity_certs.is_empty());
294 assert!(deserialized.valid_server_certs.is_empty());
295 assert!(deserialized.metadata_hashes.is_empty());
296 }
297
298 #[test]
299 fn status_token_payload_all_statuses() {
300 for status in [
301 BadgeStatus::Active,
302 BadgeStatus::Warning,
303 BadgeStatus::Deprecated,
304 BadgeStatus::Expired,
305 BadgeStatus::Revoked,
306 ] {
307 let payload = StatusTokenPayload {
308 agent_id: Uuid::nil(),
309 status,
310 iat: 0,
311 exp: 3600,
312 ans_name: AnsName::parse("ans://v1.0.0.test.example.com").unwrap(),
313 valid_identity_certs: vec![],
314 valid_server_certs: vec![],
315 metadata_hashes: BTreeMap::new(),
316 };
317 let json = serde_json::to_string(&payload).unwrap();
318 let deserialized: StatusTokenPayload = serde_json::from_str(&json).unwrap();
319 assert_eq!(deserialized.status, status);
320 }
321 }
322
323 #[test]
324 fn metadata_hashes_deterministic_ordering() {
325 let mut map = BTreeMap::new();
326 map.insert("zebra".to_string(), "hash_z".to_string());
327 map.insert("alpha".to_string(), "hash_a".to_string());
328 map.insert("middle".to_string(), "hash_m".to_string());
329
330 let keys: Vec<&String> = map.keys().collect();
331 assert_eq!(keys, vec!["alpha", "middle", "zebra"]);
332 }
333}