rocket_community/mtls/
config.rs

1use std::io;
2
3use figment::value::magic::{Either, RelativePathBuf};
4use rustls_pki_types::{pem::PemObject, CertificateDer};
5use serde::{Deserialize, Serialize};
6
7use crate::tls::{Error, Result};
8
9/// Mutual TLS configuration.
10///
11/// Configuration works in concert with the [`mtls`](crate::mtls) module, which
12/// provides a request guard to validate, verify, and retrieve client
13/// certificates in routes.
14///
15/// By default, mutual TLS is disabled and client certificates are not required,
16/// validated or verified. To enable mutual TLS, the `mtls` feature must be
17/// enabled and support configured via two `tls.mutual` parameters:
18///
19///   * `ca_certs`
20///
21///     A required path to a PEM file or raw bytes to a DER-encoded X.509 TLS
22///     certificate chain for the certificate authority to verify client
23///     certificates against. When a path is configured in a file, such as
24///     `Rocket.toml`, relative paths are interpreted as relative to the source
25///     file's directory.
26///
27///   * `mandatory`
28///
29///     An optional boolean that control whether client authentication is
30///     required.
31///
32///     When `true`, client authentication is required. TLS connections where
33///     the client does not present a certificate are immediately terminated.
34///     When `false`, the client is not required to present a certificate. In
35///     either case, if a certificate _is_ presented, it must be valid or the
36///     connection is terminated.
37///
38/// In a `Rocket.toml`, configuration might look like:
39///
40/// ```toml
41/// [default.tls.mutual]
42/// ca_certs = "/ssl/ca_cert.pem"
43/// mandatory = true                # when absent, defaults to false
44/// ```
45///
46/// Programmatically, configuration might look like:
47///
48/// ```rust
49/// # #[macro_use] extern crate rocket_community as rocket;
50/// use rocket::mtls::MtlsConfig;
51/// use rocket::figment::providers::Serialized;
52///
53/// #[launch]
54/// fn rocket() -> _ {
55///     let mtls = MtlsConfig::from_path("/ssl/ca_cert.pem");
56///     rocket::custom(rocket::Config::figment().merge(("tls.mutual", mtls)))
57/// }
58/// ```
59///
60/// Once mTLS is configured, the [`mtls::Certificate`](crate::mtls::Certificate)
61/// request guard can be used to retrieve client certificates in routes.
62#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
63pub struct MtlsConfig {
64    /// Path to a PEM file with, or raw bytes for, DER-encoded Certificate
65    /// Authority certificates which will be used to verify client-presented
66    /// certificates.
67    // TODO: Support more than one CA root.
68    pub(crate) ca_certs: Either<RelativePathBuf, Vec<u8>>,
69    /// Whether the client is required to present a certificate.
70    ///
71    /// When `true`, the client is required to present a valid certificate to
72    /// proceed with TLS. When `false`, the client is not required to present a
73    /// certificate. In either case, if a certificate _is_ presented, it must be
74    /// valid or the connection is terminated.
75    #[serde(default)]
76    #[serde(deserialize_with = "figment::util::bool_from_str_or_int")]
77    pub mandatory: bool,
78}
79
80impl MtlsConfig {
81    /// Constructs a `MtlsConfig` from a path to a PEM file with a certificate
82    /// authority `ca_certs` DER-encoded X.509 TLS certificate chain. This
83    /// method does no validation; it simply creates an [`MtlsConfig`] for later
84    /// use.
85    ///
86    /// These certificates will be used to verify client-presented certificates
87    /// in TLS connections.
88    ///
89    /// # Example
90    ///
91    /// ```rust
92    /// # extern crate rocket_community as rocket;
93    /// use rocket::mtls::MtlsConfig;
94    ///
95    /// let tls_config = MtlsConfig::from_path("/ssl/ca_certs.pem");
96    /// ```
97    pub fn from_path<C: AsRef<std::path::Path>>(ca_certs: C) -> Self {
98        MtlsConfig {
99            ca_certs: Either::Left(ca_certs.as_ref().to_path_buf().into()),
100            mandatory: Default::default(),
101        }
102    }
103
104    /// Constructs a `MtlsConfig` from a byte buffer to a certificate authority
105    /// `ca_certs` DER-encoded X.509 TLS certificate chain. This method does no
106    /// validation; it simply creates an [`MtlsConfig`] for later use.
107    ///
108    /// These certificates will be used to verify client-presented certificates
109    /// in TLS connections.
110    ///
111    /// # Example
112    ///
113    /// ```rust
114    /// # extern crate rocket_community as rocket;
115    /// use rocket::mtls::MtlsConfig;
116    ///
117    /// # let ca_certs_buf = &[];
118    /// let mtls_config = MtlsConfig::from_bytes(ca_certs_buf);
119    /// ```
120    pub fn from_bytes(ca_certs: &[u8]) -> Self {
121        MtlsConfig {
122            ca_certs: Either::Right(ca_certs.to_vec()),
123            mandatory: Default::default(),
124        }
125    }
126
127    /// Sets whether client authentication is required. Disabled by default.
128    ///
129    /// When `true`, client authentication will be required. TLS connections
130    /// where the client does not present a certificate will be immediately
131    /// terminated. When `false`, the client is not required to present a
132    /// certificate. In either case, if a certificate _is_ presented, it must be
133    /// valid or the connection is terminated.
134    ///
135    /// # Example
136    ///
137    /// ```rust
138    /// # extern crate rocket_community as rocket;
139    /// use rocket::mtls::MtlsConfig;
140    ///
141    /// # let ca_certs_buf = &[];
142    /// let mtls_config = MtlsConfig::from_bytes(ca_certs_buf).mandatory(true);
143    /// ```
144    pub fn mandatory(mut self, mandatory: bool) -> Self {
145        self.mandatory = mandatory;
146        self
147    }
148
149    /// Returns the value of the `ca_certs` parameter.
150    ///
151    /// # Example
152    ///
153    /// ```rust
154    /// # extern crate rocket_community as rocket;
155    /// use rocket::mtls::MtlsConfig;
156    ///
157    /// # let ca_certs_buf = &[];
158    /// let mtls_config = MtlsConfig::from_bytes(ca_certs_buf).mandatory(true);
159    /// assert_eq!(mtls_config.ca_certs().unwrap_right(), ca_certs_buf);
160    /// ```
161    pub fn ca_certs(&self) -> either::Either<std::path::PathBuf, &[u8]> {
162        match &self.ca_certs {
163            Either::Left(path) => either::Either::Left(path.relative()),
164            Either::Right(bytes) => either::Either::Right(bytes),
165        }
166    }
167
168    #[inline(always)]
169    pub fn ca_certs_reader(&self) -> io::Result<Box<dyn io::BufRead + Sync + Send>> {
170        crate::tls::config::to_reader(&self.ca_certs)
171    }
172
173    /// Load and decode CA certificates from `reader`.
174    pub(crate) fn load_ca_certs(&self) -> Result<rustls::RootCertStore> {
175        let mut roots = rustls::RootCertStore::empty();
176        for cert in CertificateDer::pem_reader_iter(&mut self.ca_certs_reader()?) {
177            roots
178                .add(cert.map_err(std::io::Error::other)?)
179                .map_err(Error::CertAuth)?;
180        }
181
182        Ok(roots)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use figment::{
189        providers::{Format, Toml},
190        Figment,
191    };
192    use std::path::Path;
193
194    use crate::mtls::MtlsConfig;
195
196    #[test]
197    fn test_mtls_config() {
198        figment::Jail::expect_with(|jail| {
199            jail.create_file(
200                "MTLS.toml",
201                r#"
202                certs = "/ssl/cert.pem"
203                key = "/ssl/key.pem"
204            "#,
205            )?;
206
207            let figment = || Figment::from(Toml::file("MTLS.toml"));
208            figment().extract::<MtlsConfig>().expect_err("no ca");
209
210            jail.create_file(
211                "MTLS.toml",
212                r#"
213                ca_certs = "/ssl/ca.pem"
214            "#,
215            )?;
216
217            let mtls: MtlsConfig = figment().extract()?;
218            assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem"));
219            assert!(!mtls.mandatory);
220
221            jail.create_file(
222                "MTLS.toml",
223                r#"
224                ca_certs = "/ssl/ca.pem"
225                mandatory = true
226            "#,
227            )?;
228
229            let mtls: MtlsConfig = figment().extract()?;
230            assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem"));
231            assert!(mtls.mandatory);
232
233            jail.create_file(
234                "MTLS.toml",
235                r#"
236                ca_certs = "relative/ca.pem"
237            "#,
238            )?;
239
240            let mtls: MtlsConfig = figment().extract()?;
241            assert_eq!(
242                mtls.ca_certs().unwrap_left(),
243                jail.directory().join("relative/ca.pem")
244            );
245
246            Ok(())
247        });
248    }
249}