Skip to main content

gatel_core/tls/
manager.rs

1//! TLS configuration manager with ACME auto-issuance, mTLS, and on-demand TLS support.
2//!
3//! [`TlsManager`] bridges gatel's configuration with `certon` for automatic
4//! certificate management and supports manually-specified PEM certificates on a
5//! per-site basis.
6
7use std::collections::{HashMap, HashSet};
8use std::fs;
9use std::path::Path;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::time::Duration;
13
14use arc_swap::ArcSwap;
15use certon::{
16    AcmeIssuer, CertResolver, Certificate, Config as CertAutoConfig, FileStorage, OnDemandConfig,
17    Storage,
18};
19use rustls::RootCertStore;
20use rustls::server::ResolvesServerCert;
21use rustls::sign::CertifiedKey;
22use tokio::task::JoinHandle;
23use tokio_rustls::TlsAcceptor;
24use tracing::{debug, error, info, warn};
25
26use crate::ProxyError;
27use crate::config::{
28    AcmeConfig, AppConfig, CertAuthority, ChallengeType, ClientAuthConfig, DnsProviderConfig,
29    OnDemandTlsConfig, SiteTlsConfig, TlsConfig,
30};
31
32// ---------------------------------------------------------------------------
33// TlsManager
34// ---------------------------------------------------------------------------
35
36/// Manages TLS configuration for the proxy, supporting both automatic ACME
37/// certificate issuance (via certon) and manually-provided PEM certificates.
38///
39/// The manager builds a `rustls::ServerConfig` that uses a composite
40/// certificate resolver:
41/// - Sites with explicit `SiteTlsConfig` (cert/key PEM paths) are loaded immediately and registered
42///   as manual overrides.
43/// - Sites without explicit certs are enrolled in ACME management through certon, which obtains,
44///   caches, and auto-renews their certificates.
45///
46/// Additionally supports:
47/// - **mTLS** (mutual TLS): client certificate verification using configured CA certificates. When
48///   `client_auth` is configured, the server requests (and optionally requires) client
49///   certificates.
50/// - **On-demand TLS**: automatic certificate issuance at handshake time for previously unknown
51///   domains, with optional ask-URL gating and rate limiting.
52///
53/// Call [`TlsManager::reload`] to hot-swap the TLS configuration when the
54/// proxy config changes.
55pub struct TlsManager {
56    /// The certon config that drives ACME certificate management.
57    certon_config: Option<Arc<CertAutoConfig>>,
58
59    /// The composite resolver used by the rustls `ServerConfig`.
60    resolver: Arc<CompositeResolver>,
61
62    /// The current rustls `ServerConfig`, swappable for hot-reload.
63    server_config: ArcSwap<rustls::ServerConfig>,
64
65    /// Handle to the certon maintenance task (renewal + OCSP refresh).
66    maintenance_handle: Option<JoinHandle<()>>,
67
68    /// Shared challenge map for the HTTP-01 solver. The ACME challenge
69    /// middleware reads from this map to serve challenge responses.
70    challenge_map: Arc<tokio::sync::RwLock<HashMap<String, String>>>,
71}
72
73impl TlsManager {
74    /// Build a new `TlsManager` from the application configuration.
75    ///
76    /// This performs the initial TLS setup:
77    /// 1. Loads manual certificates for sites that specify cert/key paths.
78    /// 2. Configures certon for ACME-managed sites (if ACME is enabled).
79    /// 3. Calls `manage_sync` to obtain/load certificates for ACME domains.
80    /// 4. Starts the certon maintenance loop.
81    /// 5. Sets up mTLS client certificate verification if configured.
82    /// 6. Configures on-demand TLS if configured.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if manual certificate loading fails or if ACME
87    /// management cannot be initialized.
88    pub async fn build(config: &AppConfig) -> Result<Self, ProxyError> {
89        let challenge_map: Arc<tokio::sync::RwLock<HashMap<String, String>>> =
90            Arc::new(tokio::sync::RwLock::new(HashMap::new()));
91
92        // Partition sites into manual-cert and ACME-managed.
93        let mut manual_certs: HashMap<String, Arc<CertifiedKey>> = HashMap::new();
94        let mut acme_domains: Vec<String> = Vec::new();
95
96        for site in &config.sites {
97            if let Some(ref site_tls) = site.tls {
98                match load_manual_cert(site_tls) {
99                    Ok(certified_key) => {
100                        info!(
101                            host = %site.host,
102                            cert = %site_tls.cert,
103                            "loaded manual TLS certificate"
104                        );
105                        manual_certs.insert(site.host.clone(), certified_key);
106                    }
107                    Err(e) => {
108                        error!(
109                            host = %site.host,
110                            cert = %site_tls.cert,
111                            key = %site_tls.key,
112                            error = %e,
113                            "failed to load manual TLS certificate"
114                        );
115                        return Err(ProxyError::Internal(format!(
116                            "failed to load TLS certificate for {}: {e}",
117                            site.host
118                        )));
119                    }
120                }
121            } else if config.tls.is_some() {
122                // Only enroll in ACME if global TLS/ACME is configured.
123                acme_domains.push(site.host.clone());
124            }
125        }
126
127        // Build certon config and resolver (if ACME is enabled).
128        let (certon_config, cert_resolver, maintenance_handle) =
129            if let Some(ref tls_config) = config.tls {
130                if let Some(ref acme_config) = tls_config.acme {
131                    let (ca_config, resolver, handle) = setup_acme(
132                        acme_config,
133                        &acme_domains,
134                        challenge_map.clone(),
135                        tls_config.on_demand.as_ref(),
136                        &config.sites,
137                    )
138                    .await?;
139                    (Some(Arc::new(ca_config)), Some(resolver), Some(handle))
140                } else {
141                    (None, None, None)
142                }
143            } else {
144                (None, None, None)
145            };
146
147        // Build the composite resolver.
148        let composite = Arc::new(CompositeResolver {
149            manual_certs: tokio::sync::RwLock::new(manual_certs),
150            acme_resolver: cert_resolver,
151        });
152
153        // Build the optional client certificate verifier for mTLS.
154        let client_verifier = if let Some(ref tls_config) = config.tls {
155            if let Some(ref client_auth) = tls_config.client_auth {
156                Some(build_client_verifier(client_auth)?)
157            } else {
158                None
159            }
160        } else {
161            None
162        };
163
164        // Build the rustls ServerConfig.
165        let rustls_config = build_server_config(
166            composite.clone(),
167            client_verifier.as_ref(),
168            config.tls.as_ref(),
169        );
170
171        // Log OCSP stapling status.
172        if let Some(ref tls_config) = config.tls
173            && tls_config.ocsp_stapling
174        {
175            info!(
176                "OCSP stapling enabled (handled by certon for ACME certs; \
177                     manual certs require AIA extension parsing)"
178            );
179        }
180
181        Ok(Self {
182            certon_config,
183            resolver: composite,
184            server_config: ArcSwap::from_pointee(rustls_config),
185            maintenance_handle,
186            challenge_map,
187        })
188    }
189
190    /// Get a `TlsAcceptor` for use with tokio-rustls.
191    ///
192    /// The returned acceptor references the current `ServerConfig` via an
193    /// `Arc`, so it will continue to use the config snapshot at the time of
194    /// this call. For hot-reload, call this again after [`reload`](Self::reload).
195    pub fn acceptor(&self) -> TlsAcceptor {
196        let config = self.server_config.load_full();
197        TlsAcceptor::from(config)
198    }
199
200    /// Get the current `rustls::ServerConfig` as an `Arc`.
201    pub fn server_config(&self) -> Arc<rustls::ServerConfig> {
202        self.server_config.load_full()
203    }
204
205    /// Get a reference to the shared ACME HTTP-01 challenge map.
206    ///
207    /// This is the same map that the `AcmeChallengeHoop` reads from
208    /// to serve challenge responses.
209    pub fn challenge_map(&self) -> Arc<tokio::sync::RwLock<HashMap<String, String>>> {
210        self.challenge_map.clone()
211    }
212
213    /// Hot-reload the TLS configuration.
214    ///
215    /// This rebuilds manual certificates and re-enrolls ACME domains based
216    /// on the new config. The `ServerConfig` is atomically swapped so that
217    /// in-flight connections are not affected.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if any manual certificate cannot be loaded.
222    pub async fn reload(&self, config: &AppConfig) -> Result<(), ProxyError> {
223        info!("reloading TLS configuration");
224
225        // Reload manual certificates.
226        let mut new_manual: HashMap<String, Arc<CertifiedKey>> = HashMap::new();
227        let mut acme_domains: Vec<String> = Vec::new();
228
229        for site in &config.sites {
230            if let Some(ref site_tls) = site.tls {
231                match load_manual_cert(site_tls) {
232                    Ok(certified_key) => {
233                        info!(
234                            host = %site.host,
235                            cert = %site_tls.cert,
236                            "reloaded manual TLS certificate"
237                        );
238                        new_manual.insert(site.host.clone(), certified_key);
239                    }
240                    Err(e) => {
241                        error!(
242                            host = %site.host,
243                            error = %e,
244                            "failed to reload manual TLS certificate"
245                        );
246                        return Err(ProxyError::Internal(format!(
247                            "failed to reload TLS certificate for {}: {e}",
248                            site.host
249                        )));
250                    }
251                }
252            } else if config.tls.is_some() {
253                acme_domains.push(site.host.clone());
254            }
255        }
256
257        // Swap manual certs.
258        {
259            let mut guard = self.resolver.manual_certs.write().await;
260            *guard = new_manual;
261        }
262
263        // Re-enroll new ACME domains (if any new ones appeared).
264        if !acme_domains.is_empty()
265            && let Some(ref ca_config) = self.certon_config
266            && let Err(e) = ca_config.manage_sync(&acme_domains).await
267        {
268            warn!(
269                error = %e,
270                "failed to manage new ACME domains during reload"
271            );
272        }
273
274        // Rebuild the client cert verifier if mTLS is configured.
275        let client_verifier = if let Some(ref tls_config) = config.tls {
276            if let Some(ref client_auth) = tls_config.client_auth {
277                match build_client_verifier(client_auth) {
278                    Ok(v) => Some(v),
279                    Err(e) => {
280                        error!(error = %e, "failed to rebuild client cert verifier during reload");
281                        None
282                    }
283                }
284            } else {
285                None
286            }
287        } else {
288            None
289        };
290
291        // Rebuild and swap the ServerConfig.
292        let new_config = build_server_config(
293            self.resolver.clone(),
294            client_verifier.as_ref(),
295            config.tls.as_ref(),
296        );
297        self.server_config.store(Arc::new(new_config));
298
299        info!("TLS configuration reloaded successfully");
300        Ok(())
301    }
302
303    /// Stop the certon maintenance loop.
304    ///
305    /// This should be called during graceful shutdown. After calling this,
306    /// no further certificate renewals or OCSP refreshes will occur.
307    pub fn stop_maintenance(&self) {
308        if let Some(ref ca_config) = self.certon_config {
309            ca_config.cache.stop();
310            info!("certon maintenance loop stopped");
311        }
312    }
313}
314
315impl Drop for TlsManager {
316    fn drop(&mut self) {
317        // Signal the maintenance task to stop.
318        if let Some(ref ca_config) = self.certon_config {
319            ca_config.cache.stop();
320        }
321        // Abort the maintenance task handle if it's still running.
322        if let Some(ref handle) = self.maintenance_handle {
323            handle.abort();
324        }
325    }
326}
327
328// ---------------------------------------------------------------------------
329// CompositeResolver
330// ---------------------------------------------------------------------------
331
332/// A certificate resolver that checks manual overrides first, then falls
333/// back to the certon-managed `CertResolver`.
334///
335/// Manual certificates take priority over ACME-managed ones, allowing
336/// per-site overrides while still benefiting from automatic management
337/// for everything else.
338struct CompositeResolver {
339    /// Manual certificate overrides, keyed by hostname.
340    manual_certs: tokio::sync::RwLock<HashMap<String, Arc<CertifiedKey>>>,
341    /// The certon-backed resolver for ACME-managed domains.
342    acme_resolver: Option<Arc<CertResolver>>,
343}
344
345impl std::fmt::Debug for CompositeResolver {
346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347        f.debug_struct("CompositeResolver")
348            .field("manual_certs", &"<RwLock<HashMap>>")
349            .field("acme_resolver", &self.acme_resolver.as_ref().map(|_| "..."))
350            .finish()
351    }
352}
353
354impl ResolvesServerCert for CompositeResolver {
355    fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
356        // Try manual certificates first (by SNI).
357        if let Some(sni) = client_hello.server_name()
358            && let Ok(guard) = self.manual_certs.try_read()
359            && let Some(ck) = guard.get(sni)
360        {
361            debug!(sni = %sni, "serving manual TLS certificate");
362            return Some(ck.clone());
363        }
364
365        // Fall back to the certon resolver.
366        if let Some(ref resolver) = self.acme_resolver {
367            return resolver.resolve(client_hello);
368        }
369
370        None
371    }
372}
373
374// ---------------------------------------------------------------------------
375// mTLS — Client certificate verification
376// ---------------------------------------------------------------------------
377
378/// Build a `rustls` client certificate verifier from the mTLS configuration.
379///
380/// Loads CA certificates from the PEM files specified in `client_auth.ca_certs`,
381/// then builds a `WebPkiClientVerifier`:
382/// - If `required == true`, clients *must* present a valid certificate.
383/// - If `required == false`, clients may connect without a certificate (optional mTLS).
384fn build_client_verifier(
385    client_auth: &ClientAuthConfig,
386) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, ProxyError> {
387    let mut root_store = RootCertStore::empty();
388
389    for ca_path in &client_auth.ca_certs {
390        let pem_data = fs::read(ca_path).map_err(|e| {
391            ProxyError::Internal(format!("failed to read CA cert file {ca_path}: {e}"))
392        })?;
393
394        let mut reader = std::io::BufReader::new(pem_data.as_slice());
395        let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
396            .collect::<Result<Vec<_>, _>>()
397            .map_err(|e| {
398                ProxyError::Internal(format!(
399                    "failed to parse PEM certificates from {ca_path}: {e}"
400                ))
401            })?;
402
403        if certs.is_empty() {
404            return Err(ProxyError::Internal(format!(
405                "no certificates found in CA file: {ca_path}"
406            )));
407        }
408
409        for cert in certs {
410            root_store.add(cert).map_err(|e| {
411                ProxyError::Internal(format!("failed to add CA cert from {ca_path}: {e}"))
412            })?;
413        }
414
415        info!(path = %ca_path, "loaded CA certificate for client auth");
416    }
417
418    let builder = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store));
419
420    let verifier = if client_auth.required {
421        info!("mTLS: client certificates required");
422        builder.build().map_err(|e| {
423            ProxyError::Internal(format!("failed to build client cert verifier: {e}"))
424        })?
425    } else {
426        info!("mTLS: client certificates optional");
427        builder.allow_unauthenticated().build().map_err(|e| {
428            ProxyError::Internal(format!(
429                "failed to build optional client cert verifier: {e}"
430            ))
431        })?
432    };
433
434    Ok(verifier)
435}
436
437// ---------------------------------------------------------------------------
438// ACME setup
439// ---------------------------------------------------------------------------
440
441/// Configure certon for ACME certificate management.
442///
443/// Returns the certon `Config`, a `CertResolver`, and a maintenance task
444/// handle.
445async fn setup_acme(
446    acme_config: &AcmeConfig,
447    domains: &[String],
448    challenge_map: Arc<tokio::sync::RwLock<HashMap<String, String>>>,
449    on_demand_config: Option<&OnDemandTlsConfig>,
450    sites: &[crate::config::SiteConfig],
451) -> Result<(CertAutoConfig, Arc<CertResolver>, JoinHandle<()>), ProxyError> {
452    let storage: Arc<dyn Storage> = Arc::new(FileStorage::default());
453
454    // Build the ACME issuer.
455    let ca_url = match acme_config.ca {
456        CertAuthority::LetsEncrypt => certon::LETS_ENCRYPT_PRODUCTION,
457        CertAuthority::LetsEncryptStaging => certon::LETS_ENCRYPT_STAGING,
458        CertAuthority::ZeroSsl => certon::ZEROSSL_PRODUCTION,
459    };
460
461    let mut issuer_builder = AcmeIssuer::builder()
462        .ca(ca_url)
463        .email(&acme_config.email)
464        .agreed(true)
465        .storage(storage.clone());
466
467    // Configure challenge type.
468    match acme_config.challenge {
469        ChallengeType::Http01 => {
470            // Use a custom HTTP-01 solver that shares the challenge map with
471            // our middleware instead of binding its own port 80 listener.
472            let solver = Arc::new(SharedMapHttp01Solver {
473                challenges: challenge_map,
474            });
475            issuer_builder = issuer_builder
476                .http01_solver(solver)
477                .disable_tlsalpn_challenge(true);
478        }
479        ChallengeType::TlsAlpn01 => {
480            issuer_builder = issuer_builder.disable_http_challenge(true);
481        }
482        ChallengeType::Dns01 => {
483            if let Some(ref dns_cfg) = acme_config.dns_provider {
484                let provider = create_dns_provider(dns_cfg)?;
485                let dns_solver = certon::Dns01Solver::new(provider);
486                issuer_builder = issuer_builder
487                    .dns01_solver(Arc::new(dns_solver))
488                    .disable_http_challenge(true)
489                    .disable_tlsalpn_challenge(true);
490            } else {
491                return Err(ProxyError::Internal(
492                    "DNS-01 challenge requires a dns-provider configuration".into(),
493                ));
494            }
495        }
496    }
497
498    // Apply EAB credentials if configured (required by some CAs like ZeroSSL).
499    if let Some(ref eab_cfg) = acme_config.eab {
500        let hmac_bytes = base64_decode_hmac(&eab_cfg.hmac_key)?;
501        issuer_builder =
502            issuer_builder.external_account(certon::acme_client::ExternalAccountBinding {
503                kid: eab_cfg.kid.clone(),
504                hmac_key: hmac_bytes,
505            });
506    }
507
508    let issuer = Arc::new(issuer_builder.build());
509
510    // Build the certon Config, optionally with on-demand TLS.
511    let mut config_builder = CertAutoConfig::builder()
512        .storage(storage)
513        .issuers(vec![issuer.clone()]);
514
515    // Set up on-demand TLS if configured.
516    if let Some(od_config) = on_demand_config {
517        let on_demand = build_on_demand_config(od_config, &issuer, sites)?;
518        config_builder = config_builder.on_demand(Arc::new(on_demand));
519        info!("on-demand TLS configured");
520    }
521
522    let ca_config = config_builder.build();
523
524    // Manage certificates for the configured domains.
525    if !domains.is_empty() {
526        info!(domains = ?domains, "managing ACME certificates");
527        ca_config.manage_sync(domains).await.map_err(|e| {
528            ProxyError::Internal(format!("ACME certificate management failed: {e}"))
529        })?;
530    }
531
532    // Build the CertResolver backed by certon's cache.
533    // If on-demand TLS is configured, use the on_demand variant so the
534    // resolver can trigger background certificate acquisition.
535    let resolver = if ca_config.on_demand.is_some() {
536        let on_demand = ca_config.on_demand.clone().unwrap();
537        Arc::new(CertResolver::with_on_demand(
538            ca_config.cache.clone(),
539            on_demand,
540        ))
541    } else {
542        Arc::new(CertResolver::new(ca_config.cache.clone()))
543    };
544
545    // Start the maintenance loop (renewal + OCSP refresh).
546    let maintenance_handle = certon::start_maintenance(&ca_config);
547    info!("certon maintenance loop started");
548
549    Ok((ca_config, resolver, maintenance_handle))
550}
551
552/// Build the `OnDemandConfig` from our config types.
553fn build_on_demand_config(
554    od_config: &OnDemandTlsConfig,
555    issuer: &Arc<certon::AcmeIssuer>,
556    sites: &[crate::config::SiteConfig],
557) -> Result<OnDemandConfig, ProxyError> {
558    // Build allowlist from existing site hostnames.
559    let allowlist: HashSet<String> = sites.iter().map(|s| s.host.to_lowercase()).collect();
560
561    // Build the decision function if an ask URL is configured.
562    type DecisionFn = dyn Fn(&str) -> bool + Send + Sync;
563    let decision_func: Option<Arc<DecisionFn>> = if let Some(ref ask_url) = od_config.ask {
564        let url = ask_url.clone();
565        Some(Arc::new(move |domain: &str| {
566            check_ask_url_blocking(&url, domain)
567        }))
568    } else {
569        None
570    };
571
572    // Build the rate limiter if configured.
573    let rate_limit = od_config.rate_limit.map(|max_per_minute| {
574        Arc::new(certon::rate_limiter::RateLimiter::new(
575            max_per_minute as usize,
576            Duration::from_secs(60),
577        ))
578    });
579
580    // Build the obtain function that triggers ACME certificate issuance.
581    let issuer_for_obtain = Arc::clone(issuer);
582    type ObtainFn = dyn Fn(String) -> Pin<Box<dyn std::future::Future<Output = certon::Result<()>> + Send>>
583        + Send
584        + Sync;
585    let obtain_func: Arc<ObtainFn> = Arc::new(move |domain: String| {
586        let issuer = Arc::clone(&issuer_for_obtain);
587        Box::pin(async move {
588            info!(domain = %domain, "on-demand TLS: obtaining certificate");
589            match issuer
590                .issue_for_domains(std::slice::from_ref(&domain))
591                .await
592            {
593                Ok(_cert) => {
594                    info!(domain = %domain, "on-demand TLS: certificate obtained");
595                    Ok(())
596                }
597                Err(e) => {
598                    error!(domain = %domain, error = %e, "on-demand TLS: failed to obtain certificate");
599                    Err(e)
600                }
601            }
602        })
603    });
604
605    Ok(OnDemandConfig {
606        decision_func,
607        host_allowlist: if allowlist.is_empty() {
608            None
609        } else {
610            Some(allowlist)
611        },
612        rate_limit,
613        obtain_func: Some(obtain_func),
614    })
615}
616
617/// Synchronously check the ask URL. This is called from the decision function
618/// which runs synchronously in the rustls resolve path. We use
619/// `tokio::task::block_in_place` to run an async HTTP request, which is
620/// acceptable here since it only runs for on-demand (uncached) requests.
621fn check_ask_url_blocking(ask_url: &str, domain: &str) -> bool {
622    let url = format!("{}?domain={}", ask_url, domain);
623    debug!(url = %url, "on-demand TLS: checking ask URL");
624
625    // Use block_in_place + a short-lived runtime to issue an async request
626    // from this synchronous context.
627    let result = std::panic::catch_unwind(|| {
628        tokio::task::block_in_place(|| {
629            tokio::runtime::Handle::current().block_on(async {
630                let client = reqwest::Client::builder()
631                    .timeout(Duration::from_secs(5))
632                    .build()
633                    .map_err(|e| format!("client build error: {e}"))?;
634                let resp = client
635                    .get(&url)
636                    .send()
637                    .await
638                    .map_err(|e| format!("request error: {e}"))?;
639                Ok::<bool, String>(resp.status().is_success())
640            })
641        })
642    });
643
644    match result {
645        Ok(Ok(allowed)) => {
646            debug!(url = %url, allowed = %allowed, "on-demand TLS: ask URL response");
647            allowed
648        }
649        Ok(Err(e)) => {
650            warn!(url = %url, error = %e, "on-demand TLS: ask URL request failed");
651            false
652        }
653        Err(_) => {
654            warn!(url = %url, "on-demand TLS: ask URL check failed (no runtime)");
655            false
656        }
657    }
658}
659
660// ---------------------------------------------------------------------------
661// SharedMapHttp01Solver
662// ---------------------------------------------------------------------------
663
664/// An HTTP-01 solver that stores challenge tokens in a shared in-memory map
665/// rather than running its own HTTP server.
666///
667/// The gatel HTTP server (port 80) serves challenge responses via the
668/// [`AcmeChallengeHoop`](crate::hoops::acme_challenge::AcmeChallengeHoop),
669/// so there is no need for the solver to bind a separate listener.
670struct SharedMapHttp01Solver {
671    challenges: Arc<tokio::sync::RwLock<HashMap<String, String>>>,
672}
673
674#[async_trait::async_trait]
675impl certon::Solver for SharedMapHttp01Solver {
676    async fn present(&self, _domain: &str, token: &str, key_auth: &str) -> certon::Result<()> {
677        debug!(token = %token, "presenting HTTP-01 challenge token");
678        let mut map = self.challenges.write().await;
679        map.insert(token.to_string(), key_auth.to_string());
680        Ok(())
681    }
682
683    async fn cleanup(&self, _domain: &str, token: &str, _key_auth: &str) -> certon::Result<()> {
684        debug!(token = %token, "cleaning up HTTP-01 challenge token");
685        let mut map = self.challenges.write().await;
686        map.remove(token);
687        Ok(())
688    }
689}
690
691// ---------------------------------------------------------------------------
692// Manual certificate loading
693// ---------------------------------------------------------------------------
694
695/// Load a TLS certificate and private key from PEM files specified in a
696/// [`SiteTlsConfig`].
697///
698/// Uses certon's `Certificate::from_pem_files` for PEM parsing, then
699/// converts to a rustls `CertifiedKey`.
700fn load_manual_cert(site_tls: &SiteTlsConfig) -> Result<Arc<CertifiedKey>, ProxyError> {
701    let cert_path = Path::new(&site_tls.cert);
702    let key_path = Path::new(&site_tls.key);
703
704    let cert = Certificate::from_pem_files(cert_path, key_path).map_err(|e| {
705        ProxyError::Internal(format!(
706            "failed to parse PEM certificate/key ({}, {}): {e}",
707            site_tls.cert, site_tls.key
708        ))
709    })?;
710
711    let certified_key = certon::handshake::cert_to_certified_key(&cert).map_err(|e| {
712        ProxyError::Internal(format!(
713            "failed to convert certificate to CertifiedKey: {e}"
714        ))
715    })?;
716
717    Ok(certified_key)
718}
719
720// ---------------------------------------------------------------------------
721// ServerConfig construction
722// ---------------------------------------------------------------------------
723
724/// Compute the set of protocol versions to enable given optional min/max constraints.
725///
726/// Returns `None` to indicate "use safe defaults".
727fn resolve_protocol_versions(
728    min_version: Option<&str>,
729    max_version: Option<&str>,
730) -> Option<Vec<&'static rustls::SupportedProtocolVersion>> {
731    if min_version.is_none() && max_version.is_none() {
732        return None;
733    }
734
735    // All versions in ascending order: index 0 = TLS 1.2, index 1 = TLS 1.3.
736    let all: [&'static rustls::SupportedProtocolVersion; 2] =
737        [&rustls::version::TLS12, &rustls::version::TLS13];
738
739    let version_index = |v: &str| match v {
740        "1.2" => Some(0usize),
741        "1.3" => Some(1usize),
742        _ => None,
743    };
744
745    let min_idx: usize = min_version.and_then(version_index).unwrap_or(0);
746    let max_idx: usize = max_version.and_then(version_index).unwrap_or(all.len() - 1);
747
748    let versions: Vec<&'static rustls::SupportedProtocolVersion> = all[min_idx..=max_idx].to_vec();
749
750    if versions.is_empty() {
751        None
752    } else {
753        Some(versions)
754    }
755}
756
757/// Build a CryptoProvider with only the selected cipher suites.
758fn build_provider_with_suites(suite_names: &[String]) -> rustls::crypto::CryptoProvider {
759    use rustls::crypto::ring::cipher_suite;
760    // All ring-backed cipher suites.
761    let all_suites: &[rustls::SupportedCipherSuite] = &[
762        cipher_suite::TLS13_AES_256_GCM_SHA384,
763        cipher_suite::TLS13_AES_128_GCM_SHA256,
764        cipher_suite::TLS13_CHACHA20_POLY1305_SHA256,
765        cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
766        cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
767        cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
768        cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
769        cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
770        cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
771    ];
772
773    let selected: Vec<rustls::SupportedCipherSuite> = all_suites
774        .iter()
775        .filter(|s: &&rustls::SupportedCipherSuite| {
776            let name = format!("{:?}", s.suite());
777            suite_names
778                .iter()
779                .any(|n| name.contains(n.as_str()) || n.as_str() == name.as_str())
780        })
781        .copied()
782        .collect();
783
784    let suites = if selected.is_empty() {
785        warn!(
786            "no matching cipher suites found for {:?}, using defaults",
787            suite_names
788        );
789        rustls::crypto::ring::default_provider().cipher_suites
790    } else {
791        selected
792    };
793
794    rustls::crypto::CryptoProvider {
795        cipher_suites: suites,
796        ..rustls::crypto::ring::default_provider()
797    }
798}
799
800/// Filter key-exchange groups to those matching the requested curve names.
801///
802/// Supported names (case-insensitive): `"x25519"`, `"secp256r1"`, `"secp384r1"`.
803/// Returns the default provider's groups if none of the names match.
804fn filter_kx_groups(curve_names: &[String]) -> Vec<&'static dyn rustls::crypto::SupportedKxGroup> {
805    use rustls::crypto::ring::kx_group;
806    let all: &[(&str, &'static dyn rustls::crypto::SupportedKxGroup)] = &[
807        ("x25519", kx_group::X25519),
808        ("secp256r1", kx_group::SECP256R1),
809        ("secp384r1", kx_group::SECP384R1),
810    ];
811    let mut selected: Vec<&'static dyn rustls::crypto::SupportedKxGroup> = Vec::new();
812    for name in curve_names {
813        let lower = name.to_ascii_lowercase();
814        for &(n, group) in all {
815            if n == lower {
816                selected.push(group);
817            }
818        }
819    }
820    if selected.is_empty() {
821        warn!(
822            "no matching ECDH curves for {:?}, using defaults",
823            curve_names
824        );
825        rustls::crypto::ring::default_provider().kx_groups
826    } else {
827        selected
828    }
829}
830
831/// Build a `rustls::ServerConfig` that uses the given resolver and optional
832/// client certificate verifier for mTLS.
833fn build_server_config(
834    resolver: Arc<dyn ResolvesServerCert>,
835    client_verifier: Option<&Arc<dyn rustls::server::danger::ClientCertVerifier>>,
836    tls_config: Option<&TlsConfig>,
837) -> rustls::ServerConfig {
838    // Select the crypto provider (custom cipher suites or default), then
839    // optionally filter ECDH key-exchange groups.
840    let provider = if let Some(cfg) = tls_config {
841        let mut p = if !cfg.cipher_suites.is_empty() {
842            build_provider_with_suites(&cfg.cipher_suites)
843        } else {
844            rustls::crypto::ring::default_provider()
845        };
846        if !cfg.ecdh_curves.is_empty() {
847            p.kx_groups = filter_kx_groups(&cfg.ecdh_curves);
848        }
849        Arc::new(p)
850    } else {
851        Arc::new(rustls::crypto::ring::default_provider())
852    };
853
854    // Determine protocol versions.
855    let versions = tls_config.and_then(|cfg| {
856        resolve_protocol_versions(cfg.min_version.as_deref(), cfg.max_version.as_deref())
857    });
858
859    let builder = if let Some(ref versions) = versions {
860        rustls::ServerConfig::builder_with_provider(provider)
861            .with_protocol_versions(versions)
862            .expect("TLS protocol versions are valid")
863    } else {
864        rustls::ServerConfig::builder_with_provider(provider)
865            .with_safe_default_protocol_versions()
866            .expect("default protocol versions are valid")
867    };
868
869    if let Some(verifier) = client_verifier {
870        builder
871            .with_client_cert_verifier(Arc::clone(verifier))
872            .with_cert_resolver(resolver)
873    } else {
874        builder.with_no_client_auth().with_cert_resolver(resolver)
875    }
876}
877
878// ---------------------------------------------------------------------------
879// DNS provider dispatch
880// ---------------------------------------------------------------------------
881
882/// Decode a base64url or standard base64 encoded HMAC key into raw bytes.
883///
884/// EAB HMAC keys from CAs are typically base64url-encoded (RFC 4648 §5),
885/// which uses `-` and `_` instead of `+` and `/`. We normalise those
886/// characters before running the standard alphabet decoder so that both
887/// encodings are accepted.
888fn base64_decode_hmac(input: &str) -> Result<Vec<u8>, ProxyError> {
889    // Normalise base64url → standard base64 by swapping the two variant chars.
890    let normalised: String = input
891        .trim()
892        .chars()
893        .map(|c| match c {
894            '-' => '+',
895            '_' => '/',
896            other => other,
897        })
898        .collect();
899    decode_base64(&normalised)
900        .ok_or_else(|| ProxyError::Internal("invalid base64 in EAB HMAC key".into()))
901}
902
903/// Minimal base64 decoder (standard alphabet only; call after normalising
904/// base64url characters).
905fn decode_base64(input: &str) -> Option<Vec<u8>> {
906    const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
907
908    let input = input.trim();
909    if input.is_empty() {
910        return Some(Vec::new());
911    }
912
913    let mut output = Vec::with_capacity(input.len() * 3 / 4);
914    let mut buf: u32 = 0;
915    let mut bits: u32 = 0;
916
917    for &b in input.as_bytes() {
918        if b == b'=' {
919            break;
920        }
921        let val = match TABLE.iter().position(|&c| c == b) {
922            Some(v) => v as u32,
923            None => {
924                if b == b'\n' || b == b'\r' || b == b' ' {
925                    continue;
926                }
927                return None;
928            }
929        };
930        buf = (buf << 6) | val;
931        bits += 6;
932        if bits >= 8 {
933            bits -= 8;
934            output.push((buf >> bits) as u8);
935            buf &= (1 << bits) - 1;
936        }
937    }
938
939    Some(output)
940}
941
942/// Dispatch to the concrete DNS provider implementation based on the provider
943/// name in the configuration.
944fn create_dns_provider(
945    cfg: &DnsProviderConfig,
946) -> Result<Box<dyn certon::DnsProvider>, ProxyError> {
947    match cfg.provider.as_str() {
948        "cloudflare" => Ok(Box::new(crate::tls::dns::CloudflareDns::new(cfg)?)),
949        "route53" => Ok(Box::new(crate::tls::dns::Route53Dns::new(cfg)?)),
950        "digitalocean" => Ok(Box::new(crate::tls::dns::DigitalOceanDns::new(cfg)?)),
951        "dnsimple" => Ok(Box::new(crate::tls::dns::DnSimpleDns::new(cfg)?)),
952        "porkbun" => Ok(Box::new(crate::tls::dns::PorkbunDns::new(cfg)?)),
953        "ovh" => Ok(Box::new(crate::tls::dns::OvhDns::new(cfg)?)),
954        "desec" => Ok(Box::new(crate::tls::dns::DesecDns::new(cfg)?)),
955        "bunny" => Ok(Box::new(crate::tls::dns::BunnyDns::new(cfg)?)),
956        "rfc2136" => Ok(Box::new(crate::tls::dns::Rfc2136Dns::new(cfg)?)),
957        other => Err(ProxyError::Internal(format!(
958            "unknown DNS provider: {other}"
959        ))),
960    }
961}
962
963// ---------------------------------------------------------------------------
964// Tests
965// ---------------------------------------------------------------------------
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970
971    #[test]
972    fn build_server_config_creates_valid_config_no_client_auth() {
973        // Use a dummy resolver that always returns None.
974        #[derive(Debug)]
975        struct NullResolver;
976        impl ResolvesServerCert for NullResolver {
977            fn resolve(
978                &self,
979                _client_hello: rustls::server::ClientHello<'_>,
980            ) -> Option<Arc<CertifiedKey>> {
981                None
982            }
983        }
984
985        let config = build_server_config(Arc::new(NullResolver), None, None);
986        // Verify it was constructed (no panic).
987        assert!(config.alpn_protocols.is_empty());
988    }
989
990    #[test]
991    fn build_server_config_with_client_verifier() {
992        #[derive(Debug)]
993        struct NullResolver;
994        impl ResolvesServerCert for NullResolver {
995            fn resolve(
996                &self,
997                _client_hello: rustls::server::ClientHello<'_>,
998            ) -> Option<Arc<CertifiedKey>> {
999                None
1000            }
1001        }
1002
1003        // Build a root store with no certs -- we can't build WebPkiClientVerifier
1004        // with empty roots, so this test just verifies the no-client-auth path.
1005        let config = build_server_config(Arc::new(NullResolver), None, None);
1006        assert!(config.alpn_protocols.is_empty());
1007    }
1008}