nixfleet_control_plane/polling/
revocations_poll.rs1use std::path::PathBuf;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Duration;
7
8use anyhow::Result;
9
10use crate::db::Db;
11use crate::polling::poller::SignedArtifactPoller;
12use crate::polling::signed_fetch;
13
14pub const POLL_INTERVAL: Duration = Duration::from_secs(60);
15
16#[derive(Debug, Clone)]
17pub struct RevocationsSource {
18 pub artifact_url: String,
19 pub signature_url: String,
20 pub token_file: Option<PathBuf>,
21 pub trust_path: PathBuf,
22 pub freshness_window: Duration,
23}
24
25pub fn spawn(
31 cancel: tokio_util::sync::CancellationToken,
32 db: Arc<Db>,
33 config: RevocationsSource,
34 revocations_primed: Arc<AtomicBool>,
35) -> tokio::task::JoinHandle<()> {
36 SignedArtifactPoller {
37 interval: POLL_INTERVAL,
38 label: "revocations",
39 }
40 .spawn(cancel, move |client| {
41 let db = Arc::clone(&db);
42 let config = config.clone();
43 let revocations_primed = Arc::clone(&revocations_primed);
44 async move {
45 let revs = poll_once(&client, &config).await?;
46 apply_verified_revocations(&db, &revs);
47 let was_primed = revocations_primed.swap(true, Ordering::AcqRel);
48 if !was_primed {
49 tracing::info!(
50 target: "revocations",
51 entries = revs.revocations.len(),
52 "revocations primed: first verified list applied",
53 );
54 }
55 Ok(())
56 }
57 })
58}
59
60fn apply_verified_revocations(db: &Db, revs: &nixfleet_proto::Revocations) {
62 let n = revs.revocations.len();
63 let mut applied = 0usize;
64 for entry in &revs.revocations {
65 match db.revocations().revoke_cert(
66 &entry.hostname,
67 entry.not_before,
68 entry.reason.as_deref(),
69 entry.revoked_by.as_deref(),
70 ) {
71 Ok(()) => applied += 1,
72 Err(err) => tracing::warn!(
73 hostname = %entry.hostname,
74 error = %err,
75 "revocations poll: revoke_cert failed for entry",
76 ),
77 }
78 }
79 let keep: Vec<&str> = revs
84 .revocations
85 .iter()
86 .map(|e| e.hostname.as_str())
87 .collect();
88 let pruned = match db.revocations().retain_only(&keep) {
89 Ok(n) => n,
90 Err(err) => {
91 tracing::warn!(
92 error = %err,
93 "revocations poll: retain_only failed - DB may keep stale revocations",
94 );
95 0
96 }
97 };
98 tracing::info!(
99 target: "revocations",
100 entries = n,
101 applied = applied,
102 pruned = pruned,
103 signed_at = ?revs.meta.signed_at,
104 ci_commit = ?revs.meta.ci_commit,
105 "revocations poll: list verified",
106 );
107}
108
109async fn poll_once(
110 client: &reqwest::Client,
111 config: &RevocationsSource,
112) -> Result<nixfleet_proto::Revocations> {
113 let token = signed_fetch::read_token(config.token_file.as_deref())?;
114 let (artifact_bytes, signature_bytes) = signed_fetch::fetch_signed_pair(
115 client,
116 &config.artifact_url,
117 &config.signature_url,
118 token.as_deref(),
119 )
120 .await?;
121
122 let (trusted_keys, reject_before) =
123 signed_fetch::read_trust_roots(&config.trust_path, chrono::Utc::now())?;
124
125 nixfleet_reconciler::verify_revocations(
126 &artifact_bytes,
127 &signature_bytes,
128 &trusted_keys,
129 chrono::Utc::now(),
130 config.freshness_window,
131 reject_before,
132 )
133 .map(|v| v.into_inner())
134 .map_err(|e| anyhow::anyhow!("verify_revocations (revocations poll): {e:?}"))
135}