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

Add MTLS Support #657

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
25b3ff4
Add support for MTLS.
Kmoneal Jun 7, 2018
bff3e2d
Make TLS peer certificates available to Request
akuanti Jun 25, 2018
0f917fe
Add MutualTlsUser request guard
akuanti Jun 25, 2018
9a704fd
Add client certificate verification.
Kmoneal Jun 27, 2018
4026e37
Improve error handling.
Kmoneal Jul 2, 2018
2c51d66
Make MTLS certificate store path optional.
Kmoneal Jul 10, 2018
33daf7a
Fix error message that previously said ca_certs key was required.
Kmoneal Jul 10, 2018
5d46b2e
Consolidate TLS and MTLS and add retrievable certificate information.
Kmoneal Jul 10, 2018
24ab52e
Fix spacing and indentation.
Kmoneal Jul 10, 2018
03f2c0d
Make MutualTlsUser more robust.
Kmoneal Jul 17, 2018
0486d18
Add method to LocalRequest, allowing a certificate to be added.
Kmoneal Jul 17, 2018
513003a
Add mtls example using MutualTlsUser request guard.
Kmoneal Jul 17, 2018
8a4d519
Use common name from the client's certificate in MTLS example.
Kmoneal Jul 18, 2018
fd676c8
Clean up code and comments.
Kmoneal Jul 18, 2018
4dd1d3a
Change variable name in example to cert_path_store instead of none.
Kmoneal Jul 18, 2018
62842bd
Removed unnecessary ca_cert.pem from MTLS example.
Kmoneal Jul 18, 2018
bd8cdfb
Improve error handling for MutualTlsUser. (#3)
Kmoneal Jul 27, 2018
82fd1a9
Address some stylistic issues
akuanti Aug 31, 2018
c381395
Store not_before and not_after as time::Tm
akuanti Sep 5, 2018
7e3c30f
Move name validation logic into `tls`
akuanti Sep 7, 2018
aade841
Remove certificate parsing from MutualTlsUser
akuanti Sep 7, 2018
8ca69ce
Expose subject name for `MutualTlsUser`
akuanti Sep 10, 2018
931bc52
Merge branch 'master' into feature/mtls
akuanti Mar 11, 2019
7071a7c
Remove trailing whitespace
akuanti Mar 11, 2019
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 @@ -39,6 +39,7 @@ members = [
"examples/session",
"examples/raw_sqlite",
"examples/tls",
"examples/mtls",
"examples/fairings",
"examples/hello_2018",
]
5 changes: 5 additions & 0 deletions core/http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ state = "0.4"
cookie = { version = "0.11", features = ["percent-encode"] }
pear = "0.1"
unicode-xid = "0.1"
dns-lookup = "0.9.1"
untrusted = "0.6.1"
webpki = "0.18.0-alpha4"

[dependencies.hyper-sync-rustls]
version = "=0.3.0-rc.4"
features = ["server"]
optional = true
git = "https://github.com/Kmoneal/hyper-sync-rustls"
branch = "feature/mtls"

[dev-dependencies]
rocket = { version = "0.4.0", path = "../lib" }
91 changes: 90 additions & 1 deletion core/http/src/tls.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,94 @@
extern crate rustls;
extern crate hyper_sync_rustls;
extern crate dns_lookup;
extern crate untrusted;
extern crate webpki;

pub use self::hyper_sync_rustls::{util, WrappedStream, ServerSession, TlsServer};
pub use self::rustls::{Certificate, PrivateKey};
pub use self::rustls::{Certificate, PrivateKey, RootCertStore, internal::pemfile};
pub use self::dns_lookup::lookup_addr;

use self::untrusted::Input;
use self::webpki::{EndEntityCert, DNSNameRef};


/// Find the first `Certificate` valid for the given DNS name
fn first_valid_cert_for_name<'a>(dns_name: DNSNameRef, certs: &'a [Certificate]) -> Option<&'a Certificate> {
certs.iter()
.find(|cert| {
let cert_input = Input::from(cert.as_ref());
EndEntityCert::from(cert_input)
.and_then(|ee| ee.verify_is_valid_for_dns_name(dns_name).map(|_| true))
.unwrap_or(false)
})
}

/// Given a domain name and a set of `Certificate`s, return the first certificate
/// that matches the domain name
pub fn find_valid_cert_for_peer<'a>(name: &'a str, certs: &'a [Certificate]) -> Result<&'a Certificate, ()> {
let input = Input::from(name.as_bytes());
let domain_name = DNSNameRef::try_from_ascii(input)?;

// Find the first valid cert for the given name
let valid_cert = first_valid_cert_for_name(domain_name, &certs).ok_or(())?;

Ok(valid_cert)
}

/// MTLS client authentication.
///
/// The `MutualTlsUser` type is a request guard that only allows properly authenticated clients.
///
/// #Usage
///
/// A `MutualTlsUser` can be retrieved via its `FromRequest` implementation as a request guard.
///
/// ##Examples
///
/// The following short snippet shows `MutualTlsUser` being used as a request guard in a handler to
/// verify the client's certificate and print its subject name.
///
/// ```rust
/// # #![feature(plugin, decl_macro)]
/// # #![plugin(rocket_codegen)]
/// # extern crate rocket;
/// use rocket::http::tls::MutualTlsUser;
///
/// #[get("/message")]
/// fn message(mtls: MutualTlsUser) {
/// println!("{}", mtls.subject_name());
/// }
///
/// # fn main() { }
/// ```
///
#[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
}
}
15 changes: 8 additions & 7 deletions core/lib/src/config/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct ConfigBuilder {
/// The secret key.
pub secret_key: Option<String>,
/// TLS configuration (path to certificates file, path to private key file).
pub tls: Option<(String, String)>,
pub tls: Option<(String, String, Option<String>)>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new mtls configuration is still part of the tls config, which makes for some very awkward match constructs later in the code. On the other hand, keeping this path specified in the tls config section might look a bit nicer. I've suggested ways to clean up the matches assuming this field stays like this.

@SergioBenitez: did you want this split out into another field entirely, or just made optional?

/// Size limits.
pub limits: Limits,
/// Any extra parameters that aren't part of Rocket's config.
Expand Down Expand Up @@ -211,16 +211,17 @@ impl ConfigBuilder {
/// ```rust
/// use rocket::config::{Config, Environment};
///
/// let cert_store_path: Option<String> = None;
/// let mut config = Config::build(Environment::Staging)
/// .tls("/path/to/certs.pem", "/path/to/key.pem")
/// .tls("/path/to/certs.pem", "/path/to/key.pem", cert_store_path)
/// # ; /*
/// .unwrap();
/// # */
/// ```
pub fn tls<C, K>(mut self, certs_path: C, key_path: K) -> Self
where C: Into<String>, K: Into<String>
pub fn tls<C, K, W>(mut self, certs_path: C, key_path: K, cert_store_path: W) -> Self
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cert_store_path isn't optional in this builder, but it should be.

where C: Into<String>, K: Into<String>, W: Into<Option<String>>
{
self.tls = Some((certs_path.into(), key_path.into()));
self.tls = Some((certs_path.into(), key_path.into(), cert_store_path.into()));
self
}

Expand Down Expand Up @@ -326,8 +327,8 @@ impl ConfigBuilder {
config.set_root(root);
}

if let Some((certs_path, key_path)) = self.tls {
config.set_tls(&certs_path, &key_path)?;
if let Some((certs_path, key_path, cert_store_path)) = self.tls {
config.set_tls(&certs_path, &key_path, cert_store_path.as_ref().map(String::as_str))?;
}

if let Some(key) = self.secret_key {
Expand Down
32 changes: 24 additions & 8 deletions core/lib/src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,15 +510,14 @@ impl Config {
///
/// # use rocket::config::ConfigError;
/// # fn config_test() -> Result<(), ConfigError> {
/// let mut config = Config::development();
/// config.set_tls("/etc/ssl/my_certs.pem", "/etc/ssl/priv.key")?;
/// let mut config = Config::development()?;
/// config.set_tls("/etc/ssl/my_certs.pem", "/etc/ssl/priv.key", None)?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "tls")]
pub fn set_tls(&mut self, certs_path: &str, key_path: &str) -> Result<()> {
pub fn set_tls(&mut self, certs_path: &str, key_path: &str, cert_store_path: Option<&str>) -> Result<()> {
use http::tls::util::{self, Error};

let pem_err = "malformed PEM file";

// Load the certificates.
Expand All @@ -535,21 +534,38 @@ impl Config {
_ => self.bad_type("tls", pem_err, "a valid private key file")
})?;

self.tls = Some(TlsConfig { certs, key });
// Load certs for clients.
if let Some(path) = cert_store_path {
let ca_cert_vector = util::load_cert_store_certs(self.root_relative(path))
.map_err(|e| match e {
Error::Io(e) => ConfigError::Io(e, "tls.ca_certs"),
_ => self.bad_type("tls", pem_err, "a valid certificate store directory")
})?;

let ca_certs = Some(util::generate_cert_store(ca_cert_vector)
.map_err(|e| match e {
_ => self.bad_type("tls", pem_err, "a valid certificate store")
})?);

self.tls = Some(TlsConfig { certs, key, ca_certs });
} else {
self.tls = Some(TlsConfig { certs, key, ca_certs: None });
}

Ok(())
}

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

#[inline(always)]
fn set_raw_tls(&mut self, _paths: (&str, &str)) -> Result<()> {
fn set_raw_tls(&mut self, _paths: (&str, &str, Option<&str>)) -> Result<()> {
#[cfg(not(test))]
{ self.set_tls(_paths.0, _paths.1) }
{ self.set_tls(_paths.0, _paths.1, _paths.2) }

// During unit testing, we don't want to actually read certs/keys.
#[cfg(test)]
Expand Down
12 changes: 7 additions & 5 deletions core/lib/src/config/custom_values.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::fmt;

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

use config::{Result, Config, Value, ConfigError, LoggingLevel};
Expand Down Expand Up @@ -47,7 +47,8 @@ impl fmt::Display for SecretKey {
#[derive(Clone)]
pub struct TlsConfig {
pub certs: Vec<Certificate>,
pub key: PrivateKey
pub key: PrivateKey,
pub ca_certs: Option<RootCertStore>
}

#[cfg(not(feature = "tls"))]
Expand Down Expand Up @@ -238,8 +239,8 @@ pub fn log_level(conf: &Config,
pub fn tls_config<'v>(conf: &Config,
name: &str,
value: &'v Value,
) -> Result<(&'v str, &'v str)> {
let (mut certs_path, mut key_path) = (None, None);
) -> Result<(&'v str, &'v str, Option<&'v str>)> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here with separating tls configs and mtls configs.

let (mut certs_path, mut key_path, mut cert_store_path) = (None, None, None);
let table = value.as_table()
.ok_or_else(|| conf.bad_type(name, value.type_str(), "a table"))?;

Expand All @@ -248,12 +249,13 @@ pub fn tls_config<'v>(conf: &Config,
match key.as_str() {
"certs" => certs_path = Some(str(conf, "tls.certs", value)?),
"key" => key_path = Some(str(conf, "tls.key", value)?),
"ca_certs" => cert_store_path = Some(str(conf, "tls.ca_certs", value)?),
_ => return Err(ConfigError::UnknownKey(format!("{}.tls.{}", env, key)))
}
}

if let (Some(certs), Some(key)) = (certs_path, key_path) {
Ok((certs, key))
Ok((certs, key, cert_store_path))
} else {
Err(conf.bad_type(name, "a table with missing entries",
"a table with `certs` and `key` entries"))
Expand Down
37 changes: 36 additions & 1 deletion core/lib/src/data/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::fs::File;
use std::time::Duration;

#[cfg(feature = "tls")] use super::net_stream::HttpsStream;
#[cfg(feature = "tls")] use http::tls::Certificate;

use super::data_stream::{DataStream, kill_stream};
use super::net_stream::NetStream;
Expand Down Expand Up @@ -59,6 +60,8 @@ pub struct Data {
buffer: Vec<u8>,
is_complete: bool,
stream: BodyReader,
#[cfg(feature = "tls")]
peer_certs: Option<Vec<Certificate>>,
}

impl Data {
Expand Down Expand Up @@ -119,6 +122,10 @@ impl Data {
// Set the read timeout to 5 seconds.
let _ = net_stream.set_read_timeout(Some(Duration::from_secs(5)));

// Grab the certificate info
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this tls stuff should be grouped together to avoid using so many feature(tls) attributes.

One idea is to have a block:

#[cfg(feature = "tls")]
{
    let mut data = Data::new(http_stream);
    if let Some(certs) = net_stream.get_peer_certificates() {
        data.set_peer_certificates(certs);
    }

    Ok(data)
}

#[cfg(not(feature = "tls"))]
Ok(Data::new(http_stream))

#[cfg(feature = "tls")]
let cert_info = net_stream.get_peer_certificates();

// Steal the internal, undecoded data buffer from Hyper.
let (mut hyper_buf, pos, cap) = body.get_mut().take_buf();
hyper_buf.truncate(cap); // slow, but safe
Expand All @@ -134,6 +141,16 @@ impl Data {
ChunkedReader(_, n) => ChunkedReader(inner_data, n)
};

#[cfg(feature = "tls")]
{
let mut data = Data::new(http_stream);
if let Some(certs) = cert_info {
data.set_peer_certificates(certs);
}
Ok(data)
}

#[cfg(not(feature = "tls"))]
Ok(Data::new(http_stream))
}

Expand Down Expand Up @@ -255,7 +272,13 @@ impl Data {
};

trace_!("Peek bytes: {}/{} bytes.", peek_buf.len(), PEEK_BYTES);
Data { buffer: peek_buf, stream, is_complete: eof }
Data {
buffer: peek_buf,
stream: stream,
is_complete: eof,
#[cfg(feature = "tls")]
peer_certs: None,
}
}

/// This creates a `data` object from a local data source `data`.
Expand All @@ -267,8 +290,20 @@ impl Data {
buffer: data,
stream: HttpReader::SizedReader(empty_stream, 0),
is_complete: true,
#[cfg(feature = "tls")]
peer_certs: None,
}
}

#[cfg(feature = "tls")]
fn set_peer_certificates(&mut self, certs: Vec<Certificate>) {
self.peer_certs = Some(certs)
}

#[cfg(feature = "tls")]
pub(crate) fn get_peer_certificates(&self) -> Option<Vec<Certificate>> {
self.peer_certs.clone()
}
}

impl Drop for Data {
Expand Down
15 changes: 14 additions & 1 deletion core/lib/src/data/net_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::io;
use std::net::{SocketAddr, Shutdown};
use std::time::Duration;

#[cfg(feature = "tls")] use http::tls::{WrappedStream, ServerSession};
#[cfg(feature = "tls")] use http::tls::{Certificate, WrappedStream, ServerSession};
use http::hyper::net::{HttpStream, NetworkStream};

use self::NetStream::*;
Expand All @@ -19,6 +19,19 @@ pub enum NetStream {
Empty,
}

#[cfg(feature = "tls")]
impl NetStream {
// add code here
pub fn get_peer_certificates(&self) -> Option<Vec<Certificate>> {
match *self {
// stream is a WrappedStream
Https(ref stream) => stream.get_peer_certificates(),
_ => None
}

}
}

impl io::Read for NetStream {
#[inline(always)]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
Expand Down
Loading