1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
10#[non_exhaustive]
11pub enum BadgeStatus {
12 Active,
14 Warning,
16 Deprecated,
18 Expired,
20 Revoked,
22}
23
24impl BadgeStatus {
25 pub fn is_valid_for_connection(&self) -> bool {
27 matches!(self, Self::Active | Self::Warning | Self::Deprecated)
28 }
29
30 pub fn is_active(&self) -> bool {
32 matches!(self, Self::Active | Self::Warning)
33 }
34
35 pub fn should_reject(&self) -> bool {
37 matches!(self, Self::Expired | Self::Revoked)
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
46#[non_exhaustive]
47pub enum EventType {
48 AgentRegistered,
50 AgentRenewed,
52 AgentDeprecated,
54 AgentRevoked,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61#[non_exhaustive]
62pub struct Badge {
63 pub status: BadgeStatus,
65 pub payload: BadgePayload,
67 pub schema_version: String,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub signature: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub merkle_proof: Option<MerkleProof>,
75}
76
77impl Badge {
78 pub fn agent_name(&self) -> &str {
80 &self.payload.producer.event.ans_name
81 }
82
83 pub fn agent_host(&self) -> &str {
85 &self.payload.producer.event.agent.host
86 }
87
88 pub fn agent_version(&self) -> &str {
90 &self.payload.producer.event.agent.version
91 }
92
93 pub fn server_cert_fingerprint(&self) -> &str {
95 &self
96 .payload
97 .producer
98 .event
99 .attestations
100 .server_cert
101 .fingerprint
102 }
103
104 pub fn identity_cert_fingerprint(&self) -> &str {
106 &self
107 .payload
108 .producer
109 .event
110 .attestations
111 .identity_cert
112 .fingerprint
113 }
114
115 pub fn agent_id(&self) -> Uuid {
117 self.payload.producer.event.ans_id
118 }
119
120 pub fn event_type(&self) -> EventType {
122 self.payload.producer.event.event_type
123 }
124
125 pub fn is_valid(&self) -> bool {
127 self.status.is_valid_for_connection()
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134#[non_exhaustive]
135pub struct BadgePayload {
136 pub log_id: Uuid,
138 pub producer: Producer,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145#[non_exhaustive]
146pub struct Producer {
147 pub event: AgentEvent,
149 pub key_id: String,
151 pub signature: String,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158#[non_exhaustive]
159pub struct AgentEvent {
160 pub ans_id: Uuid,
162 pub ans_name: String,
164 pub event_type: EventType,
166 pub agent: AgentInfo,
168 pub attestations: Attestations,
170 pub expires_at: DateTime<Utc>,
172 pub issued_at: DateTime<Utc>,
174 pub ra_id: String,
176 pub timestamp: DateTime<Utc>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182#[non_exhaustive]
183pub struct AgentInfo {
184 pub host: String,
186 pub name: String,
188 pub version: String,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195#[non_exhaustive]
196pub struct Attestations {
197 pub domain_validation: String,
199 pub identity_cert: CertAttestation,
201 pub server_cert: CertAttestation,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207#[non_exhaustive]
208pub struct CertAttestation {
209 pub fingerprint: String,
211 #[serde(rename = "type")]
213 pub cert_type: String,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219#[non_exhaustive]
220pub struct MerkleProof {
221 pub leaf_hash: String,
223 pub leaf_index: u64,
225 pub path: Vec<String>,
227 pub root_hash: String,
229 pub root_signature: String,
231 pub tree_size: u64,
233 pub tree_version: u64,
235}
236
237#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_badge_status_valid_for_connection() {
244 assert!(BadgeStatus::Active.is_valid_for_connection());
245 assert!(BadgeStatus::Warning.is_valid_for_connection());
246 assert!(BadgeStatus::Deprecated.is_valid_for_connection());
247 assert!(!BadgeStatus::Expired.is_valid_for_connection());
248 assert!(!BadgeStatus::Revoked.is_valid_for_connection());
249 }
250
251 #[test]
252 fn test_badge_status_should_reject() {
253 assert!(!BadgeStatus::Active.should_reject());
254 assert!(!BadgeStatus::Warning.should_reject());
255 assert!(!BadgeStatus::Deprecated.should_reject());
256 assert!(BadgeStatus::Expired.should_reject());
257 assert!(BadgeStatus::Revoked.should_reject());
258 }
259
260 #[test]
261 fn test_deserialize_badge() {
262 let json = r#"{
263 "status": "ACTIVE",
264 "payload": {
265 "logId": "019be7f3-5720-77c9-9672-adae3394502f",
266 "producer": {
267 "event": {
268 "ansId": "7b93c61c-e261-488c-89a3-f948119be0a0",
269 "ansName": "ans://v1.0.0.agent.example.com",
270 "eventType": "AGENT_REGISTERED",
271 "agent": {
272 "host": "agent.example.com",
273 "name": "Test Agent",
274 "version": "v1.0.0"
275 },
276 "attestations": {
277 "domainValidation": "ACME-DNS-01",
278 "identityCert": {
279 "fingerprint": "SHA256:aebdc9da0c20d6d5e4999a773839095ed050a9d7252bf212056fddc0c38f3496",
280 "type": "X509-OV-CLIENT"
281 },
282 "serverCert": {
283 "fingerprint": "SHA256:e7b64d16f42055d6faf382a43dc35b98be76aba0db145a904b590a034b33b904",
284 "type": "X509-DV-SERVER"
285 }
286 },
287 "expiresAt": "2027-01-22T22:58:52.000000Z",
288 "issuedAt": "2026-01-22T22:58:51.839533Z",
289 "raId": "gd-ra-us-west-2-ote-db21525-9ffa069a429b4a938e09d1e3e701958c",
290 "timestamp": "2026-01-22T23:04:02.890851Z"
291 },
292 "keyId": "ra-gd-ra-us-west-2-ote",
293 "signature": "eyJhbGci..."
294 }
295 },
296 "schemaVersion": "V1"
297 }"#;
298
299 let badge: Badge = serde_json::from_str(json).unwrap();
300 assert_eq!(badge.status, BadgeStatus::Active);
301 assert_eq!(badge.agent_host(), "agent.example.com");
302 assert_eq!(badge.agent_version(), "v1.0.0");
303 assert!(badge.server_cert_fingerprint().starts_with("SHA256:"));
304 }
305}