Skip to main content

orca_proxy/acme/
resolver.rs

1//! SNI-based dynamic certificate resolver for multi-domain TLS.
2//!
3//! Allows hot-adding certificates at runtime when new domains are deployed.
4
5use std::collections::HashMap;
6use std::sync::{Arc, RwLock};
7
8use rustls::server::{ClientHello, ResolvesServerCert};
9use rustls::sign::CertifiedKey;
10use tracing::debug;
11
12/// Thread-safe certificate store that resolves certs by SNI hostname.
13///
14/// New certs can be added at runtime without restarting the TLS listener.
15#[derive(Clone, Debug, Default)]
16pub struct DynCertResolver {
17    certs: Arc<RwLock<HashMap<String, Arc<CertifiedKey>>>>,
18}
19
20impl DynCertResolver {
21    pub fn new() -> Self {
22        Self {
23            certs: Arc::new(RwLock::new(HashMap::new())),
24        }
25    }
26
27    /// Add or replace a certificate for a domain.
28    pub fn add_cert(&self, domain: &str, key: Arc<CertifiedKey>) {
29        self.certs
30            .write()
31            .expect("cert store poisoned")
32            .insert(domain.to_string(), key);
33    }
34
35    /// Check if a cert exists for the given domain.
36    pub fn has_cert(&self, domain: &str) -> bool {
37        self.certs
38            .read()
39            .expect("cert store poisoned")
40            .contains_key(domain)
41    }
42}
43
44impl ResolvesServerCert for DynCertResolver {
45    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
46        let sni = client_hello.server_name()?;
47        let certs = self.certs.read().expect("cert store poisoned");
48        let key = certs.get(sni).cloned();
49        if key.is_none() {
50            debug!(sni, "No cert for SNI hostname");
51        }
52        key
53    }
54}