Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add MTLS support #1419

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ members = [
"examples/session",
"examples/raw_sqlite",
"examples/tls",
"examples/mtls",
"examples/fairings",
"examples/hello_2018",
]
11 changes: 11 additions & 0 deletions core/http/src/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ use tokio::io::{AsyncRead, AsyncWrite};
use tokio::time::Delay;
use tokio::net::{TcpListener, TcpStream};

#[cfg(feature = "tls")]
use crate::tls::Certificate;

// TODO.async: 'Listener' and 'Connection' provide common enough functionality
// that they could be introduced in upstream libraries.
/// A 'Listener' yields incoming connections
Expand All @@ -30,6 +33,9 @@ pub trait Listener {
/// A 'Connection' represents an open connection to a client
pub trait Connection: AsyncRead + AsyncWrite {
fn remote_addr(&self) -> Option<SocketAddr>;

#[cfg(feature = "tls")]
fn peer_certificates(&self) -> Option<Vec<Certificate>>;
}

/// This is a genericized version of hyper's AddrIncoming that is intended to be
Expand Down Expand Up @@ -177,4 +183,9 @@ impl Connection for TcpStream {
fn remote_addr(&self) -> Option<SocketAddr> {
self.peer_addr().ok()
}

#[cfg(feature = "tls")]
fn peer_certificates(&self) -> Option<Vec<Certificate>> {
None
}
}
58 changes: 55 additions & 3 deletions core/http/src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use tokio_rustls::{TlsAcceptor, server::TlsStream};
use tokio_rustls::rustls;

pub use rustls::internal::pemfile;
pub use rustls::{Certificate, PrivateKey, ServerConfig};
pub use rustls::{Certificate, PrivateKey, ServerConfig, RootCertStore, Session};

use crate::listener::{Connection, Listener};

Expand Down Expand Up @@ -67,6 +67,19 @@ pub fn load_private_key<P: AsRef<Path>>(path: P) -> Result<rustls::PrivateKey, E
}
}

pub fn empty_ca_certs() -> rustls::RootCertStore {
rustls::RootCertStore::empty()
}

pub fn load_ca_certs<P: AsRef<Path>>(path: P) -> Result<rustls::RootCertStore, Error> {
let mut ca = rustls::RootCertStore::empty();
let certfile = fs::File::open(path.as_ref()).map_err(|e| Error::Io(e))?;
let mut reader = BufReader::new(certfile);
ca.add_pem_file(&mut reader).map_err(|_| Error::BadCerts);

Ok(ca)
}

pub struct TlsListener {
listener: TcpListener,
acceptor: TlsAcceptor,
Expand Down Expand Up @@ -116,11 +129,15 @@ impl Listener for TlsListener {
pub async fn bind_tls(
address: SocketAddr,
cert_chain: Vec<Certificate>,
key: PrivateKey
key: PrivateKey,
ca_root: RootCertStore,
required: bool,
) -> io::Result<TlsListener> {
let listener = TcpListener::bind(address).await?;

let client_auth = rustls::NoClientAuth::new();
let client_auth = if required
{ rustls::AllowAnyAuthenticatedClient::new(ca_root) } else
{ rustls::AllowAnyAnonymousOrAuthenticatedClient::new(ca_root) };
let mut tls_config = ServerConfig::new(client_auth);
let cache = rustls::ServerSessionMemoryCache::new(1024);
tls_config.set_persistence(cache);
Expand All @@ -137,4 +154,39 @@ impl Connection for TlsStream<TcpStream> {
fn remote_addr(&self) -> Option<SocketAddr> {
self.get_ref().0.remote_addr()
}

fn peer_certificates(&self) -> Option<Vec<Certificate>> {
(self.get_ref().1 as &dyn Session).get_peer_certificates()
}
}

#[derive(Debug)]
pub struct MutualTlsUser {
subject_name: String,
}

impl MutualTlsUser {
pub fn new(subject_name: &str) -> MutualTlsUser {
// NOTE: `subject_name` is not necessarily the subject name in the certificate,
// but it is the name for which the certificate was validated.
MutualTlsUser {
subject_name: subject_name.to_string()
}
}

/// Return the client's subject name.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::tls::MutualTlsUser;
///
/// fn handler(mtls: MutualTlsUser) {
/// let subject_name = mtls.subject_name();
/// }
/// ```
pub fn subject_name(&self) -> &str {
&self.subject_name
}
}
31 changes: 31 additions & 0 deletions core/lib/src/config/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub struct ConfigBuilder {
pub secret_key: Option<String>,
/// TLS configuration (path to certificates file, path to private key file).
pub tls: Option<(String, String)>,
/// MTLS configuration, path to root certificates file.
pub mtls: Option<(String, bool)>,
/// Size limits.
pub limits: Limits,
/// Any extra parameters that aren't part of Rocket's config.
Expand Down Expand Up @@ -61,6 +63,7 @@ impl ConfigBuilder {
log_level: config.log_level,
secret_key: None,
tls: None,
mtls: None,
limits: config.limits,
extras: config.extras,
root: None,
Expand Down Expand Up @@ -226,6 +229,30 @@ impl ConfigBuilder {
self
}

/// Sets the TLS configuration in the configuration being built.
///
/// Certificates are read from `certs_path`. The certificate chain must be
/// in X.509 PEM format. The private key is read from `key_path`. The
/// private key must be an RSA key in either PKCS#1 or PKCS#8 PEM format.
///
/// # Example
///
/// ```rust
/// use rocket::config::{Config, Environment};
///
/// let mut config = Config::build(Environment::Staging)
/// .tls("/path/to/certs.pem", "/path/to/key.pem")
/// # ; /*
/// .unwrap();
/// # */
/// ```
pub fn mtls<C>(mut self, ca: C, required: bool) -> Self
where C: Into<String>
{
self.mtls = Some((ca.into(), required));
self
}

/// Sets the `environment` in the configuration being built.
///
/// # Example
Expand Down Expand Up @@ -332,6 +359,10 @@ impl ConfigBuilder {
config.set_tls(&certs_path, &key_path)?;
}

if let Some((ca_path, required)) = self.mtls {
config.set_mtls(&ca_path, required)?;
}

if let Some(key) = self.secret_key {
config.set_secret_key(key)?;
}
Expand Down
59 changes: 59 additions & 0 deletions core/lib/src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub struct Config {
pub(crate) secret_key: SecretKey,
/// TLS configuration.
pub(crate) tls: Option<TlsConfig>,
/// Mutual TLS configuration.
pub(crate) mtls: Option<MutualTlsConfig>,
/// Streaming data limits.
pub limits: Limits,
/// Extra parameters that aren't part of Rocket's core config.
Expand Down Expand Up @@ -279,6 +281,7 @@ impl Config {
log_level: LoggingLevel::Normal,
secret_key: key,
tls: None,
mtls: None,
limits: Limits::default(),
extras: HashMap::new(),
config_file_path: None,
Expand All @@ -295,6 +298,7 @@ impl Config {
log_level: LoggingLevel::Normal,
secret_key: key,
tls: None,
mtls: None,
limits: Limits::default(),
extras: HashMap::new(),
config_file_path: None,
Expand All @@ -311,6 +315,7 @@ impl Config {
log_level: LoggingLevel::Critical,
secret_key: key,
tls: None,
mtls: None,
limits: Limits::default(),
extras: HashMap::new(),
config_file_path: None,
Expand Down Expand Up @@ -347,6 +352,7 @@ impl Config {
/// * **log**: String
/// * **secret_key**: String (256-bit base64 or base16)
/// * **tls**: Table (`certs` (path as String), `key` (path as String))
/// * **ca_path**: String
pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> Result<()> {
let (id, ok) = (|val| val, |_| Ok(()));
config_from_raw!(self, name, val,
Expand All @@ -357,6 +363,7 @@ impl Config {
log => (log_level, set_log_level, ok),
secret_key => (str, set_secret_key, id),
tls => (tls_config, set_raw_tls, id),
mtls => (mtls_config, set_raw_mtls, id),
limits => (limits, set_limits, ok),
| _ => {
self.extras.insert(name.into(), val.clone());
Expand Down Expand Up @@ -604,6 +611,58 @@ impl Config {
{ Ok(()) }
}

/// Sets the Mutual TLS configuration in `self`.
///
/// Certificates are read from `ca_path`. The certificate chain must be
/// in X.509 PEM format.
///
/// # Errors
///
/// If reading either the certificates fails, an error of variant `Io` is returned.
///
/// # Example
///
/// ```rust
/// # use rocket::config::ConfigError;
/// # fn config_test() -> Result<(), ConfigError> {
/// let mut config = rocket::Config::development();
/// config.set_mtls("/etc/ssl/certs/GlobalSign_Root_CA.pem", true)?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "tls")]
pub fn set_mtls(&mut self, ca_path: &str, required: bool) -> Result<()> {
use crate::http::tls::{load_ca_certs, Error};
let pem_err = "malformed PEM file";

let ca = load_ca_certs(self.root_relative(ca_path))
.map_err(|e| match e {
Error::Io(e) => ConfigError::Io(e, "mtls.ca"),
_ => self.bad_type("mtls", pem_err, "a valid ca root file")
})?;

self.mtls = Some(MutualTlsConfig { ca, required });
Ok(())
}

#[doc(hidden)]
#[cfg(not(feature = "tls"))]
pub fn set_mtls(&mut self, _: &str, _: bool) -> Result<()> {
self.mtls = Some(MutualTlsConfig);
Ok(())
}

#[inline(always)]
fn set_raw_mtls(&mut self, _paths: (&str, bool)) -> Result<()> {
#[cfg(not(test))]
{ self.set_mtls(_paths.0, _paths.1) }

// During unit testing, we don't want to actually read certs/keys.
#[cfg(test)]
{ Ok(()) }
}


/// Sets the extras for `self` to be the key/value pairs in `extras`.
/// encoded string.
///
Expand Down
45 changes: 44 additions & 1 deletion core/lib/src/config/custom_values.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fmt;

#[cfg(feature = "tls")] use crate::http::tls::{Certificate, PrivateKey};
#[cfg(feature = "tls")] use crate::http::tls::{Certificate, PrivateKey, RootCertStore};

use crate::http::private::cookie::Key;
use crate::config::{Result, Config, Value, ConfigError, LoggingLevel};
Expand Down Expand Up @@ -56,10 +56,28 @@ pub struct TlsConfig {
#[derive(Clone)]
pub struct TlsConfig;

#[cfg(feature = "tls")]
#[derive(Clone)]
pub struct MutualTlsConfig {
pub ca: RootCertStore,
pub required: bool,
}

#[cfg(not(feature = "tls"))]
#[derive(Clone)]
pub struct MutualTlsConfig;

pub fn str<'a>(conf: &Config, name: &str, v: &'a Value) -> Result<&'a str> {
v.as_str().ok_or_else(|| conf.bad_type(name, v.type_str(), "a string"))
}

pub fn bool(conf: &Config, name: &str, value: &Value) -> Result<bool> {
match value.as_bool() {
Some(x) => Ok(x as bool),
_ => Err(conf.bad_type(name, value.type_str(), "a boolean"))
}
}

pub fn u64(conf: &Config, name: &str, value: &Value) -> Result<u64> {
match value.as_integer() {
Some(x) if x >= 0 => Ok(x as u64),
Expand Down Expand Up @@ -114,6 +132,31 @@ pub fn tls_config<'v>(conf: &Config,
}
}

pub fn mtls_config<'v>(conf: &Config,
name: &str,
value: &'v Value,
) -> Result<(&'v str, bool)> {
let (mut ca_path, mut required) = (None, false);
let table = value.as_table()
.ok_or_else(|| conf.bad_type(name, value.type_str(), "a table"))?;

let env = conf.environment;
for (key, value) in table {
match key.as_str() {
"ca" => ca_path = Some(str(conf, "mtls.ca", value)?),
"required" => required = bool(conf, &format!("limits.{}", key), value)?,
_ => return Err(ConfigError::UnknownKey(format!("{}.mtls.{}", env, key)))
}
}

if let (Some(ca), ) = (ca_path,) {
Ok((ca, required))
} else {
Err(conf.bad_type(name, "a table with missing entries",
"a table with `ca` entries"))
}
}

pub fn limits(conf: &Config, name: &str, value: &Value) -> Result<Limits> {
let table = value.as_table()
.ok_or_else(|| conf.bad_type(name, value.type_str(), "a table"))?;
Expand Down
12 changes: 12 additions & 0 deletions core/lib/src/local/asynchronous/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ use crate::http::{Status, Method, uri::Origin, ext::IntoOwned};

use super::{Client, LocalResponse};

#[cfg(feature = "tls")]
use crate::http::tls::Certificate;

/// An `async` local request as returned by [`Client`](super::Client).
///
/// For details, see [the top-level documentation](../index.html#localrequest).
Expand Down Expand Up @@ -109,6 +112,15 @@ impl<'c> LocalRequest<'c> {
response
}

/// Add a certificate to this request.
#[cfg(feature = "tls")]
pub fn certificate(mut self, cert: Certificate) -> Self {
let peer_certs = vec![cert];
self._request_mut().set_peer_certificates(peer_certs);

self
}

pub_request_impl!("# use rocket::local::asynchronous::Client;
use rocket::local::asynchronous::LocalRequest;" async await);
}
Expand Down
Loading