-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add MTLS Support #657
Changes from all commits
25b3ff4
bff3e2d
0f917fe
9a704fd
4026e37
2c51d66
33daf7a
5d46b2e
24ab52e
03f2c0d
0486d18
513003a
8a4d519
fd676c8
4dd1d3a
62842bd
bd8cdfb
82fd1a9
c381395
7e3c30f
aade841
8ca69ce
931bc52
7071a7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>)>, | ||
/// Size limits. | ||
pub limits: Limits, | ||
/// Any extra parameters that aren't part of Rocket's config. | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
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 | ||
} | ||
|
||
|
@@ -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 { | ||
|
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}; | ||
|
@@ -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"))] | ||
|
@@ -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>)> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thing here with separating |
||
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"))?; | ||
|
||
|
@@ -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")) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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 { | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of this 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 | ||
|
@@ -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)) | ||
} | ||
|
||
|
@@ -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`. | ||
|
@@ -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 { | ||
|
There was a problem hiding this comment.
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 thetls
config, which makes for some very awkwardmatch
constructs later in the code. On the other hand, keeping this path specified in thetls
config section might look a bit nicer. I've suggested ways to clean up thematch
es assuming this field stays like this.@SergioBenitez: did you want this split out into another field entirely, or just made optional?