Complete mTLS implementation.

Resolves #254.
This commit is contained in:
Sergio Benitez 2021-07-08 23:59:47 -07:00
parent bbc36ba27f
commit 7ffe3a7360
16 changed files with 704 additions and 142 deletions

View File

@ -4,20 +4,8 @@
//! These types will, with certainty, be removed with time, but they reside here
//! while necessary.
#[doc(hidden)] pub use hyper::{Body, Error, Request, Response, Version};
#[doc(hidden)] pub use hyper::body::{Bytes, HttpBody, Sender as BodySender};
#[doc(hidden)] pub use hyper::rt::Executor;
#[doc(hidden)] pub use hyper::server::Server;
#[doc(hidden)] pub use hyper::service::{make_service_fn, service_fn, Service};
#[doc(hidden)] pub use http::header::HeaderMap;
#[doc(hidden)] pub use http::header::HeaderName as HeaderName;
#[doc(hidden)] pub use http::header::HeaderValue as HeaderValue;
#[doc(hidden)] pub use http::method::Method;
#[doc(hidden)] pub use http::request::Parts as RequestParts;
#[doc(hidden)] pub use http::response::Builder as ResponseBuilder;
#[doc(hidden)] pub use http::status::StatusCode;
#[doc(hidden)] pub use http::uri::{Uri, Parts as UriParts};
#[doc(hidden)] pub use hyper::*;
#[doc(hidden)] pub use http::*;
/// Reexported http header types.
pub mod header {

View File

@ -89,6 +89,98 @@ pub enum Error {
Trailing(usize),
}
/// A request guard for validated, verified client certificates.
///
/// This type is a wrapper over [`x509::TbsCertificate`] with convenient
/// methods and complete documentation. Should the data exposed by the inherent
/// methods not suffice, this type derefs to [`x509::TbsCertificate`].
///
/// # Request Guard
///
/// The request guard implementation succeeds if:
///
/// * The client presents certificates.
/// * The certificates are active and not yet expired.
/// * The client's certificate chain was signed by the CA identified by the
/// configured `ca_certs` and with respect to SNI, if any. See [module level
/// docs](self) for configuration details.
///
/// If the client does not present certificates, the guard _forwards_.
///
/// If the certificate chain fails to validate or verify, the guard _fails_ with
/// the respective [`Error`].
///
/// # Wrapping
///
/// To implement roles, the `Certificate` guard can be wrapped with a more
/// semantically meaningful type with extra validation. For example, if a
/// certificate with a specific serial number is known to belong to an
/// administrator, a `CertifiedAdmin` type can authorize as follow:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::mtls::{self, bigint::BigUint, Certificate};
/// use rocket::request::{Request, FromRequest, Outcome};
/// use rocket::outcome::try_outcome;
///
/// // The serial number for the certificate issued to the admin.
/// const ADMIN_SERIAL: &str = "65828378108300243895479600452308786010218223563";
///
/// // A request guard that authenticates and authorizes an administrator.
/// struct CertifiedAdmin<'r>(Certificate<'r>);
///
/// #[rocket::async_trait]
/// impl<'r> FromRequest<'r> for CertifiedAdmin<'r> {
/// type Error = mtls::Error;
///
/// async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
/// let cert = try_outcome!(req.guard::<Certificate<'r>>().await);
/// if let Some(true) = cert.has_serial(ADMIN_SERIAL) {
/// Outcome::Success(CertifiedAdmin(cert))
/// } else {
/// Outcome::Forward(())
/// }
/// }
/// }
///
/// #[get("/admin")]
/// fn admin(admin: CertifiedAdmin<'_>) {
/// // This handler can only execute if an admin is authenticated.
/// }
///
/// #[get("/admin", rank = 2)]
/// fn unauthorized(user: Option<Certificate<'_>>) {
/// // This handler always executes, whether there's a non-admin user that's
/// // authenticated (user = Some()) or not (user = None).
/// }
/// ```
///
/// # Example
///
/// To retrieve certificate data in a route, use `Certificate` as a guard:
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::get;
/// use rocket::mtls::{self, Certificate};
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// // This handler only runs when a valid certificate was presented.
/// }
///
/// #[get("/maybe")]
/// fn maybe_auth(cert: Option<Certificate<'_>>) {
/// // This handler runs even if no certificate was presented or an invalid
/// // certificate was presented.
/// }
///
/// #[get("/ok")]
/// fn ok_auth(cert: mtls::Result<Certificate<'_>>) {
/// // This handler does not run if a certificate was not presented but
/// // _does_ run if a valid (Ok) or invalid (Err) one was presented.
/// }
/// ```
#[repr(transparent)]
#[derive(Debug, PartialEq)]
pub struct Certificate<'a>(X509Certificate<'a>);
@ -138,26 +230,136 @@ impl<'a> Certificate<'a> {
}
}
/// Returns the serial number of the X.509 certificate.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::get;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// let cert = cert.serial();
/// }
/// ```
pub fn serial(&self) -> &bigint::BigUint {
&self.inner().serial
}
/// Returns the version of the X.509 certificate.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::get;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// let cert = cert.version();
/// }
/// ```
pub fn version(&self) -> u32 {
self.inner().version.0
}
/// Returns the subject (a "DN" or "Distinguised Name") of the X.509
/// certificate.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::get;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// if let Some(name) = cert.subject().common_name() {
/// println!("Hello, {}!", name);
/// }
/// }
/// ```
pub fn subject(&self) -> &Name<'a> {
Name::ref_cast(&self.inner().subject)
}
/// Returns the issuer (a "DN" or "Distinguised Name") of the X.509
/// certificate.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::get;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// if let Some(name) = cert.issuer().common_name() {
/// println!("Issued by: {}", name);
/// }
/// }
/// ```
pub fn issuer(&self) -> &Name<'a> {
Name::ref_cast(&self.inner().issuer)
}
/// Returns a map of the extensions in the X.509 certificate.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::get;
/// use rocket::mtls::{oid, x509, Certificate};
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// let subject_alt = cert.extensions()
/// .get(&oid::OID_X509_EXT_SUBJECT_ALT_NAME)
/// .and_then(|e| match e.parsed_extension() {
/// x509::ParsedExtension::SubjectAlternativeName(s) => Some(s),
/// _ => None
/// });
///
/// if let Some(subject_alt) = subject_alt {
/// for name in &subject_alt.general_names {
/// if let x509::GeneralName::RFC822Name(name) = name {
/// println!("An email, perhaps? {}", name);
/// }
/// }
/// }
/// }
/// ```
pub fn extensions(&self) -> &HashMap<oid::Oid<'a>, x509::X509Extension<'a>> {
&self.inner().extensions
}
/// Checks if the certificate has the serial number `number`.
///
/// If `number` is not a valid unsigned integer in base 10, returns `None`.
///
/// Otherwise, returns `Some(true)` if it does and `Some(false)` if it does
/// not.
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::get;
/// use rocket::mtls::Certificate;
///
/// const SERIAL: &str = "65828378108300243895479600452308786010218223563";
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// if cert.has_serial(SERIAL).unwrap_or(false) {
/// println!("certificate has the expected serial number");
/// }
/// }
/// ```
pub fn has_serial(&self, number: &str) -> Option<bool> {
let uint: bigint::BigUint = number.parse().ok()?;
Some(&uint == self.serial())
@ -173,22 +375,116 @@ impl<'a> Deref for Certificate<'a> {
}
impl<'a> Name<'a> {
/// Returns the _first_ UTF-8 _string_ common name, if any.
///
/// Note that common names need not be UTF-8 strings, or strings at all.
/// This method returns the first common name attribute that is.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// if let Some(name) = cert.subject().common_name() {
/// println!("Hello, {}!", name);
/// }
/// }
/// ```
pub fn common_name(&self) -> Option<&'a str> {
self.common_names().next()
}
/// Returns an iterator over all of the UTF-8 _string_ common names in
/// `self`.
///
/// Note that common names need not be UTF-8 strings, or strings at all.
/// This method filters the common names in `self` to those that are. Use
/// the raw [`iter_common_name()`](#method.iter_common_name) to iterate over
/// all value types.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// for name in cert.issuer().common_names() {
/// println!("Issued by {}.", name);
/// }
/// }
/// ```
pub fn common_names(&self) -> impl Iterator<Item = &'a str> + '_ {
self.iter_by_oid(&oid::OID_X509_COMMON_NAME).filter_map(|n| n.as_str().ok())
}
/// Returns the _first_ UTF-8 _string_ email address, if any.
///
/// Note that email addresses need not be UTF-8 strings, or strings at all.
/// This method returns the first email address attribute that is.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// if let Some(email) = cert.subject().email() {
/// println!("Hello, {}!", email);
/// }
/// }
/// ```
pub fn email(&self) -> Option<&'a str> {
self.emails().next()
}
/// Returns an iterator over all of the UTF-8 _string_ email addresses in
/// `self`.
///
/// Note that email addresses need not be UTF-8 strings, or strings at all.
/// This method filters the email addresss in `self` to those that are. Use
/// the raw [`iter_email()`](#method.iter_email) to iterate over all value
/// types.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// for email in cert.subject().emails() {
/// println!("Reach me at: {}", email);
/// }
/// }
/// ```
pub fn emails(&self) -> impl Iterator<Item = &'a str> + '_ {
self.iter_by_oid(&oid::OID_PKCS9_EMAIL_ADDRESS).filter_map(|n| n.as_str().ok())
}
/// Returns `true` if `self` has no data.
///
/// When this is the case for a `subject()`, the subject data can be found
/// in the `subjectAlt` [`extension()`](Certificate::extensions()).
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::mtls::Certificate;
///
/// #[get("/auth")]
/// fn auth(cert: Certificate<'_>) {
/// let no_data = cert.subject().is_empty();
/// }
/// ```
pub fn is_empty(&self) -> bool {
self.0.as_raw().is_empty()
}

View File

@ -21,6 +21,7 @@ all-features = true
[features]
default = []
tls = ["rocket_http/tls"]
mtls = ["rocket_http/mtls", "tls"]
secrets = ["rocket_http/private-cookies"]
json = ["serde_json", "tokio/io-util"]
msgpack = ["rmp-serde", "tokio/io-util"]

View File

@ -70,13 +70,17 @@ pub struct Config {
pub port: u16,
/// Number of threads to use for executing futures. **(default: `num_cores`)**
pub workers: usize,
/// Keep-alive timeout in seconds; disabled when `0`. **(default: `5`)**
pub keep_alive: u32,
/// Streaming read size limits. **(default: [`Limits::default()`])**
pub limits: Limits,
/// How, if at all, to identify the server via the `Server` header.
/// **(default: `"Rocket"`)**
pub ident: Ident,
/// Streaming read size limits. **(default: [`Limits::default()`])**
pub limits: Limits,
/// Directory to store temporary files in. **(default:
/// [`std::env::temp_dir()`])**
#[serde(serialize_with = "RelativePathBuf::serialize_relative")]
pub temp_dir: RelativePathBuf,
/// Keep-alive timeout in seconds; disabled when `0`. **(default: `5`)**
pub keep_alive: u32,
/// The TLS configuration, if any. **(default: `None`)**
#[cfg(feature = "tls")]
#[cfg_attr(nightly, doc(cfg(feature = "tls")))]
@ -89,14 +93,10 @@ pub struct Config {
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
#[serde(serialize_with = "SecretKey::serialize_zero")]
pub secret_key: SecretKey,
/// Directory to store temporary files in. **(default:
/// [`std::env::temp_dir()`])**
#[serde(serialize_with = "RelativePathBuf::serialize_relative")]
pub temp_dir: RelativePathBuf,
/// Max level to log. **(default: _debug_ `normal` / _release_ `critical`)**
pub log_level: LogLevel,
/// Graceful shutdown configuration. **(default: [`Shutdown::default()`])**
pub shutdown: Shutdown,
/// Max level to log. **(default: _debug_ `normal` / _release_ `critical`)**
pub log_level: LogLevel,
/// Whether to use colors and emoji when logging. **(default: `true`)**
#[serde(deserialize_with = "figment::util::bool_from_str_or_int")]
pub cli_colors: bool,
@ -167,16 +167,16 @@ impl Config {
address: Ipv4Addr::new(127, 0, 0, 1).into(),
port: 8000,
workers: num_cpus::get(),
keep_alive: 5,
limits: Limits::default(),
ident: Ident::default(),
limits: Limits::default(),
temp_dir: std::env::temp_dir().into(),
keep_alive: 5,
#[cfg(feature = "tls")]
tls: None,
#[cfg(feature = "secrets")]
secret_key: SecretKey::zero(),
temp_dir: std::env::temp_dir().into(),
log_level: LogLevel::Normal,
shutdown: Shutdown::default(),
log_level: LogLevel::Normal,
cli_colors: true,
__non_exhaustive: (),
}
@ -316,31 +316,62 @@ impl Config {
#[cfg(not(feature = "tls"))] { false }
}
/// Returns `true` if mTLS is enabled.
///
/// mTLS is enabled when TLS is enabled ([`Config::tls_enabled()`]) _and_
/// the `mtls` feature is enabled _and_ mTLS has been configured with a CA
/// certificate chain.
///
/// # Example
///
/// ```rust
/// let config = rocket::Config::default();
/// if config.mtls_enabled() {
/// println!("mTLS is enabled!");
/// } else {
/// println!("mTLS is disabled.");
/// }
/// ```
pub fn mtls_enabled(&self) -> bool {
if !self.tls_enabled() {
return false;
}
#[cfg(feature = "mtls")] {
self.tls.as_ref().map_or(false, |tls| tls.mutual.is_some())
}
#[cfg(not(feature = "mtls"))] { false }
}
pub(crate) fn pretty_print(&self, figment: &Figment) {
use crate::log::PaintExt;
launch_info!("{}Configured for {}.", Paint::emoji("🔧 "), self.profile);
launch_info_!("address: {}", Paint::default(&self.address).bold());
launch_info_!("port: {}", Paint::default(&self.port).bold());
launch_info_!("workers: {}", Paint::default(self.workers).bold());
launch_info_!("ident: {}", Paint::default(&self.ident).bold());
let ka = self.keep_alive;
if ka > 0 {
launch_info_!("keep-alive: {}", Paint::default(format!("{}s", ka)).bold());
} else {
launch_info_!("keep-alive: {}", Paint::default("disabled").bold());
fn bold<T: std::fmt::Display>(val: T) -> Paint<T> {
Paint::default(val).bold()
}
launch_info_!("limits: {}", Paint::default(&self.limits).bold());
match self.tls_enabled() {
true => launch_info_!("tls: {}", Paint::default("enabled").bold()),
false => launch_info_!("tls: {}", Paint::default("disabled").bold()),
launch_info!("{}Configured for {}.", Paint::emoji("🔧 "), self.profile);
launch_info_!("address: {}", bold(&self.address));
launch_info_!("port: {}", bold(&self.port));
launch_info_!("workers: {}", bold(self.workers));
launch_info_!("ident: {}", bold(&self.ident));
launch_info_!("limits: {}", bold(&self.limits));
launch_info_!("temp dir: {}", bold(&self.temp_dir.relative().display()));
match self.keep_alive {
0 => launch_info_!("keep-alive: {}", bold("disabled")),
ka => launch_info_!("keep-alive: {}{}", bold(ka), bold("s")),
}
match (self.tls_enabled(), self.mtls_enabled()) {
(true, true) => launch_info_!("tls: {}", bold("enabled w/mtls")),
(true, false) => launch_info_!("tls: {} w/o mtls", bold("enabled")),
(false, _) => launch_info_!("tls: {}", bold("disabled")),
}
#[cfg(feature = "secrets")] {
launch_info_!("secret key: {:?}", Paint::default(&self.secret_key).bold());
launch_info_!("secret key: {}", bold(&self.secret_key));
if !self.secret_key.is_provided() {
warn!("secrets enabled without a stable `secret_key`");
launch_info_!("disable `secrets` feature or configure a `secret_key`");
@ -348,10 +379,9 @@ impl Config {
}
}
launch_info_!("temp dir: {}", Paint::default(&self.temp_dir.relative().display()).bold());
launch_info_!("log level: {}", Paint::default(self.log_level).bold());
launch_info_!("cli colors: {}", Paint::default(&self.cli_colors).bold());
launch_info_!("shutdown: {}", Paint::default(&self.shutdown).bold());
launch_info_!("shutdown: {}", bold(&self.shutdown));
launch_info_!("log level: {}", bold(self.log_level));
launch_info_!("cli colors: {}", bold(&self.cli_colors));
// Check for now depreacted config values.
for (key, replacement) in Self::DEPRECATED_KEYS {
@ -399,7 +429,6 @@ impl Config {
/// The default profile: "debug" on `debug`, "release" on `release`.
#[cfg(not(debug_assertions))]
pub const DEFAULT_PROFILE: Profile = Self::RELEASE_PROFILE;
}
/// Associated constants for stringy versions of configuration parameters.

View File

@ -131,6 +131,9 @@ pub use ident::Ident;
#[cfg(feature = "tls")]
pub use tls::{TlsConfig, CipherSuite};
#[cfg(feature = "mtls")]
pub use tls::MutualTls;
#[cfg(feature = "secrets")]
pub use secret_key::SecretKey;
@ -423,6 +426,70 @@ mod tests {
});
}
#[test]
#[cfg(feature = "mtls")]
fn test_mtls_config() {
use std::path::Path;
figment::Jail::expect_with(|jail| {
jail.create_file("Rocket.toml", r#"
[default.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
"#)?;
let config = Config::from(Config::figment());
assert!(config.tls.is_some());
assert!(config.tls.as_ref().unwrap().mutual.is_none());
assert!(config.tls_enabled());
assert!(!config.mtls_enabled());
jail.create_file("Rocket.toml", r#"
[default.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
mutual = { ca_certs = "/ssl/ca.pem" }
"#)?;
let config = Config::from(Config::figment());
assert!(config.tls_enabled());
assert!(config.mtls_enabled());
let mtls = config.tls.as_ref().unwrap().mutual.as_ref().unwrap();
assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem"));
assert!(!mtls.mandatory);
jail.create_file("Rocket.toml", r#"
[default.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
[default.tls.mutual]
ca_certs = "/ssl/ca.pem"
mandatory = true
"#)?;
let config = Config::from(Config::figment());
let mtls = config.tls.as_ref().unwrap().mutual.as_ref().unwrap();
assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem"));
assert!(mtls.mandatory);
jail.create_file("Rocket.toml", r#"
[default.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
mutual = { ca_certs = "relative/ca.pem" }
"#)?;
let config = Config::from(Config::figment());
let mtls = config.tls.as_ref().unwrap().mutual().unwrap();
assert_eq!(mtls.ca_certs().unwrap_left(),
jail.directory().join("relative/ca.pem"));
Ok(())
});
}
#[test]
fn test_profiles_merge() {
figment::Jail::expect_with(|jail| {

View File

@ -246,7 +246,7 @@ impl<'de> Deserialize<'de> for SecretKey {
}
}
impl fmt::Debug for SecretKey {
impl fmt::Display for SecretKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_zero() {
f.write_str("[zero]")
@ -258,3 +258,9 @@ impl fmt::Debug for SecretKey {
}
}
}
impl fmt::Debug for SecretKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<Self as fmt::Display>::fmt(self, f)
}
}

View File

@ -26,44 +26,63 @@ use indexmap::IndexSet;
/// ciphersuite preferences over the client's. The default and recommended
/// value is `false`.
///
/// The following example illustrates manual configuration:
/// Additionally, the `mutual` parameter controls if and how the server
/// authenticates clients via mutual TLS. It works in concert with the
/// [`mtls`](crate::mtls) module. See [`MutualTls`] for configuration details.
///
/// In `Rocket.toml`, configuration might look like:
///
/// ```toml
/// [default.tls]
/// certs = "private/rsa_sha256_cert.pem"
/// key = "private/rsa_sha256_key.pem"
/// ```
///
/// With a custom programmatic configuration, this might look like:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::config::{Config, TlsConfig, CipherSuite};
///
/// // From a manually constructed figment.
/// let figment = rocket::Config::figment()
/// .merge(("tls.certs", "path/to/certs.pem"))
/// .merge(("tls.key", vec![0; 32]));
///
/// let config = rocket::Config::from(figment);
/// let tls_config = config.tls.as_ref().unwrap();
/// assert!(tls_config.certs().is_left());
/// assert!(tls_config.key().is_right());
/// assert_eq!(tls_config.ciphers().count(), 9);
/// assert!(!tls_config.prefer_server_cipher_order());
///
/// // From a serialized `TlsConfig`.
/// #[launch]
/// fn rocket() -> _ {
/// let tls_config = TlsConfig::from_paths("/ssl/certs.pem", "/ssl/key.pem")
/// .with_ciphers(CipherSuite::TLS_V13_SET)
/// .with_preferred_server_cipher_order(true);
///
/// let figment = rocket::Config::figment()
/// .merge(("tls", tls_config));
/// let config = Config {
/// tls: Some(tls_config),
/// ..Default::default()
/// };
///
/// let config = rocket::Config::from(figment);
/// let tls_config = config.tls.as_ref().unwrap();
/// assert_eq!(tls_config.ciphers().count(), 3);
/// assert!(tls_config.prefer_server_cipher_order());
/// rocket::custom(config)
/// }
/// ```
///
/// Or by creating a custom figment:
///
/// ```rust
/// use rocket::config::Config;
///
/// let figment = Config::figment()
/// .merge(("tls.certs", "path/to/certs.pem"))
/// .merge(("tls.key", vec![0; 32]));
/// #
/// # let config = rocket::Config::from(figment);
/// # let tls_config = config.tls.as_ref().unwrap();
/// # assert!(tls_config.certs().is_left());
/// # assert!(tls_config.key().is_right());
/// # assert_eq!(tls_config.ciphers().count(), 9);
/// # assert!(!tls_config.prefer_server_cipher_order());
/// ```
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(nightly, doc(cfg(feature = "tls")))]
#[serde(deny_unknown_fields)]
pub struct TlsConfig {
/// Path or raw bytes for the DER-encoded X.509 TLS certificate chain.
/// Path to a PEM file with, or raw bytes for, a DER-encoded X.509 TLS
/// certificate chain.
pub(crate) certs: Either<RelativePathBuf, Vec<u8>>,
/// Path or raw bytes to DER-encoded ASN.1 key in either PKCS#8 or PKCS#1
/// format.
/// Path to a PEM file with, or raw bytes for, DER-encoded private key in
/// either PKCS#8 or PKCS#1 format.
pub(crate) key: Either<RelativePathBuf, Vec<u8>>,
/// List of TLS cipher suites in server-preferred order.
#[serde(default = "CipherSuite::default_set")]
@ -78,11 +97,77 @@ pub struct TlsConfig {
pub(crate) mutual: Option<MutualTls>,
}
/// Mutual TLS configuration.
///
/// Configuration works in concert with the [`mtls`](crate::mtls) module, which
/// provides a request guard to validate, verify, and retrieve client
/// certificates in routes.
///
/// By default, mutual TLS is disabled and client certificates are not required,
/// validated or verified. To enable mutual TLS, the `mtls` feature must be
/// enabled and support configured via two `tls.mutual` parameters:
///
/// * `ca_certs`
///
/// A required path to a PEM file or raw bytes to a DER-encoded X.509 TLS
/// certificate chain for the certificate authority to verify client
/// certificates against. When a path is configured in a file, such as
/// `Rocket.toml`, relative paths are interpreted as relative to the source
/// file's directory.
///
/// * `mandatory`
///
/// An optional boolean that control whether client authentication is
/// required.
///
/// When `true`, client authentication is required. TLS connections where
/// the client does not present a certificate are immediately terminated.
/// When `false`, the client is not required to present a certificate. In
/// either case, if a certificate _is_ presented, it must be valid or the
/// connection is terminated.
///
/// In a `Rocket.toml`, configuration might look like:
///
/// ```toml
/// [default.tls.mutual]
/// ca_certs = "/ssl/ca_cert.pem"
/// mandatory = true # when absent, defaults to false
/// ```
///
/// Programmatically, configuration might look like:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::config::{Config, TlsConfig, MutualTls};
///
/// #[launch]
/// fn rocket() -> _ {
/// let tls_config = TlsConfig::from_paths("/ssl/certs.pem", "/ssl/key.pem")
/// .with_mutual(MutualTls::from_path("/ssl/ca_cert.pem"));
///
/// let config = Config {
/// tls: Some(tls_config),
/// ..Default::default()
/// };
///
/// rocket::custom(config)
/// }
/// ```
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[cfg(feature = "mtls")]
#[cfg_attr(nightly, doc(cfg(feature = "mtls")))]
pub struct MutualTls {
/// Path to a PEM file with, or raw bytes for, DER-encoded Certificate
/// Authority certificates which will be used to verify client-presented
/// certificates.
// TODO: We should support more than one root.
pub(crate) ca_certs: Either<RelativePathBuf, Vec<u8>>,
/// Whether the client is required to present a certificate.
///
/// When `true`, the client is required to present a valid certificate to
/// proceed with TLS. When `false`, the client is not required to present a
/// certificate. In either case, if a certificate _is_ presented, it must be
/// valid or the connection is terminated.
#[serde(default)]
#[serde(deserialize_with = "figment::util::bool_from_str_or_int")]
pub mandatory: bool,

View File

@ -52,7 +52,7 @@ pub struct StreamReader<'r> {
/// The current state of `StreamReader` `AsyncRead` adapter.
enum State {
Pending,
Partial(Cursor<hyper::Bytes>),
Partial(Cursor<hyper::body::Bytes>),
Done,
}
@ -257,7 +257,7 @@ impl AsyncRead for DataStream<'_> {
}
impl Stream for StreamKind<'_> {
type Item = io::Result<hyper::Bytes>;
type Item = io::Result<hyper::body::Bytes>;
fn poll_next(
self: Pin<&mut Self>,

View File

@ -63,6 +63,7 @@
//! |-----------|---------------------------------------------------------|
//! | `secrets` | Support for authenticated, encrypted [private cookies]. |
//! | `tls` | Support for [TLS] encrypted connections. |
//! | `mtls` | Support for verified clients via [mutual TLS]. |
//! | `json` | Support for [JSON (de)serialization]. |
//! | `msgpack` | Support for [MessagePack (de)serialization]. |
//! | `uuid` | Support for [UUID value parsing and (de)serialization]. |
@ -79,6 +80,7 @@
//! [UUID value parsing and (de)serialization]: crate::serde::uuid
//! [private cookies]: https://rocket.rs/v0.5-rc/guide/requests/#private-cookies
//! [TLS]: https://rocket.rs/v0.5-rc/guide/configuration/#tls
//! [mutual TLS]: crate::mtls
//!
//! ## Configuration
//!
@ -137,6 +139,10 @@ pub mod http {
pub use crate::cookies::*;
}
#[cfg(feature = "mtls")]
#[cfg_attr(nightly, doc(cfg(feature = "mtls")))]
pub mod mtls;
/// TODO: We need a futures mod or something.
mod trip_wire;
mod shutdown;

24
core/lib/src/mtls.rs Normal file
View File

@ -0,0 +1,24 @@
//! Support for mutual TLS client certificates.
//!
//! For details on how to configure mutual TLS, see
//! [`MutualTls`](crate::config::MutualTls) and the [TLS
//! guide](https://rocket.rs/v0.5-rc/guide/configuration/#tls). See
//! [`Certificate`] for a request guard that validated, verifies, and retrieves
//! client certificates.
#[doc(inline)]
pub use crate::http::tls::mtls::*;
use crate::request::{Request, FromRequest, Outcome};
use crate::outcome::{try_outcome, IntoOutcome};
use crate::http::Status;
#[crate::async_trait]
impl<'r> FromRequest<'r> for Certificate<'r> {
type Error = Error;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let certs = try_outcome!(req.connection.client_certificates.as_ref().or_forward(()));
Certificate::parse(certs).into_outcome(Status::Unauthorized)
}
}

View File

@ -14,6 +14,8 @@ pub use self::from_param::{FromParam, FromSegments};
#[doc(inline)]
pub use crate::response::flash::FlashMessage;
pub(crate) use self::request::ConnectionMeta;
crate::export! {
/// Store and immediately retrieve a value `$v` in `$request`'s local cache
/// using a locally generated anonymous type to avoid type conflicts.

View File

@ -8,16 +8,16 @@ use state::{Container, Storage};
use futures::future::BoxFuture;
use atomic::{Atomic, Ordering};
// use crate::request::{FromParam, FromSegments, FromRequest, Outcome};
use crate::{Rocket, Route, Orbit};
use crate::request::{FromParam, FromSegments, FromRequest, Outcome};
use crate::form::{self, ValueField, FromForm};
use crate::data::Limits;
use crate::{Rocket, Route, Orbit};
use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority};
use crate::http::{hyper, Method, Header, HeaderMap};
use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie};
use crate::http::uncased::UncasedStr;
use crate::data::Limits;
use crate::http::private::RawCertificate;
use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority};
/// The type of an incoming web request.
///
@ -28,12 +28,19 @@ use crate::data::Limits;
pub struct Request<'r> {
method: Atomic<Method>,
uri: Origin<'r>,
host: Option<Host<'r>>,
headers: HeaderMap<'r>,
remote: Option<SocketAddr>,
pub(crate) connection: ConnectionMeta,
pub(crate) state: RequestState<'r>,
}
/// Information derived from an incoming connection, if any.
#[derive(Clone)]
pub(crate) struct ConnectionMeta {
pub remote: Option<SocketAddr>,
pub client_certificates: Option<Arc<Vec<RawCertificate>>>,
}
/// Information derived from the request.
pub(crate) struct RequestState<'r> {
pub rocket: &'r Rocket<Orbit>,
pub route: Atomic<Option<&'r Route>>,
@ -41,6 +48,7 @@ pub(crate) struct RequestState<'r> {
pub accept: Storage<Option<Accept>>,
pub content_type: Storage<Option<ContentType>>,
pub cache: Arc<Container![Send + Sync]>,
pub host: Option<Host<'r>>,
}
impl Request<'_> {
@ -48,9 +56,8 @@ impl Request<'_> {
Request {
method: Atomic::new(self.method()),
uri: self.uri.clone(),
host: self.host.clone(),
headers: self.headers.clone(),
remote: self.remote,
connection: self.connection.clone(),
state: self.state.clone(),
}
}
@ -65,6 +72,7 @@ impl RequestState<'_> {
accept: self.accept.clone(),
content_type: self.content_type.clone(),
cache: self.cache.clone(),
host: self.host.clone(),
}
}
}
@ -79,10 +87,12 @@ impl<'r> Request<'r> {
) -> Request<'r> {
Request {
uri,
host: None,
method: Atomic::new(method),
headers: HeaderMap::new(),
connection: ConnectionMeta {
remote: None,
client_certificates: None,
},
state: RequestState {
rocket,
route: Atomic::new(None),
@ -90,6 +100,7 @@ impl<'r> Request<'r> {
accept: Storage::new(),
content_type: Storage::new(),
cache: Arc::new(<Container![Send + Sync]>::new()),
host: None,
}
}
}
@ -179,6 +190,10 @@ impl<'r> Request<'r> {
/// component. Otherwise, this method returns the contents of the
/// `:authority` pseudo-header request field.
///
/// Note that this method _only_ reflects the `HOST` header in the _initial_
/// request and not any changes made thereafter. To change the value
/// returned by this method, use [`Request::set_host()`].
///
/// # ⚠️ DANGER ⚠️
///
/// Using the user-controlled `host` to construct URLs is a security hazard!
@ -257,7 +272,7 @@ impl<'r> Request<'r> {
/// ```
#[inline(always)]
pub fn host(&self) -> Option<&Host<'r>> {
self.host.as_ref()
self.state.host.as_ref()
}
/// Sets the host of `self` to `host`.
@ -282,7 +297,7 @@ impl<'r> Request<'r> {
/// ```
#[inline(always)]
pub fn set_host(&mut self, host: Host<'r>) {
self.host = Some(host);
self.state.host = Some(host);
}
/// Returns the raw address of the remote connection that initiated this
@ -314,7 +329,7 @@ impl<'r> Request<'r> {
/// ```
#[inline(always)]
pub fn remote(&self) -> Option<SocketAddr> {
self.remote
self.connection.remote
}
/// Sets the remote address of `self` to `address`.
@ -337,7 +352,7 @@ impl<'r> Request<'r> {
/// ```
#[inline(always)]
pub fn set_remote(&mut self, address: SocketAddr) {
self.remote = Some(address);
self.connection.remote = Some(address);
}
/// Returns the IP address in the "X-Real-IP" header of the request if such
@ -949,8 +964,8 @@ impl<'r> Request<'r> {
/// Convert from Hyper types into a Rocket Request.
pub(crate) fn from_hyp(
rocket: &'r Rocket<Orbit>,
hyper: &'r hyper::RequestParts,
addr: SocketAddr
hyper: &'r hyper::request::Parts,
connection: Option<ConnectionMeta>,
) -> Result<Request<'r>, Error<'r>> {
// Ensure that the method is known. TODO: Allow made-up methods?
let method = Method::from_hyp(&hyper.method)
@ -965,11 +980,13 @@ impl<'r> Request<'r> {
// Construct the request object.
let mut request = Request::new(rocket, method, uri);
request.set_remote(addr);
if let Some(connection) = connection {
request.connection = connection;
}
// Determine the host. On HTTP < 2, use the `HOST` header. Otherwise,
// use the `:authority` pseudo-header which hyper makes part of the URI.
request.host = if hyper.version < hyper::Version::HTTP_2 {
request.state.host = if hyper.version < hyper::Version::HTTP_2 {
hyper.headers.get("host").and_then(|h| Host::parse_bytes(h.as_bytes()).ok())
} else {
hyper.uri.host().map(|h| Host::new(Authority::new(None, h, hyper.uri.port_u16())))

View File

@ -1,4 +1,3 @@
use std::net::{Ipv4Addr, SocketAddrV4};
use std::collections::HashMap;
use crate::Request;
@ -17,9 +16,8 @@ macro_rules! assert_headers {
// Create a valid `Rocket` and convert the hyper req to a Rocket one.
let client = Client::debug_with(vec![]).unwrap();
let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000).into();
let hyper = req.into_parts().0;
let req = Request::from_hyp(client.rocket(), &hyper, addr).unwrap();
let req = Request::from_hyp(client.rocket(), &hyper, None).unwrap();
// Dispatch the request and check that the headers match.
let actual_headers = req.headers();

View File

@ -12,6 +12,7 @@ use crate::form::Form;
use crate::outcome::Outcome;
use crate::error::{Error, ErrorKind};
use crate::ext::{AsyncReadExt, CancellableListener, CancellableIo};
use crate::request::ConnectionMeta;
use crate::http::{uri::Origin, hyper, Method, Status, Header};
use crate::http::private::{bind_tcp, Listener, Connection, Incoming};
@ -61,7 +62,7 @@ async fn handle<Fut, T, F>(name: Option<&str>, run: F) -> Option<T>
// `HyperResponse` type, this function does the actual response processing.
async fn hyper_service_fn(
rocket: Arc<Rocket<Orbit>>,
addr: std::net::SocketAddr,
conn: ConnectionMeta,
hyp_req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, io::Error> {
// This future must return a hyper::Response, but the response body might
@ -72,7 +73,7 @@ async fn hyper_service_fn(
tokio::spawn(async move {
// Convert a Hyper request into a Rocket request.
let (h_parts, mut h_body) = hyp_req.into_parts();
match Request::from_hyp(&rocket, &h_parts, addr) {
match Request::from_hyp(&rocket, &h_parts, Some(conn)) {
Ok(mut req) => {
// Convert into Rocket `Data`, dispatch request, write response.
let mut data = Data::from(&mut h_body);
@ -366,21 +367,19 @@ impl Rocket<Orbit> {
.map_err(|e| Error::new(ErrorKind::Io(e)))?;
#[cfg(feature = "tls")]
if self.config.tls_enabled() {
if let Some(ref config) = self.config.tls {
use crate::http::tls::TlsListener;
let (certs, key) = config.to_readers().map_err(ErrorKind::Io)?;
let ciphers = config.rustls_ciphers();
let server_order = config.prefer_server_cipher_order;
let l = TlsListener::bind(addr, certs, key, ciphers, server_order).await
.map_err(ErrorKind::Bind)?;
let conf = config.to_native_config().map_err(ErrorKind::Io)?;
let l = TlsListener::bind(addr, conf).await.map_err(ErrorKind::Bind)?;
addr = l.local_addr().unwrap_or(addr);
self.config.address = addr.ip();
self.config.port = addr.port();
ready(&mut self).await;
return self.http_server(l).await;
}
}
let l = bind_tcp(addr).await.map_err(ErrorKind::Bind)?;
addr = l.local_addr().unwrap_or(addr);
@ -443,10 +442,14 @@ impl Rocket<Orbit> {
let rocket = Arc::new(self);
let service_fn = move |conn: &CancellableIo<_, L::Connection>| {
let rocket = rocket.clone();
let remote = conn.peer_address().unwrap_or_else(|| ([0, 0, 0, 0], 0).into());
let connection = ConnectionMeta {
remote: conn.peer_address(),
client_certificates: conn.peer_certificates().map(Arc::new),
};
async move {
Ok::<_, std::convert::Infallible>(hyper::service_fn(move |req| {
hyper_service_fn(rocket.clone(), remote, req)
Ok::<_, std::convert::Infallible>(hyper::service::service_fn(move |req| {
hyper_service_fn(rocket.clone(), connection.clone(), req)
}))
}
};
@ -457,7 +460,7 @@ impl Rocket<Orbit> {
.http1_keepalive(http1_keepalive)
.http1_preserve_header_case(true)
.http2_keep_alive_interval(http2_keep_alive)
.serve(hyper::make_service_fn(service_fn))
.serve(hyper::service::make_service_fn(service_fn))
.with_graceful_shutdown(shutdown.clone())
.map_err(|e| Error::new(ErrorKind::Runtime(Box::new(e))));

View File

@ -102,6 +102,7 @@ function test_core() {
FEATURES=(
secrets
tls
mtls
json
msgpack
uuid

View File

@ -236,24 +236,8 @@ Security). To enable TLS support:
certs = "path/to/certs.pem" # Path or bytes to DER-encoded X.509 TLS cert chain.
```
Next time the server is run, Rocket will report that TLS is enabled during
ignition:
```text
🔧 Configured for debug.
...
>> tls: enabled
```
The [TLS example](@example/tls) illustrates a fully configured TLS server.
! warning: Rocket's built-in TLS supports only TLS 1.2 and 1.3. This may not be
suitable for production use.
#### TLS Parameters
The `tls` parameter is expected to be a dictionary with at-most four keys which
deserialize into the [`TlsConfig`] structure. These are:
The `tls` parameter is expected to be a dictionary that deserializes into a
[`TlsConfig`] structure:
| key | required | type |
|------------------------------|-----------|-------------------------------------------------------|
@ -261,9 +245,11 @@ deserialize into the [`TlsConfig`] structure. These are:
| `certs` | **_yes_** | Path or bytes to DER-encoded X.509 TLS cert chain. |
| `ciphers` | no | Array of [`CipherSuite`]s to enable. |
| `prefer_server_cipher_order` | no | Boolean for whether to [prefer server cipher suites]. |
| `mutual` | no | A map with [mutual TLS] configuration. |
[`CipherSuite`]: @api/rocket/config/enum.CipherSuite.html
[prefer server cipher suites]: @api/rocket/config/struct.TlsConfig.html#method.with_preferred_server_cipher_order
[mutual TLS]: #mutual-tls
When specified via TOML or other serialized formats, each [`CipherSuite`] is
written as a string representation of the respective variant. For example,
@ -288,16 +274,69 @@ ciphers = [
]
```
### Mutual TLS
Rocket supports mutual TLS client authentication. Configuration works in concert
with the [`mtls`] module, which provides a request guard to validate, verify,
and retrieve client certificates in routes.
By default, mutual TLS is disabled and client certificates are not required,
validated or verified. To enable mutual TLS, the `mtls` feature must be
enabled and support configured via the `tls.mutual` config parameter:
1. Enable the `mtls` crate feature in `Cargo.toml`:
```toml,ignore
[dependencies]
rocket = { version = "0.5.0-rc.1", features = ["mtls"] }
```
This implicitly enables the `tls` feature.
2. Configure a CA certificate chain via the `tls.mutual.ca_certs`
configuration parameter. With the default provider, this can be done via
`Rocket.toml` as:
```toml,ignore
[default.tls.mutual]
ca_certs = "path/to/ca_certs.pem" # Path or bytes to DER-encoded X.509 TLS cert chain.
mandatory = true # when absent, defaults to false
```
The `tls.mutual` parameter is expected to be a dictionary that deserializes into a
[`MutualTls`] structure:
| key | required | type |
|-------------|-----------|-------------------------------------------------------------|
| `ca_certs` | **_yes_** | Path or bytes to DER-encoded X.509 TLS cert chain. |
| `mandatory` | no | Boolean controlling whether the client _must_ authenticate. |
[`MutualTls`]: @api/rocket/config/struct.MutualTls.html
[`mtls`]: @api/rocket/mtls/index.html
Rocket reports if TLS and/or mTLS are enabled at launch time:
```text
🔧 Configured for debug.
...
>> tls: enabled w/mtls
```
The [TLS example](@example/tls) illustrates a fully configured TLS server with
mutual TLS.
! warning: Rocket's built-in TLS supports only TLS 1.2 and 1.3. This may not be
suitable for production use.
### Workers
The `workers` parameter sets the number of threads used for parallel task
execution; there is no limit to the number of concurrent tasks. Due to a
limitation in upstream async executers, unlike other values, the `workers`
configuration value cannot be reconfigured or be configured from sources other
than those provided by [`Config::figment()`], detailed below. In other words,
only the values set by the `ROCKET_WORKERS` environment variable or in the
`workers` property of `Rocket.toml` will be considered - all other `workers`
values are ignored.
than those provided by [`Config::figment()`]. In other words, only the values
set by the `ROCKET_WORKERS` environment variable or in the `workers` property of
`Rocket.toml` will be considered - all other `workers` values are ignored.
## Extracting Values