1use std::sync::Arc;
39
40use rustls::server::{ClientHello, ResolvesServerCert};
41use rustls::sign::CertifiedKey;
42
43use crate::directory::Environment;
44use crate::dns::AcmeDnsChallenge;
45use crate::manager::{BootstrapError, CertificateManager};
46use crate::renew::{AlertLevel, RenewalPolicy};
47use crate::resolver::SwappableCert;
48
49#[derive(Debug, thiserror::Error)]
51pub enum FleetError {
52 #[error("a certificate fleet needs at least one zone")]
54 NoZones,
55 #[error("zone {0:?} is configured more than once")]
59 DuplicateZone(String),
60 #[error("zone {zone:?}: {source}")]
64 Zone {
65 zone: String,
67 #[source]
69 source: Box<BootstrapError>,
70 },
71}
72
73pub fn zone_covers(zone: &str, host: &str) -> bool {
81 let zone = zone.trim_end_matches('.');
82 let host = host.trim_end_matches('.');
83 if host.eq_ignore_ascii_case(zone) {
84 return true;
85 }
86 match host.len().checked_sub(zone.len() + 1) {
87 Some(prefix_len) if prefix_len > 0 => {
88 let (prefix, rest) = host.split_at(prefix_len);
89 rest.eq_ignore_ascii_case(&format!(".{zone}")) && !prefix.contains('.')
90 }
91 _ => false,
92 }
93}
94
95#[derive(Debug)]
112pub struct ZoneResolver {
113 zones: Vec<(String, Arc<SwappableCert>)>,
118 primary: Arc<SwappableCert>,
121}
122
123impl ZoneResolver {
124 pub fn new(zones: Vec<(String, Arc<SwappableCert>)>) -> Result<Arc<Self>, FleetError> {
129 if zones.is_empty() {
130 return Err(FleetError::NoZones);
131 }
132 for i in 0..zones.len() {
133 for j in (i + 1)..zones.len() {
134 if zones[i].0.eq_ignore_ascii_case(&zones[j].0) {
135 return Err(FleetError::DuplicateZone(zones[i].0.clone()));
136 }
137 }
138 }
139 let primary = zones[0].1.clone();
140 let mut zones = zones;
141 zones.sort_by_key(|(zone, _)| std::cmp::Reverse(zone.matches('.').count()));
142 Ok(Arc::new(Self { zones, primary }))
143 }
144
145 pub fn zones(&self) -> Vec<&str> {
147 self.zones.iter().map(|(zone, _)| zone.as_str()).collect()
148 }
149
150 pub fn certificate_for(&self, host: &str) -> Option<Arc<CertifiedKey>> {
155 self.zones
156 .iter()
157 .find(|(zone, _)| zone_covers(zone, host))
158 .map(|(_, cert)| cert.current())
159 }
160}
161
162impl ResolvesServerCert for ZoneResolver {
163 fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
164 match client_hello.server_name() {
165 Some(host) => {
166 let found = self.certificate_for(host);
167 if found.is_none() {
168 tracing::debug!(
169 sni = host,
170 zones = ?self.zones(),
171 "no configured zone covers this sni name; refusing the handshake"
172 );
173 }
174 found
175 }
176 None => Some(self.primary_cert()),
177 }
178 }
179}
180
181impl ZoneResolver {
182 pub fn primary_cert(&self) -> Arc<CertifiedKey> {
185 self.primary.current()
186 }
187}
188
189pub struct CertificateFleet<D: AcmeDnsChallenge> {
192 managers: Vec<Arc<CertificateManager<D>>>,
193 resolver: Arc<ZoneResolver>,
194}
195
196impl<D: AcmeDnsChallenge + 'static> CertificateFleet<D> {
197 pub async fn bootstrap(
209 data_dir: &std::path::Path,
210 zones: Vec<(String, Arc<D>)>,
211 environment: Environment,
212 contact: &[&str],
213 policy: RenewalPolicy,
214 ) -> Result<Arc<Self>, FleetError> {
215 if zones.is_empty() {
216 return Err(FleetError::NoZones);
217 }
218 for i in 0..zones.len() {
219 for j in (i + 1)..zones.len() {
220 if zones[i].0.eq_ignore_ascii_case(&zones[j].0) {
221 return Err(FleetError::DuplicateZone(zones[i].0.clone()));
222 }
223 }
224 }
225
226 let mut managers = Vec::with_capacity(zones.len());
227 for (zone, dns) in zones {
231 let manager =
232 CertificateManager::bootstrap(data_dir, &zone, environment, contact, policy, dns)
233 .await
234 .map_err(|source| FleetError::Zone {
235 zone: zone.clone(),
236 source: Box::new(source),
237 })?;
238 managers.push(manager);
239 }
240
241 Self::from_managers(managers)
242 }
243
244 pub fn from_managers(
248 managers: Vec<Arc<CertificateManager<D>>>,
249 ) -> Result<Arc<Self>, FleetError> {
250 let resolver = ZoneResolver::new(
251 managers
252 .iter()
253 .map(|manager| (manager.zone().to_owned(), manager.resolver()))
254 .collect(),
255 )?;
256 tracing::info!(zones = ?resolver.zones(), "tls certificates ready, one pair per zone");
257 Ok(Arc::new(Self { managers, resolver }))
258 }
259
260 pub fn resolver(&self) -> Arc<ZoneResolver> {
262 self.resolver.clone()
263 }
264
265 pub fn server_config(&self) -> Arc<rustls::ServerConfig> {
269 let mut config = rustls::ServerConfig::builder()
270 .with_no_client_auth()
271 .with_cert_resolver(self.resolver.clone());
272 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
273 Arc::new(config)
274 }
275
276 pub fn managers(&self) -> &[Arc<CertificateManager<D>>] {
278 &self.managers
279 }
280
281 pub async fn alert_level(&self) -> AlertLevel {
283 let mut worst = AlertLevel::Ok;
284 for manager in &self.managers {
285 worst = worst.max(manager.alert_level().await);
286 }
287 worst
288 }
289
290 pub async fn zone_alert_levels(&self) -> Vec<(String, AlertLevel)> {
293 let mut levels = Vec::with_capacity(self.managers.len());
294 for manager in &self.managers {
295 levels.push((manager.zone().to_owned(), manager.alert_level().await));
296 }
297 levels
298 }
299
300 pub async fn run_renewal_loops(self: Arc<Self>) {
306 let mut tasks = Vec::with_capacity(self.managers.len());
307 for manager in &self.managers {
308 tasks.push(tokio::spawn(manager.clone().run_renewal_loop()));
309 }
310 let _guard = AbortOnDrop(tasks);
311 std::future::pending::<()>().await;
312 }
313}
314
315struct AbortOnDrop(Vec<tokio::task::JoinHandle<()>>);
318
319impl Drop for AbortOnDrop {
320 fn drop(&mut self) {
321 for task in &self.0 {
322 task.abort();
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::cert::certified_key_from_pem;
331 use crate::test_support::self_signed_pem;
332
333 fn swappable(name: &str) -> Arc<SwappableCert> {
334 let (cert, key) = self_signed_pem(name);
335 let provider = rustls::crypto::ring::default_provider();
336 SwappableCert::new(certified_key_from_pem(&cert, &key, &provider).unwrap())
337 }
338
339 fn resolver(zones: &[&str]) -> Arc<ZoneResolver> {
340 ZoneResolver::new(
341 zones
342 .iter()
343 .map(|zone| ((*zone).to_owned(), swappable(zone)))
344 .collect(),
345 )
346 .unwrap()
347 }
348
349 #[test]
350 fn a_zone_covers_its_apex_and_exactly_one_label_under_it() {
351 assert!(zone_covers("pds.did.bot", "pds.did.bot"));
352 assert!(zone_covers("pds.did.bot", "claudes.pds.did.bot"));
353 assert!(!zone_covers("pds.did.bot", "agent.claudes.pds.did.bot"));
354 assert!(!zone_covers("pds.did.bot", "did.bot"));
355 assert!(!zone_covers("pds.did.bot", "notpds.did.bot"));
356 assert!(!zone_covers("pds.did.bot", ".pds.did.bot"));
357 }
358
359 #[test]
360 fn coverage_ignores_case_and_a_trailing_root_dot() {
361 assert!(zone_covers("pds.did.bot", "Claudes.PDS.did.bot"));
362 assert!(zone_covers("pds.did.bot", "claudes.pds.did.bot."));
363 }
364
365 #[test]
366 fn each_zone_gets_its_own_certificate() {
367 let resolver = resolver(&["claudes.pds.did.bot", "vllm.pds.did.bot"]);
368
369 let a = resolver
373 .certificate_for("agent.claudes.pds.did.bot")
374 .unwrap();
375 let b = resolver.certificate_for("agent.vllm.pds.did.bot").unwrap();
376 assert_ne!(a.cert.first().unwrap(), b.cert.first().unwrap());
377 assert_eq!(
378 a.cert.first().unwrap(),
379 resolver
380 .certificate_for("claudes.pds.did.bot")
381 .unwrap()
382 .cert
383 .first()
384 .unwrap()
385 );
386
387 assert!(resolver
390 .certificate_for("agent.sub.claudes.pds.did.bot")
391 .is_none());
392 }
393
394 #[test]
395 fn a_name_no_zone_covers_gets_nothing() {
396 let resolver = resolver(&["claudes.pds.did.bot"]);
397 assert!(resolver.certificate_for("elsewhere.example").is_none());
398 assert!(resolver
399 .certificate_for("deep.one.claudes.pds.did.bot")
400 .is_none());
401 }
402
403 #[test]
404 fn a_nested_zone_wins_over_its_parent() {
405 let resolver = resolver(&["pds.did.bot", "claudes.pds.did.bot"]);
408 let nested = resolver
409 .certificate_for("agent.claudes.pds.did.bot")
410 .unwrap();
411 let own = resolver.certificate_for("claudes.pds.did.bot").unwrap();
412 assert_eq!(nested.cert.first().unwrap(), own.cert.first().unwrap());
416 }
417
418 #[test]
419 fn no_sni_gets_the_primary_zone() {
420 let resolver = resolver(&["pds.did.bot", "claudes.pds.did.bot"]);
421 let primary = resolver.certificate_for("pds.did.bot").unwrap();
422 assert_eq!(
423 resolver.primary_cert().cert.first().unwrap(),
424 primary.cert.first().unwrap()
425 );
426 }
427
428 #[test]
429 fn a_renewal_swaps_only_its_own_zone() {
430 let claudes = swappable("claudes.pds.did.bot");
431 let vllm = swappable("vllm.pds.did.bot");
432 let resolver = ZoneResolver::new(vec![
433 ("claudes.pds.did.bot".to_owned(), claudes.clone()),
434 ("vllm.pds.did.bot".to_owned(), vllm.clone()),
435 ])
436 .unwrap();
437
438 let vllm_before = resolver.certificate_for("vllm.pds.did.bot").unwrap();
439 let claudes_before = resolver.certificate_for("claudes.pds.did.bot").unwrap();
440
441 let (cert, key) = self_signed_pem("claudes.pds.did.bot");
442 let provider = rustls::crypto::ring::default_provider();
443 claudes.swap(certified_key_from_pem(&cert, &key, &provider).unwrap());
444
445 assert_ne!(
446 claudes_before.cert.first().unwrap(),
447 resolver
448 .certificate_for("claudes.pds.did.bot")
449 .unwrap()
450 .cert
451 .first()
452 .unwrap()
453 );
454 assert_eq!(
455 vllm_before.cert.first().unwrap(),
456 resolver
457 .certificate_for("vllm.pds.did.bot")
458 .unwrap()
459 .cert
460 .first()
461 .unwrap()
462 );
463 }
464
465 #[test]
466 fn duplicate_and_empty_zone_sets_are_refused() {
467 assert!(matches!(
468 ZoneResolver::new(vec![]).unwrap_err(),
469 FleetError::NoZones
470 ));
471 let dup = ZoneResolver::new(vec![
472 ("pds.did.bot".to_owned(), swappable("pds.did.bot")),
473 ("PDS.did.bot".to_owned(), swappable("pds.did.bot")),
474 ]);
475 assert!(matches!(dup.unwrap_err(), FleetError::DuplicateZone(_)));
476 }
477}