Skip to main content

tako_rs_server/builder/
tls_cert.rs

1// Used by both the TLS variants of `TlsCert` (any combination of compio+tls
2// or non-compio+tls) and the rustls config builders below — every reference in
3// this module is gated on the `tls` feature.
4#[cfg(feature = "tls")]
5use std::sync::Arc;
6
7/// Client-authentication policy applied to a TLS server.
8///
9/// Both variants carry the trusted [`rustls::RootCertStore`] used to validate
10/// the client-presented chain. `Optional` allows clients without a cert to
11/// proceed (the application can later inspect the peer certs); `Required`
12/// terminates handshakes that omit a cert.
13#[cfg(feature = "tls")]
14#[derive(Clone)]
15pub enum ClientAuth {
16  /// Verify the client cert if presented; allow connections without one.
17  Optional(Arc<rustls::RootCertStore>),
18  /// Require a valid client cert; reject the handshake otherwise.
19  Required(Arc<rustls::RootCertStore>),
20}
21
22#[cfg(feature = "tls")]
23impl std::fmt::Debug for ClientAuth {
24  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25    match self {
26      ClientAuth::Optional(_) => f.debug_tuple("Optional").field(&"<root_store>").finish(),
27      ClientAuth::Required(_) => f.debug_tuple("Required").field(&"<root_store>").finish(),
28    }
29  }
30}
31
32/// Optional TLS material the builder can attach to a TLS-mode server.
33///
34/// Variants:
35/// - [`TlsCert::PemPaths`] — load cert and key from disk on every spawn.
36/// - [`TlsCert::Der`] — pre-loaded DER cert chain + key.
37/// - [`TlsCert::Resolver`] — user-supplied [`rustls::server::ResolvesServerCert`]
38///   for SNI multi-cert serving or hot-reloadable certificates (see
39///   [`ReloadableResolver`]).
40#[derive(Clone)]
41pub enum TlsCert {
42  /// Filesystem paths for cert + key PEM files.
43  PemPaths {
44    /// Path to the PEM-encoded certificate chain.
45    cert_path: String,
46    /// Path to the PEM-encoded private key.
47    key_path: String,
48    /// Optional mTLS policy.
49    #[cfg(feature = "tls")]
50    client_auth: Option<ClientAuth>,
51  },
52  /// Pre-loaded DER cert chain + key. Useful when certs come from secret
53  /// storage rather than the filesystem.
54  #[cfg(feature = "tls")]
55  Der {
56    /// DER-encoded certificate chain (leaf first).
57    certs: Arc<Vec<rustls::pki_types::CertificateDer<'static>>>,
58    /// DER-encoded private key.
59    key: Arc<rustls::pki_types::PrivateKeyDer<'static>>,
60    /// Optional mTLS policy.
61    client_auth: Option<ClientAuth>,
62  },
63  /// User-supplied certificate resolver. The most flexible variant — drives
64  /// SNI multi-cert serving, hot reload (see [`ReloadableResolver`]), and any
65  /// custom logic that picks a cert per client-hello.
66  #[cfg(feature = "tls")]
67  Resolver {
68    /// The resolver used by rustls to pick a cert per handshake.
69    resolver: Arc<dyn rustls::server::ResolvesServerCert>,
70    /// Optional mTLS policy.
71    client_auth: Option<ClientAuth>,
72  },
73}
74
75impl std::fmt::Debug for TlsCert {
76  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77    match self {
78      TlsCert::PemPaths {
79        cert_path,
80        key_path,
81        ..
82      } => f
83        .debug_struct("PemPaths")
84        .field("cert_path", cert_path)
85        .field("key_path", key_path)
86        .finish_non_exhaustive(),
87      #[cfg(feature = "tls")]
88      TlsCert::Der { client_auth, .. } => f
89        .debug_struct("Der")
90        .field("client_auth", client_auth)
91        .finish_non_exhaustive(),
92      #[cfg(feature = "tls")]
93      TlsCert::Resolver { client_auth, .. } => f
94        .debug_struct("Resolver")
95        .field("client_auth", client_auth)
96        .finish_non_exhaustive(),
97    }
98  }
99}
100
101impl TlsCert {
102  /// Construct from filesystem paths (PEM cert + PEM key).
103  pub fn pem_paths(cert: impl Into<String>, key: impl Into<String>) -> Self {
104    Self::PemPaths {
105      cert_path: cert.into(),
106      key_path: key.into(),
107      #[cfg(feature = "tls")]
108      client_auth: None,
109    }
110  }
111
112  /// Like [`TlsCert::pem_paths`] with an attached mTLS policy.
113  #[cfg(feature = "tls")]
114  pub fn pem_paths_with_client_auth(
115    cert: impl Into<String>,
116    key: impl Into<String>,
117    client_auth: ClientAuth,
118  ) -> Self {
119    Self::PemPaths {
120      cert_path: cert.into(),
121      key_path: key.into(),
122      client_auth: Some(client_auth),
123    }
124  }
125
126  /// Construct from pre-loaded DER cert chain + key.
127  #[cfg(feature = "tls")]
128  pub fn der(
129    certs: Vec<rustls::pki_types::CertificateDer<'static>>,
130    key: rustls::pki_types::PrivateKeyDer<'static>,
131  ) -> Self {
132    Self::Der {
133      certs: Arc::new(certs),
134      key: Arc::new(key),
135      client_auth: None,
136    }
137  }
138
139  /// Construct from a user-supplied certificate resolver. This is the entry
140  /// point for SNI multi-cert servers and hot-reload (see [`ReloadableResolver`]).
141  #[cfg(feature = "tls")]
142  pub fn resolver(resolver: Arc<dyn rustls::server::ResolvesServerCert>) -> Self {
143    Self::Resolver {
144      resolver,
145      client_auth: None,
146    }
147  }
148
149  /// Returns a clone of the resolver (or no-op for static cert variants).
150  ///
151  /// Useful when the caller wants to swap the live cert at runtime — they pass
152  /// in a [`ReloadableResolver`] via [`TlsCert::resolver`] and keep the `Arc`
153  /// for later `.reload_*()` calls.
154  #[cfg(feature = "tls")]
155  pub fn with_client_auth(mut self, auth: ClientAuth) -> Self {
156    match &mut self {
157      TlsCert::PemPaths { client_auth, .. }
158      | TlsCert::Der { client_auth, .. }
159      | TlsCert::Resolver { client_auth, .. } => *client_auth = Some(auth),
160    }
161    self
162  }
163}
164
165/// A `ResolvesServerCert` whose backing [`rustls::sign::CertifiedKey`] can be
166/// swapped at runtime via [`ReloadableResolver::reload_from_pem`].
167///
168/// Backed by [`arc_swap::ArcSwap`], the swap is atomic and lock-free on the
169/// hot path (one `Arc` clone per TLS handshake). Use it via
170/// [`TlsCert::resolver`] and keep the returned `Arc` so callers can trigger
171/// reloads from anywhere (file watcher, signal handler, admin endpoint, …).
172///
173/// # Example
174///
175/// ```rust,no_run
176/// # #[cfg(feature = "tls")]
177/// # async fn _example() -> anyhow::Result<()> {
178/// use std::sync::Arc;
179/// use tako_rs_server::{ReloadableResolver, Server, TlsCert};
180///
181/// let resolver = Arc::new(ReloadableResolver::from_pem("cert.pem", "key.pem")?);
182/// let cert = TlsCert::resolver(resolver.clone());
183/// let server = Server::builder().tls(cert).build();
184/// // Later, after a cert rotation:
185/// resolver.reload_from_pem("cert.pem", "key.pem")?;
186/// # Ok(())
187/// # }
188/// ```
189#[cfg(feature = "tls")]
190pub struct ReloadableResolver {
191  current: arc_swap::ArcSwap<rustls::sign::CertifiedKey>,
192}
193
194#[cfg(feature = "tls")]
195impl std::fmt::Debug for ReloadableResolver {
196  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197    f.debug_struct("ReloadableResolver").finish_non_exhaustive()
198  }
199}
200
201#[cfg(feature = "tls")]
202impl ReloadableResolver {
203  /// Construct from on-disk PEM files.
204  pub fn from_pem(cert_path: &str, key_path: &str) -> anyhow::Result<Self> {
205    let ck = build_certified_key(cert_path, key_path)?;
206    Ok(Self {
207      current: arc_swap::ArcSwap::from_pointee(ck),
208    })
209  }
210
211  /// Atomically swap to a new cert + key loaded from the given PEM files.
212  ///
213  /// Hot-path TLS handshakes pick up the new cert on the next `resolve` call
214  /// without dropping any in-flight session.
215  pub fn reload_from_pem(&self, cert_path: &str, key_path: &str) -> anyhow::Result<()> {
216    let ck = build_certified_key(cert_path, key_path)?;
217    self.current.store(Arc::new(ck));
218    Ok(())
219  }
220
221  /// Atomically swap to a pre-built [`rustls::sign::CertifiedKey`].
222  pub fn reload(&self, ck: rustls::sign::CertifiedKey) {
223    self.current.store(Arc::new(ck));
224  }
225}
226
227#[cfg(feature = "tls")]
228impl rustls::server::ResolvesServerCert for ReloadableResolver {
229  fn resolve(
230    &self,
231    _client_hello: rustls::server::ClientHello<'_>,
232  ) -> Option<Arc<rustls::sign::CertifiedKey>> {
233    Some(self.current.load_full())
234  }
235}
236
237#[cfg(feature = "tls")]
238fn build_certified_key(
239  cert_path: &str,
240  key_path: &str,
241) -> anyhow::Result<rustls::sign::CertifiedKey> {
242  let certs = tako_rs_core::tls::load_certs(cert_path)?;
243  let key = tako_rs_core::tls::load_key(key_path)?;
244
245  // Use whatever rustls CryptoProvider is installed — server_h3 / webtransport
246  // install `ring` on first use; pure-TLS apps may not have installed any yet.
247  // Opportunistically install rustls's default backend (`aws-lc-rs`) if the
248  // global slot is still empty, so callers don't have to wire it themselves.
249  //
250  // SRV-08: if a provider was ALREADY installed by another part of the
251  // process (commonly `ring` via h3/webtransport bootstrap), this code uses
252  // it as-is and `load_private_key` runs against that backend. Cross-
253  // provider key loading is supported in principle, but signature output
254  // depends on which backend signs — operators surprised by behavior diff
255  // need to know we did not install aws-lc-rs in that case.
256  let we_installed = if rustls::crypto::CryptoProvider::get_default().is_none() {
257    rustls::crypto::aws_lc_rs::default_provider()
258      .install_default()
259      .is_ok()
260  } else {
261    false
262  };
263  if !we_installed {
264    // Fire a one-shot warning so it shows up once in the log instead of
265    // once per certified-key build (which could be thousands per process
266    // in tests / hot-reload). Static `Once` keeps this lock-free after
267    // the first call.
268    static WARNED: std::sync::Once = std::sync::Once::new();
269    WARNED.call_once(|| {
270      tracing::warn!(
271        "tako-server: a rustls CryptoProvider was already installed before \
272         `build_certified_key` ran — Tako will use that provider for key \
273         loading instead of installing aws-lc-rs. If signing behavior is \
274         not what you expect (e.g. h3 installed `ring` first), pin the \
275         provider at process startup with `rustls::crypto::aws_lc_rs::\
276         default_provider().install_default()` BEFORE constructing the \
277         server."
278      );
279    });
280  }
281  let provider = rustls::crypto::CryptoProvider::get_default().ok_or_else(|| {
282    anyhow::anyhow!(
283      "no rustls CryptoProvider installed — enable rustls's `aws_lc_rs` or `ring` feature"
284    )
285  })?;
286  let signer = provider
287    .key_provider
288    .load_private_key(key)
289    .map_err(|e| anyhow::anyhow!("failed to load signing key from '{key_path}': {e}"))?;
290  Ok(rustls::sign::CertifiedKey::new(certs, signer))
291}
292
293/// Build an `Arc<rustls::ServerConfig>` from a [`TlsCert`] and the desired
294/// ALPN protocol list.
295///
296/// Internal helper used by every `Server::spawn_*` TLS-mode method. Exposed
297/// so embedders can build the rustls config the same way Tako does and pass
298/// it to the lower-level `serve_*_with_rustls_config_*` entrypoints.
299#[cfg(feature = "tls")]
300pub fn build_rustls_server_config(
301  cert: &TlsCert,
302  alpn: Vec<Vec<u8>>,
303) -> anyhow::Result<Arc<rustls::ServerConfig>> {
304  use rustls::ServerConfig as RustlsServerConfig;
305
306  let builder = RustlsServerConfig::builder();
307
308  // Resolve the client-auth verifier first. `with_no_client_auth` and
309  // `with_client_cert_verifier` produce the same `ConfigBuilder<...>` next
310  // step, so we can branch cleanly here.
311  let client_auth = match cert {
312    TlsCert::PemPaths { client_auth, .. }
313    | TlsCert::Der { client_auth, .. }
314    | TlsCert::Resolver { client_auth, .. } => client_auth.clone(),
315  };
316
317  let builder_with_auth = match client_auth {
318    Some(ClientAuth::Optional(roots)) => {
319      let verifier = rustls::server::WebPkiClientVerifier::builder(roots)
320        .allow_unauthenticated()
321        .build()
322        .map_err(|e| anyhow::anyhow!("WebPkiClientVerifier build failed: {e}"))?;
323      builder.with_client_cert_verifier(verifier)
324    }
325    Some(ClientAuth::Required(roots)) => {
326      let verifier = rustls::server::WebPkiClientVerifier::builder(roots)
327        .build()
328        .map_err(|e| anyhow::anyhow!("WebPkiClientVerifier build failed: {e}"))?;
329      builder.with_client_cert_verifier(verifier)
330    }
331    None => builder.with_no_client_auth(),
332  };
333
334  let mut config = match cert {
335    TlsCert::PemPaths {
336      cert_path,
337      key_path,
338      ..
339    } => {
340      let certs = tako_rs_core::tls::load_certs(cert_path)?;
341      let key = tako_rs_core::tls::load_key(key_path)?;
342      builder_with_auth
343        .with_single_cert(certs, key)
344        .map_err(|e| anyhow::anyhow!("rustls config build failed: {e}"))?
345    }
346    TlsCert::Der { certs, key, .. } => {
347      let certs = certs.as_ref().clone();
348      let key = key.as_ref().clone_key();
349      builder_with_auth
350        .with_single_cert(certs, key)
351        .map_err(|e| anyhow::anyhow!("rustls config build failed: {e}"))?
352    }
353    TlsCert::Resolver { resolver, .. } => builder_with_auth.with_cert_resolver(resolver.clone()),
354  };
355
356  config.alpn_protocols = alpn;
357  // Defense in depth against H3 0-RTT replay: when ALPN advertises h3 the
358  // resulting config will end up driving a quinn endpoint, and Tako has no
359  // replay cache on the request path. `server_h3::run_with_rustls_config`
360  // also clears this defensively, but doing it at construction prevents the
361  // window where the unprotected config exists in caller memory before being
362  // handed to `serve_h3_*`.
363  if config.alpn_protocols.iter().any(|p| p.as_slice() == b"h3") {
364    config.max_early_data_size = 0;
365  }
366  Ok(Arc::new(config))
367}