Skip to main content

solti_tls/
server.rs

1//! # Server-side TLS configuration.
2//!
3//! [`ServerTlsConfig`] (built via [`ServerTlsConfigBuilder`]) describes a TLS listener:
4//! the server's own cert/key, an optional client-CA bundle that turns on **mandatory** mTLS, and ALPN.
5//! [`ServerTlsConfig::into_rustls_config`] performs the I/O + parsing and yields a [`rustls::ServerConfig`].
6
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use crate::{PemSource, TlsError};
11
12/// Server-side TLS configuration.
13///
14/// Construct via [`ServerTlsConfig::builder`].
15///
16/// ## Security
17///
18/// `key` (and `cert`/`client_ca`) are held as [`PemSource`]; the `Bytes` variant keeps the raw private key.
19/// The derived `Debug` redacts those bytes (see [`PemSource`]), so logging this struct will not leak the key, but the key is **not** zeroed while the config is alive.
20///
21/// ## Also
22///
23/// - [`ClientTlsConfig`](crate::ClientTlsConfig) - the peer side.
24/// - [`ServerTlsConfigBuilder`] - the builder.
25/// - [`PemSource`], [`TlsError`].
26#[derive(Debug, Clone)]
27pub struct ServerTlsConfig {
28    /// Server certificate chain (leaf first).
29    pub cert: PemSource,
30    /// Server private key (PKCS#8, PKCS#1, or SEC1).
31    pub key: PemSource,
32    /// Trusted CA bundle for verifying client certificates (mTLS).
33    /// `None` = standard TLS (no client cert required).
34    pub client_ca: Option<PemSource>,
35    /// ALPN protocol list, in preference order (e.g. `[b"h2"]` for gRPC).
36    /// Empty = no ALPN negotiation requested.
37    pub alpn: Vec<Vec<u8>>,
38}
39
40impl ServerTlsConfig {
41    /// Start a new builder.
42    pub fn builder() -> ServerTlsConfigBuilder {
43        ServerTlsConfigBuilder::default()
44    }
45
46    /// Build a [`rustls::ServerConfig`] from this configuration.
47    ///
48    /// Reads the PEM sources (disk or memory), parses the cert chain and key, optionally constructs a `WebPkiClientVerifier` for mTLS, and applies ALPN.
49    /// Auto-installs the `ring` [`CryptoProvider`](crate::ensure_default_provider) if none is set process-wide.
50    ///
51    /// ## Security
52    ///
53    /// The server always presents `cert` + `key`.
54    /// If `client_ca` is set, client authentication is **mandatory**:
55    /// the server demands a client certificate chaining to that CA and rejects unauthenticated clients at the handshake (`WebPkiClientVerifier` defaults to deny-anonymous).
56    ///
57    /// Server *hostname* is not this method's concern: it is the client that verifies the server's identity.
58    ///
59    /// ## Errors
60    ///
61    /// - [`TlsError::Io`]: PEM read
62    /// - [`TlsError::NoCertificates`] / [`TlsError::NoPrivateKey`]: parse
63    /// - [`TlsError::ClientVerifier`]: mTLS trust-anchor build
64    /// - [`TlsError::Rustls`]: e.g. cert/key mismatch
65    pub fn into_rustls_config(self) -> Result<rustls::ServerConfig, TlsError> {
66        crate::ensure_default_provider();
67
68        let cert_bytes = self.cert.read()?;
69        let key_bytes = self.key.read()?;
70
71        let certs = crate::load_certs_from_pem(cert_bytes.as_slice())?;
72        let key = crate::load_key_from_pem(key_bytes.as_slice())?;
73
74        let builder = rustls::ServerConfig::builder();
75        let server_builder = match self.client_ca {
76            Some(ca_src) => {
77                let ca_bytes = ca_src.read()?;
78                let ca_certs = crate::load_certs_from_pem(ca_bytes.as_slice())?;
79                let mut roots = rustls::RootCertStore::empty();
80                for ca in ca_certs {
81                    roots.add(ca)?;
82                }
83                let verifier =
84                    rustls::server::WebPkiClientVerifier::builder(Arc::new(roots)).build()?;
85                builder.with_client_cert_verifier(verifier)
86            }
87            None => builder.with_no_client_auth(),
88        };
89
90        let mut config = server_builder.with_single_cert(certs, key)?;
91        config.alpn_protocols = self.alpn;
92        Ok(config)
93    }
94}
95
96/// Incremental builder for [`ServerTlsConfig`].
97#[derive(Debug, Default, Clone)]
98pub struct ServerTlsConfigBuilder {
99    cert: Option<PemSource>,
100    key: Option<PemSource>,
101    client_ca: Option<PemSource>,
102    alpn: Vec<Vec<u8>>,
103}
104
105impl ServerTlsConfigBuilder {
106    /// Set the server cert chain from any [`PemSource`].
107    pub fn cert(mut self, src: PemSource) -> Self {
108        self.cert = Some(src);
109        self
110    }
111
112    /// Set the server private key from any [`PemSource`].
113    pub fn key(mut self, src: PemSource) -> Self {
114        self.key = Some(src);
115        self
116    }
117
118    /// Set the ALPN protocol list, in preference order.
119    ///
120    /// Pass `["h2"]` for gRPC-only, `["h2", "http/1.1"]` for axum HTTP.
121    /// Default is empty (no ALPN negotiation).
122    pub fn with_alpn<I, S>(mut self, protocols: I) -> Self
123    where
124        I: IntoIterator<Item = S>,
125        S: Into<Vec<u8>>,
126    {
127        self.alpn = protocols.into_iter().map(Into::into).collect();
128        self
129    }
130
131    /// Convenience: set the server cert chain from a file path.
132    pub fn cert_pem_file(self, path: impl Into<PathBuf>) -> Self {
133        self.cert(PemSource::Path(path.into()))
134    }
135
136    /// Convenience: set the server cert chain from in-memory bytes.
137    pub fn cert_pem_bytes(self, bytes: impl Into<Vec<u8>>) -> Self {
138        self.cert(PemSource::Bytes(bytes.into()))
139    }
140
141    /// Convenience: set the server private key from a file path.
142    pub fn key_pem_file(self, path: impl Into<PathBuf>) -> Self {
143        self.key(PemSource::Path(path.into()))
144    }
145
146    /// Convenience: set the server private key from in-memory bytes.
147    pub fn key_pem_bytes(self, bytes: impl Into<Vec<u8>>) -> Self {
148        self.key(PemSource::Bytes(bytes.into()))
149    }
150
151    /// Convenience: enable mTLS with a CA bundle from a file path.
152    pub fn require_client_ca_pem_file(self, path: impl Into<PathBuf>) -> Self {
153        self.require_client_ca(PemSource::Path(path.into()))
154    }
155
156    /// Convenience: enable mTLS with a CA bundle from in-memory bytes.
157    pub fn require_client_ca_pem_bytes(self, bytes: impl Into<Vec<u8>>) -> Self {
158        self.require_client_ca(PemSource::Bytes(bytes.into()))
159    }
160
161    /// Require client certificates signed by this CA bundle (turns on mTLS).
162    pub fn require_client_ca(mut self, src: PemSource) -> Self {
163        self.client_ca = Some(src);
164        self
165    }
166
167    /// Finalize the configuration.
168    ///
169    /// Validates that `cert` and `key` are present (else [`TlsError::MissingField`]).
170    /// Does no I/O - the PEM sources are read later by [`ServerTlsConfig::into_rustls_config`].
171    ///
172    /// ## Example
173    ///
174    /// ```
175    /// use solti_tls::ServerTlsConfig;
176    ///
177    /// let cfg = ServerTlsConfig::builder()
178    ///     .cert_pem_bytes(b"-----BEGIN CERTIFICATE-----\n...".to_vec())
179    ///     .key_pem_bytes(b"-----BEGIN PRIVATE KEY-----\n...".to_vec())
180    ///     .with_alpn(["h2"])
181    ///     .build()
182    ///     .unwrap();
183    /// assert!(cfg.client_ca.is_none()); // standard TLS until require_client_ca
184    /// ```
185    pub fn build(self) -> Result<ServerTlsConfig, TlsError> {
186        let cert = self.cert.ok_or(TlsError::MissingField("cert"))?;
187        let key = self.key.ok_or(TlsError::MissingField("key"))?;
188        Ok(ServerTlsConfig {
189            cert,
190            key,
191            client_ca: self.client_ca,
192            alpn: self.alpn,
193        })
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::PemSource;
201
202    #[test]
203    fn debug_of_config_does_not_leak_key_bytes() {
204        let cfg = ServerTlsConfig::builder()
205            .cert_pem_bytes(vec![10, 20, 30])
206            .key_pem_bytes(vec![201, 202, 203])
207            .build()
208            .unwrap();
209        let rendered = format!("{cfg:?}");
210        assert!(
211            !rendered.contains("201") && !rendered.contains("202"),
212            "config Debug must not leak key bytes: {rendered}"
213        );
214        assert!(
215            rendered.contains("redacted"),
216            "expected redaction marker: {rendered}"
217        );
218    }
219
220    #[test]
221    fn builder_returns_config_when_cert_and_key_provided() {
222        let cfg = ServerTlsConfig::builder()
223            .cert_pem_bytes(b"--FAKE CERT--".to_vec())
224            .key_pem_bytes(b"--FAKE KEY--".to_vec())
225            .build()
226            .unwrap();
227        assert!(matches!(cfg.cert, PemSource::Bytes(_)));
228        assert!(matches!(cfg.key, PemSource::Bytes(_)));
229    }
230
231    #[test]
232    fn builder_errors_when_cert_is_missing() {
233        let err = ServerTlsConfig::builder()
234            .key_pem_bytes(vec![1])
235            .build()
236            .unwrap_err();
237        assert!(matches!(err, TlsError::MissingField("cert")));
238    }
239
240    #[test]
241    fn builder_errors_when_key_is_missing() {
242        let err = ServerTlsConfig::builder()
243            .cert_pem_bytes(vec![1])
244            .build()
245            .unwrap_err();
246        assert!(matches!(err, TlsError::MissingField("key")));
247    }
248
249    #[test]
250    fn cert_pem_file_creates_path_source() {
251        let cfg = ServerTlsConfig::builder()
252            .cert_pem_file("/etc/server.crt")
253            .key_pem_bytes(vec![1])
254            .build()
255            .unwrap();
256        assert!(matches!(cfg.cert, PemSource::Path(_)));
257    }
258
259    #[test]
260    fn client_ca_defaults_to_none() {
261        let cfg = ServerTlsConfig::builder()
262            .cert_pem_bytes(vec![1])
263            .key_pem_bytes(vec![2])
264            .build()
265            .unwrap();
266        assert!(cfg.client_ca.is_none());
267    }
268
269    #[test]
270    fn require_client_ca_pem_bytes_enables_mtls() {
271        let cfg = ServerTlsConfig::builder()
272            .cert_pem_bytes(vec![1])
273            .key_pem_bytes(vec![2])
274            .require_client_ca_pem_bytes(b"--FAKE CA--".to_vec())
275            .build()
276            .unwrap();
277        assert!(matches!(cfg.client_ca, Some(PemSource::Bytes(_))));
278    }
279
280    #[test]
281    fn require_client_ca_pem_file_enables_mtls() {
282        let cfg = ServerTlsConfig::builder()
283            .cert_pem_bytes(vec![1])
284            .key_pem_bytes(vec![2])
285            .require_client_ca_pem_file("/etc/ca.crt")
286            .build()
287            .unwrap();
288        assert!(matches!(cfg.client_ca, Some(PemSource::Path(_))));
289    }
290
291    #[test]
292    fn alpn_defaults_to_empty() {
293        let cfg = ServerTlsConfig::builder()
294            .cert_pem_bytes(vec![1])
295            .key_pem_bytes(vec![2])
296            .build()
297            .unwrap();
298        assert!(cfg.alpn.is_empty());
299    }
300
301    #[test]
302    fn with_alpn_sets_protocols() {
303        let cfg = ServerTlsConfig::builder()
304            .cert_pem_bytes(vec![1])
305            .key_pem_bytes(vec![2])
306            .with_alpn(["h2", "http/1.1"])
307            .build()
308            .unwrap();
309        assert_eq!(cfg.alpn, vec![b"h2".to_vec(), b"http/1.1".to_vec()]);
310    }
311
312    fn rcgen_self_signed() -> (Vec<u8>, Vec<u8>) {
313        let b = rcgen::generate_simple_self_signed(vec!["example.com".into()]).unwrap();
314        (
315            b.cert.pem().into_bytes(),
316            b.signing_key.serialize_pem().into_bytes(),
317        )
318    }
319
320    #[test]
321    fn into_rustls_config_succeeds_with_real_cert_and_key() {
322        let (cert, key) = rcgen_self_signed();
323        let cfg = ServerTlsConfig::builder()
324            .cert_pem_bytes(cert)
325            .key_pem_bytes(key)
326            .build()
327            .unwrap();
328
329        let _rustls = cfg.into_rustls_config().unwrap();
330    }
331
332    #[test]
333    fn into_rustls_config_succeeds_with_mtls_client_ca() {
334        let (cert, key) = rcgen_self_signed();
335        let (ca, _) = rcgen_self_signed();
336        let cfg = ServerTlsConfig::builder()
337            .cert_pem_bytes(cert)
338            .key_pem_bytes(key)
339            .require_client_ca_pem_bytes(ca)
340            .build()
341            .unwrap();
342
343        let _rustls = cfg.into_rustls_config().unwrap();
344    }
345
346    #[test]
347    fn into_rustls_config_propagates_alpn_to_rustls() {
348        let (cert, key) = rcgen_self_signed();
349        let cfg = ServerTlsConfig::builder()
350            .cert_pem_bytes(cert)
351            .key_pem_bytes(key)
352            .with_alpn(["h2"])
353            .build()
354            .unwrap();
355
356        let rustls = cfg.into_rustls_config().unwrap();
357        assert_eq!(rustls.alpn_protocols, vec![b"h2".to_vec()]);
358    }
359
360    #[test]
361    fn into_rustls_config_rejects_cert_key_mismatch() {
362        let (cert, _) = rcgen_self_signed();
363        let (_, other_key) = rcgen_self_signed();
364        let cfg = ServerTlsConfig::builder()
365            .cert_pem_bytes(cert)
366            .key_pem_bytes(other_key)
367            .build()
368            .unwrap();
369
370        let err = cfg.into_rustls_config().unwrap_err();
371        assert!(
372            matches!(err, TlsError::Rustls(_)),
373            "cert/key mismatch must surface as TlsError::Rustls, got {err:?}"
374        );
375    }
376
377    #[test]
378    fn into_rustls_config_errors_on_malformed_cert_pem() {
379        let (_, key) = rcgen_self_signed();
380        let cfg = ServerTlsConfig::builder()
381            .cert_pem_bytes(b"not a pem".to_vec())
382            .key_pem_bytes(key)
383            .build()
384            .unwrap();
385
386        let err = cfg.into_rustls_config().unwrap_err();
387        assert!(
388            matches!(err, TlsError::NoCertificates),
389            "malformed cert PEM must surface as NoCertificates, got {err:?}"
390        );
391    }
392}