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}