rocket/mtls/
config.rs

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