nixfleet_proto/
revocations.rs

1//! `revocations.json` - signed agent-cert revocation sidecar. Same
2//! trust class as `fleet.resolved.json` (signed by `ciReleaseKey`).
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use crate::fleet_resolved::Meta;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10#[serde(rename_all = "camelCase")]
11pub struct Revocations {
12    pub schema_version: u32,
13    /// Empty list is the steady state.
14    pub revocations: Vec<RevocationEntry>,
15    pub meta: Meta,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19#[serde(rename_all = "camelCase")]
20pub struct RevocationEntry {
21    pub hostname: String,
22    /// Any cert for `hostname` with `notBefore` strictly older than this
23    /// is rejected at mTLS handshake time.
24    pub not_before: DateTime<Utc>,
25    #[serde(default)]
26    pub reason: Option<String>,
27    #[serde(default)]
28    pub revoked_by: Option<String>,
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    fn meta_v1() -> Meta {
36        Meta {
37            schema_version: 1,
38            signed_at: Some("2026-04-28T10:00:00Z".parse().unwrap()),
39            ci_commit: Some("abc12345".into()),
40            signature_algorithm: Some("ed25519".into()),
41        }
42    }
43
44    #[test]
45    fn empty_revocations_round_trip() {
46        let r = Revocations {
47            schema_version: 1,
48            revocations: vec![],
49            meta: meta_v1(),
50        };
51        let s = serde_json::to_string(&r).unwrap();
52        let parsed: Revocations = serde_json::from_str(&s).unwrap();
53        assert_eq!(parsed, r);
54    }
55
56    #[test]
57    fn revocation_entry_round_trip() {
58        let r = Revocations {
59            schema_version: 1,
60            revocations: vec![RevocationEntry {
61                hostname: "old-laptop".into(),
62                not_before: "2026-04-26T00:00:00Z".parse().unwrap(),
63                reason: Some("decommissioned".into()),
64                revoked_by: Some("operator".into()),
65            }],
66            meta: meta_v1(),
67        };
68        let s = serde_json::to_string(&r).unwrap();
69        let parsed: Revocations = serde_json::from_str(&s).unwrap();
70        assert_eq!(parsed, r);
71    }
72
73    #[test]
74    fn revocation_entry_optional_fields_default_to_none() {
75        let json = r#"{
76            "hostname": "old-laptop",
77            "notBefore": "2026-04-26T00:00:00Z"
78        }"#;
79        let entry: RevocationEntry = serde_json::from_str(json).unwrap();
80        assert_eq!(entry.hostname, "old-laptop");
81        assert!(entry.reason.is_none());
82        assert!(entry.revoked_by.is_none());
83    }
84}