Skip to main content

tako_rs_server/builder/
tls_cert.rs

1#[cfg(feature = "tls")]
2use std::sync::Arc;
3
4/// mTLS policy: client certificates are verified against the [`rustls::RootCertStore`].
5#[cfg(feature = "tls")]
6#[derive(Clone)]
7pub enum ClientAuth {
8  /// Verify the client cert if presented; allow connections without one.
9  Optional(Arc<rustls::RootCertStore>),
10  /// Require a valid client cert; reject the handshake otherwise.
11  Required(Arc<rustls::RootCertStore>),
12}
13
14#[cfg(feature = "tls")]
15impl std::fmt::Debug for ClientAuth {
16  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17    match self {
18      ClientAuth::Optional(_) => f.debug_tuple("Optional").field(&"<root_store>").finish(),
19      ClientAuth::Required(_) => f.debug_tuple("Required").field(&"<root_store>").finish(),
20    }
21  }
22}
23
24/// Certificate source for `spawn_tls` and `spawn_h3`; `Der` and `Resolver` need the `tls` feature.
25#[derive(Clone)]
26pub enum TlsCert {
27  /// Filesystem paths for cert + key PEM files.
28  PemPaths {
29    /// Path to the PEM-encoded certificate chain.
30    cert_path: String,
31    /// Path to the PEM-encoded private key.
32    key_path: String,
33    /// Optional mTLS policy.
34    #[cfg(feature = "tls")]
35    client_auth: Option<ClientAuth>,
36  },
37  /// In-memory DER chain and key, e.g. from a secret store.
38  #[cfg(feature = "tls")]
39  Der {
40    /// DER-encoded certificate chain (leaf first).
41    certs: Arc<Vec<rustls::pki_types::CertificateDer<'static>>>,
42    /// DER-encoded private key.
43    key: Arc<rustls::pki_types::PrivateKeyDer<'static>>,
44    /// Optional mTLS policy.
45    client_auth: Option<ClientAuth>,
46  },
47  /// Picks a certificate per handshake: SNI multi-cert serving or hot reload through
48  /// [`ReloadableResolver`].
49  #[cfg(feature = "tls")]
50  Resolver {
51    /// The resolver used by rustls to pick a cert per handshake.
52    resolver: Arc<dyn rustls::server::ResolvesServerCert>,
53    /// Optional mTLS policy.
54    client_auth: Option<ClientAuth>,
55  },
56}
57
58impl std::fmt::Debug for TlsCert {
59  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60    match self {
61      TlsCert::PemPaths {
62        cert_path,
63        key_path,
64        ..
65      } => f
66        .debug_struct("PemPaths")
67        .field("cert_path", cert_path)
68        .field("key_path", key_path)
69        .finish_non_exhaustive(),
70      #[cfg(feature = "tls")]
71      TlsCert::Der { client_auth, .. } => f
72        .debug_struct("Der")
73        .field("client_auth", client_auth)
74        .finish_non_exhaustive(),
75      #[cfg(feature = "tls")]
76      TlsCert::Resolver { client_auth, .. } => f
77        .debug_struct("Resolver")
78        .field("client_auth", client_auth)
79        .finish_non_exhaustive(),
80    }
81  }
82}
83
84impl TlsCert {
85  /// PEM certificate chain and key files, read when the server starts.
86  pub fn pem_paths(cert: impl Into<String>, key: impl Into<String>) -> Self {
87    Self::PemPaths {
88      cert_path: cert.into(),
89      key_path: key.into(),
90      #[cfg(feature = "tls")]
91      client_auth: None,
92    }
93  }
94
95  /// Like [`TlsCert::pem_paths`] with an attached mTLS policy.
96  #[cfg(feature = "tls")]
97  pub fn pem_paths_with_client_auth(
98    cert: impl Into<String>,
99    key: impl Into<String>,
100    client_auth: ClientAuth,
101  ) -> Self {
102    Self::PemPaths {
103      cert_path: cert.into(),
104      key_path: key.into(),
105      client_auth: Some(client_auth),
106    }
107  }
108
109  /// In-memory DER chain (leaf first) and key.
110  #[cfg(feature = "tls")]
111  pub fn der(
112    certs: Vec<rustls::pki_types::CertificateDer<'static>>,
113    key: rustls::pki_types::PrivateKeyDer<'static>,
114  ) -> Self {
115    Self::Der {
116      certs: Arc::new(certs),
117      key: Arc::new(key),
118      client_auth: None,
119    }
120  }
121
122  /// Certificate resolver for SNI multi-cert servers or hot reload (see [`ReloadableResolver`]).
123  #[cfg(feature = "tls")]
124  pub fn resolver(resolver: Arc<dyn rustls::server::ResolvesServerCert>) -> Self {
125    Self::Resolver {
126      resolver,
127      client_auth: None,
128    }
129  }
130
131  /// Replaces the mTLS policy of any variant.
132  #[cfg(feature = "tls")]
133  pub fn with_client_auth(mut self, auth: ClientAuth) -> Self {
134    match &mut self {
135      TlsCert::PemPaths { client_auth, .. }
136      | TlsCert::Der { client_auth, .. }
137      | TlsCert::Resolver { client_auth, .. } => *client_auth = Some(auth),
138    }
139    self
140  }
141}
142
143/// Certificate resolver whose key can be swapped at runtime; keep the `Arc` passed to
144/// [`TlsCert::resolver`] to reload. Each handshake costs one lock-free `Arc` clone.
145///
146/// # Example
147///
148/// ```rust,no_run
149/// # #[cfg(feature = "tls")]
150/// # async fn _example() -> anyhow::Result<()> {
151/// use std::sync::Arc;
152/// use tako_rs_server::{ReloadableResolver, TlsCert};
153///
154/// let resolver = Arc::new(ReloadableResolver::from_pem("cert.pem", "key.pem")?);
155/// let cert = TlsCert::resolver(resolver.clone());
156/// # #[cfg(not(feature = "compio"))]
157/// let server = tako_rs_server::Server::builder().tls(cert).build();
158/// # #[cfg(feature = "compio-tls")]
159/// # let server = tako_rs_server::CompioServer::builder().tls(cert).build();
160/// // Later, after a cert rotation:
161/// resolver.reload_from_pem("cert.pem", "key.pem")?;
162/// # Ok(())
163/// # }
164/// ```
165#[cfg(feature = "tls")]
166pub struct ReloadableResolver {
167  current: arc_swap::ArcSwap<rustls::sign::CertifiedKey>,
168}
169
170#[cfg(feature = "tls")]
171impl std::fmt::Debug for ReloadableResolver {
172  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173    f.debug_struct("ReloadableResolver").finish_non_exhaustive()
174  }
175}
176
177#[cfg(feature = "tls")]
178impl ReloadableResolver {
179  /// Loads a certificate chain and key from PEM files.
180  pub fn from_pem(cert_path: &str, key_path: &str) -> anyhow::Result<Self> {
181    let ck = build_certified_key(cert_path, key_path)?;
182    Ok(Self {
183      current: arc_swap::ArcSwap::from_pointee(ck),
184    })
185  }
186
187  /// Atomically swaps in a key loaded from PEM files; established sessions are unaffected and
188  /// new handshakes use it.
189  pub fn reload_from_pem(&self, cert_path: &str, key_path: &str) -> anyhow::Result<()> {
190    let ck = build_certified_key(cert_path, key_path)?;
191    self.current.store(Arc::new(ck));
192    Ok(())
193  }
194
195  /// Atomically swaps in a prebuilt [`rustls::sign::CertifiedKey`].
196  pub fn reload(&self, ck: rustls::sign::CertifiedKey) {
197    self.current.store(Arc::new(ck));
198  }
199}
200
201#[cfg(feature = "tls")]
202impl rustls::server::ResolvesServerCert for ReloadableResolver {
203  fn resolve(
204    &self,
205    _client_hello: rustls::server::ClientHello<'_>,
206  ) -> Option<Arc<rustls::sign::CertifiedKey>> {
207    Some(self.current.load_full())
208  }
209}
210
211#[cfg(feature = "tls")]
212fn build_certified_key(
213  cert_path: &str,
214  key_path: &str,
215) -> anyhow::Result<rustls::sign::CertifiedKey> {
216  let certs = tako_rs_core::tls::load_certs(cert_path)?;
217  let key = tako_rs_core::tls::load_key(key_path)?;
218
219  // Installs aws-lc-rs only when no provider exists yet; otherwise the earlier one, e.g. ring,
220  // loads and signs with the key, which the one-time warning below reports.
221  let we_installed = if rustls::crypto::CryptoProvider::get_default().is_none() {
222    rustls::crypto::aws_lc_rs::default_provider()
223      .install_default()
224      .is_ok()
225  } else {
226    false
227  };
228  if !we_installed {
229    static WARNED: std::sync::Once = std::sync::Once::new();
230    WARNED.call_once(|| {
231      tracing::warn!(
232        "tako-server: a rustls CryptoProvider was already installed before \
233         `build_certified_key` ran — Tako will use that provider for key \
234         loading instead of installing aws-lc-rs. If signing behavior is \
235         not what you expect (e.g. h3 installed `ring` first), pin the \
236         provider at process startup with `rustls::crypto::aws_lc_rs::\
237         default_provider().install_default()` BEFORE constructing the \
238         server."
239      );
240    });
241  }
242  let provider = rustls::crypto::CryptoProvider::get_default().ok_or_else(|| {
243    anyhow::anyhow!(
244      "no rustls CryptoProvider installed — enable rustls's `aws_lc_rs` or `ring` feature"
245    )
246  })?;
247  let signer = provider
248    .key_provider
249    .load_private_key(key)
250    .map_err(|e| anyhow::anyhow!("failed to load signing key from '{key_path}': {e}"))?;
251  Ok(rustls::sign::CertifiedKey::new(certs, signer))
252}
253
254/// Builds the rustls config the TLS spawn methods use, for the `serve_*_with_rustls_config`
255/// functions. With `h3` in `alpn`, 0-RTT early data is disabled.
256#[cfg(feature = "tls")]
257pub fn build_rustls_server_config(
258  cert: &TlsCert,
259  alpn: Vec<Vec<u8>>,
260) -> anyhow::Result<Arc<rustls::ServerConfig>> {
261  use rustls::ServerConfig as RustlsServerConfig;
262
263  // With `http3`, quinn compiles rustls's ring provider next to the default
264  // aws-lc-rs one, and `ServerConfig::builder()` then refuses to choose.
265  if rustls::crypto::CryptoProvider::get_default().is_none() {
266    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
267  }
268  let builder = RustlsServerConfig::builder();
269
270  let client_auth = match cert {
271    TlsCert::PemPaths { client_auth, .. }
272    | TlsCert::Der { client_auth, .. }
273    | TlsCert::Resolver { client_auth, .. } => client_auth.clone(),
274  };
275
276  let builder_with_auth = match client_auth {
277    Some(ClientAuth::Optional(roots)) => {
278      let verifier = rustls::server::WebPkiClientVerifier::builder(roots)
279        .allow_unauthenticated()
280        .build()
281        .map_err(|e| anyhow::anyhow!("WebPkiClientVerifier build failed: {e}"))?;
282      builder.with_client_cert_verifier(verifier)
283    }
284    Some(ClientAuth::Required(roots)) => {
285      let verifier = rustls::server::WebPkiClientVerifier::builder(roots)
286        .build()
287        .map_err(|e| anyhow::anyhow!("WebPkiClientVerifier build failed: {e}"))?;
288      builder.with_client_cert_verifier(verifier)
289    }
290    None => builder.with_no_client_auth(),
291  };
292
293  let mut config = match cert {
294    TlsCert::PemPaths {
295      cert_path,
296      key_path,
297      ..
298    } => {
299      let certs = tako_rs_core::tls::load_certs(cert_path)?;
300      let key = tako_rs_core::tls::load_key(key_path)?;
301      builder_with_auth
302        .with_single_cert(certs, key)
303        .map_err(|e| anyhow::anyhow!("rustls config build failed: {e}"))?
304    }
305    TlsCert::Der { certs, key, .. } => {
306      let certs = certs.as_ref().clone();
307      let key = key.as_ref().clone_key();
308      builder_with_auth
309        .with_single_cert(certs, key)
310        .map_err(|e| anyhow::anyhow!("rustls config build failed: {e}"))?
311    }
312    TlsCert::Resolver { resolver, .. } => builder_with_auth.with_cert_resolver(resolver.clone()),
313  };
314
315  config.alpn_protocols = alpn;
316  // Nothing protects requests from 0-RTT replay, so an h3 config never accepts early data.
317  if config.alpn_protocols.iter().any(|p| p.as_slice() == b"h3") {
318    config.max_early_data_size = 0;
319  }
320  Ok(Arc::new(config))
321}