Skip to main content

ans_types/
badge.rs

1//! Badge data models from the Transparency Log API.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Badge status values.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
10#[non_exhaustive]
11pub enum BadgeStatus {
12    /// Agent is registered and in good standing.
13    Active,
14    /// Certificate expires within 30 days.
15    Warning,
16    /// AHP has marked this version for retirement; consumers should migrate.
17    Deprecated,
18    /// Certificate has expired.
19    Expired,
20    /// Registration has been explicitly revoked.
21    Revoked,
22}
23
24impl BadgeStatus {
25    /// Check if this status is valid for establishing connections.
26    pub fn is_valid_for_connection(&self) -> bool {
27        matches!(self, Self::Active | Self::Warning | Self::Deprecated)
28    }
29
30    /// Check if this status is fully active (not deprecated).
31    pub fn is_active(&self) -> bool {
32        matches!(self, Self::Active | Self::Warning)
33    }
34
35    /// Check if this status indicates the badge should be rejected.
36    pub fn should_reject(&self) -> bool {
37        matches!(self, Self::Expired | Self::Revoked)
38    }
39}
40
41/// Event types for badge events.
42///
43/// These match the TL API swagger spec eventType enum.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
46#[non_exhaustive]
47pub enum EventType {
48    /// Agent was initially registered.
49    AgentRegistered,
50    /// Agent certificates were renewed.
51    AgentRenewed,
52    /// AHP has marked this version for retirement.
53    AgentDeprecated,
54    /// Agent registration was revoked.
55    AgentRevoked,
56}
57
58/// Full badge response from the Transparency Log API.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61#[non_exhaustive]
62pub struct Badge {
63    /// Current status of the badge.
64    pub status: BadgeStatus,
65    /// Badge payload containing the signed event.
66    pub payload: BadgePayload,
67    /// Schema version (e.g., "V1").
68    pub schema_version: String,
69    /// Signature over the badge.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub signature: Option<String>,
72    /// Merkle proof for transparency log inclusion.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub merkle_proof: Option<MerkleProof>,
75}
76
77impl Badge {
78    /// Get the agent's ANS name.
79    pub fn agent_name(&self) -> &str {
80        &self.payload.producer.event.ans_name
81    }
82
83    /// Get the agent's host FQDN.
84    pub fn agent_host(&self) -> &str {
85        &self.payload.producer.event.agent.host
86    }
87
88    /// Get the agent's version string.
89    pub fn agent_version(&self) -> &str {
90        &self.payload.producer.event.agent.version
91    }
92
93    /// Get the server certificate fingerprint.
94    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    /// Get the identity certificate fingerprint.
105    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    /// Get the agent ID (UUID).
116    pub fn agent_id(&self) -> Uuid {
117        self.payload.producer.event.ans_id
118    }
119
120    /// Get the event type.
121    pub fn event_type(&self) -> EventType {
122        self.payload.producer.event.event_type
123    }
124
125    /// Check if this badge is valid for connections.
126    pub fn is_valid(&self) -> bool {
127        self.status.is_valid_for_connection()
128    }
129}
130
131/// Badge payload containing the producer and signed event.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134#[non_exhaustive]
135pub struct BadgePayload {
136    /// Log ID for this entry.
137    pub log_id: Uuid,
138    /// Producer information with signed event.
139    pub producer: Producer,
140}
141
142/// Producer information with the agent event and signature.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145#[non_exhaustive]
146pub struct Producer {
147    /// The agent event details.
148    pub event: AgentEvent,
149    /// Key ID used for signing.
150    pub key_id: String,
151    /// Signature over the event.
152    pub signature: String,
153}
154
155/// Agent event containing all registration/verification details.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158#[non_exhaustive]
159pub struct AgentEvent {
160    /// Agent's unique ID.
161    pub ans_id: Uuid,
162    /// Full ANS name (e.g., "<ans://v1.0.0.agent.example.com>").
163    pub ans_name: String,
164    /// Type of event.
165    pub event_type: EventType,
166    /// Agent information.
167    pub agent: AgentInfo,
168    /// Certificate attestations.
169    pub attestations: Attestations,
170    /// When this registration expires.
171    pub expires_at: DateTime<Utc>,
172    /// When this registration was issued.
173    pub issued_at: DateTime<Utc>,
174    /// Registration Authority ID.
175    pub ra_id: String,
176    /// Event timestamp.
177    pub timestamp: DateTime<Utc>,
178}
179
180/// Basic agent information.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182#[non_exhaustive]
183pub struct AgentInfo {
184    /// Agent's host FQDN.
185    pub host: String,
186    /// Human-readable agent name.
187    pub name: String,
188    /// Agent version string.
189    pub version: String,
190}
191
192/// Certificate attestations.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195#[non_exhaustive]
196pub struct Attestations {
197    /// Domain validation method used.
198    pub domain_validation: String,
199    /// Identity certificate attestation.
200    pub identity_cert: CertAttestation,
201    /// Server certificate attestation.
202    pub server_cert: CertAttestation,
203}
204
205/// Certificate attestation with fingerprint and type.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[non_exhaustive]
208pub struct CertAttestation {
209    /// Certificate fingerprint in `SHA256:<hex>` format.
210    pub fingerprint: String,
211    /// Certificate type (e.g., "X509-DV-SERVER", "X509-OV-CLIENT").
212    #[serde(rename = "type")]
213    pub cert_type: String,
214}
215
216/// Merkle proof for transparency log inclusion verification.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219#[non_exhaustive]
220pub struct MerkleProof {
221    /// Hash of the leaf node.
222    pub leaf_hash: String,
223    /// Index of the leaf in the tree.
224    pub leaf_index: u64,
225    /// Proof path (sibling hashes).
226    pub path: Vec<String>,
227    /// Root hash of the tree.
228    pub root_hash: String,
229    /// Signature over the root.
230    pub root_signature: String,
231    /// Total size of the tree.
232    pub tree_size: u64,
233    /// Version of the tree.
234    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}