use std::io;
use std::sync::Arc;
use figment::value::magic::{Either, RelativePathBuf};
use futures::TryFutureExt;
use indexmap::IndexSet;
use rustls::crypto::{ring, CryptoProvider};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::server::{ServerConfig, ServerSessionMemoryCache, WebPkiClientVerifier};
use rustls_pki_types::pem::PemObject;
use serde::{Deserialize, Serialize};
use crate::tls::error::{Error, KeyError, Result};
use crate::tls::resolver::DynResolver;
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub struct TlsConfig {
pub(crate) certs: Either<RelativePathBuf, Vec<u8>>,
pub(crate) key: Either<RelativePathBuf, Vec<u8>>,
#[serde(default = "CipherSuite::default_set")]
pub(crate) ciphers: IndexSet<CipherSuite>,
#[serde(default)]
pub(crate) prefer_server_cipher_order: bool,
#[serde(default)]
#[cfg(feature = "mtls")]
#[cfg_attr(nightly, doc(cfg(feature = "mtls")))]
pub(crate) mutual: Option<crate::mtls::MtlsConfig>,
#[serde(skip)]
pub(crate) resolver: Option<DynResolver>,
}
#[allow(non_camel_case_types)]
#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, Deserialize, Serialize)]
#[non_exhaustive]
pub enum CipherSuite {
TLS_CHACHA20_POLY1305_SHA256,
TLS_AES_256_GCM_SHA384,
TLS_AES_128_GCM_SHA256,
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}
impl Default for TlsConfig {
fn default() -> Self {
TlsConfig {
certs: Either::Right(vec![]),
key: Either::Right(vec![]),
ciphers: CipherSuite::default_set(),
prefer_server_cipher_order: false,
#[cfg(feature = "mtls")]
mutual: None,
resolver: None,
}
}
}
impl TlsConfig {
pub fn from_paths<C, K>(certs: C, key: K) -> Self
where
C: AsRef<std::path::Path>,
K: AsRef<std::path::Path>,
{
TlsConfig {
certs: Either::Left(certs.as_ref().to_path_buf().into()),
key: Either::Left(key.as_ref().to_path_buf().into()),
..TlsConfig::default()
}
}
pub fn from_bytes(certs: &[u8], key: &[u8]) -> Self {
TlsConfig {
certs: Either::Right(certs.to_vec()),
key: Either::Right(key.to_vec()),
..TlsConfig::default()
}
}
pub fn with_ciphers<C>(mut self, ciphers: C) -> Self
where
C: IntoIterator<Item = CipherSuite>,
{
self.ciphers = ciphers.into_iter().collect();
self
}
pub fn with_preferred_server_cipher_order(mut self, prefer_server_order: bool) -> Self {
self.prefer_server_cipher_order = prefer_server_order;
self
}
#[cfg(feature = "mtls")]
#[cfg_attr(nightly, doc(cfg(feature = "mtls")))]
pub fn with_mutual(mut self, config: crate::mtls::MtlsConfig) -> Self {
self.mutual = Some(config);
self
}
pub fn certs(&self) -> either::Either<std::path::PathBuf, &[u8]> {
match &self.certs {
Either::Left(path) => either::Either::Left(path.relative()),
Either::Right(bytes) => either::Either::Right(bytes),
}
}
pub fn certs_reader(&self) -> io::Result<Box<dyn io::BufRead + Sync + Send>> {
to_reader(&self.certs)
}
pub fn key(&self) -> either::Either<std::path::PathBuf, &[u8]> {
match &self.key {
Either::Left(path) => either::Either::Left(path.relative()),
Either::Right(bytes) => either::Either::Right(bytes),
}
}
pub fn key_reader(&self) -> io::Result<Box<dyn io::BufRead + Sync + Send>> {
to_reader(&self.key)
}
pub fn ciphers(&self) -> impl Iterator<Item = CipherSuite> + '_ {
self.ciphers.iter().copied()
}
pub fn prefer_server_cipher_order(&self) -> bool {
self.prefer_server_cipher_order
}
#[cfg(feature = "mtls")]
#[cfg_attr(nightly, doc(cfg(feature = "mtls")))]
pub fn mutual(&self) -> Option<&crate::mtls::MtlsConfig> {
self.mutual.as_ref()
}
pub async fn server_config(&self) -> Result<rustls::server::ServerConfig> {
let this = self.clone();
tokio::task::spawn_blocking(move || this._server_config())
.map_err(io::Error::other)
.await?
}
pub(crate) fn _server_config(&self) -> Result<rustls::server::ServerConfig> {
let provider = Arc::new(self.default_crypto_provider());
#[cfg(feature = "mtls")]
let verifier = match self.mutual {
Some(ref mtls) => {
let ca = Arc::new(mtls.load_ca_certs()?);
let verifier = WebPkiClientVerifier::builder_with_provider(ca, provider.clone());
match mtls.mandatory {
true => verifier.build()?,
false => verifier.allow_unauthenticated().build()?,
}
}
None => WebPkiClientVerifier::no_client_auth(),
};
#[cfg(not(feature = "mtls"))]
let verifier = WebPkiClientVerifier::no_client_auth();
let mut tls_config = ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.with_client_cert_verifier(verifier)
.with_single_cert(self.load_certs()?, self.load_key()?)?;
tls_config.ignore_client_order = self.prefer_server_cipher_order;
tls_config.session_storage = ServerSessionMemoryCache::new(1024);
tls_config.ticketer = rustls::crypto::ring::Ticketer::new()?;
tls_config.alpn_protocols = vec![b"http/1.1".to_vec()];
if cfg!(feature = "http2") {
tls_config.alpn_protocols.insert(0, b"h2".to_vec());
}
Ok(tls_config)
}
pub fn validate(&self) -> Result<()> {
self._server_config().map(|_| ())
}
}
impl TlsConfig {
pub(crate) fn load_certs(&self) -> Result<Vec<CertificateDer<'static>>> {
CertificateDer::pem_reader_iter(&mut self.certs_reader()?)
.collect::<Result<_, _>>()
.map_err(|e| Error::CertChain(std::io::Error::other(e)))
}
pub(crate) fn load_key(&self) -> Result<PrivateKeyDer<'static>> {
let mut keys = PrivateKeyDer::pem_reader_iter(&mut self.key_reader()?)
.collect::<Result<Vec<PrivateKeyDer<'static>>, _>>()
.map_err(|e| KeyError::Io(std::io::Error::other(e)))?;
if keys.len() != 1 {
return Err(KeyError::BadKeyCount(keys.len()).into());
}
let key = keys.remove(0);
self.default_crypto_provider()
.key_provider
.load_private_key(key.clone_key())
.map_err(KeyError::Unsupported)?;
Ok(key)
}
pub(crate) fn default_crypto_provider(&self) -> CryptoProvider {
CryptoProvider::get_default()
.map(|arc| (**arc).clone())
.unwrap_or_else(|| rustls::crypto::CryptoProvider {
cipher_suites: self
.ciphers()
.map(|cipher| match cipher {
CipherSuite::TLS_CHACHA20_POLY1305_SHA256 => {
ring::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256
}
CipherSuite::TLS_AES_256_GCM_SHA384 => {
ring::cipher_suite::TLS13_AES_256_GCM_SHA384
}
CipherSuite::TLS_AES_128_GCM_SHA256 => {
ring::cipher_suite::TLS13_AES_128_GCM_SHA256
}
CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => {
ring::cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
}
CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => {
ring::cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => {
ring::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
}
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => {
ring::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
}
CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => {
ring::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
}
CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => {
ring::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
}
})
.collect(),
..ring::default_provider()
})
}
}
impl CipherSuite {
pub const DEFAULT_SET: [CipherSuite; 9] = [
CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_AES_256_GCM_SHA384,
CipherSuite::TLS_AES_128_GCM_SHA256,
CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
];
pub const TLS_V13_SET: [CipherSuite; 3] = [
CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_AES_256_GCM_SHA384,
CipherSuite::TLS_AES_128_GCM_SHA256,
];
pub const TLS_V12_SET: [CipherSuite; 6] = [
CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
];
fn default_set() -> IndexSet<Self> {
Self::DEFAULT_SET.iter().copied().collect()
}
}
pub(crate) fn to_reader(
value: &Either<RelativePathBuf, Vec<u8>>,
) -> io::Result<Box<dyn io::BufRead + Sync + Send>> {
match value {
Either::Left(path) => {
let path = path.relative();
let file = std::fs::File::open(&path).map_err(move |e| {
let source = figment::Source::File(path);
let msg = format!("error reading TLS file `{source}`: {e}");
io::Error::new(e.kind(), msg)
})?;
Ok(Box::new(io::BufReader::new(file)))
}
Either::Right(vec) => Ok(Box::new(io::Cursor::new(vec.clone()))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use figment::{
providers::{Format, Toml},
Figment,
};
#[test]
fn test_tls_config_from_file() {
use crate::tls::{CipherSuite, TlsConfig};
use pretty_assertions::assert_eq;
figment::Jail::expect_with(|jail| {
jail.create_file(
"Rocket.toml",
r#"
[global]
shutdown.ctrlc = 0
ident = false
[global.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
[global.limits]
forms = "1mib"
json = "10mib"
stream = "50kib"
"#,
)?;
let config: TlsConfig = crate::Config::figment().extract_inner("tls")?;
assert_eq!(
config,
TlsConfig::from_paths("/ssl/cert.pem", "/ssl/key.pem")
);
jail.create_file(
"Rocket.toml",
r#"
[global.tls]
certs = "cert.pem"
key = "key.pem"
"#,
)?;
let config: TlsConfig = crate::Config::figment().extract_inner("tls")?;
assert_eq!(
config,
TlsConfig::from_paths(
jail.directory().join("cert.pem"),
jail.directory().join("key.pem")
)
);
jail.create_file(
"TLS.toml",
r#"
certs = "cert.pem"
key = "key.pem"
prefer_server_cipher_order = true
ciphers = [
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
]
"#,
)?;
let config: TlsConfig = Figment::from(Toml::file("TLS.toml")).extract()?;
let cert_path = jail.directory().join("cert.pem");
let key_path = jail.directory().join("key.pem");
assert_eq!(
config,
TlsConfig::from_paths(cert_path, key_path)
.with_preferred_server_cipher_order(true)
.with_ciphers([
CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_AES_256_GCM_SHA384,
CipherSuite::TLS_AES_128_GCM_SHA256,
CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
])
);
jail.create_file(
"Rocket.toml",
r#"
[global]
shutdown.ctrlc = 0
ident = false
[global.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
[global.limits]
forms = "1mib"
json = "10mib"
stream = "50kib"
"#,
)?;
let config: TlsConfig = crate::Config::figment().extract_inner("tls")?;
assert_eq!(
config,
TlsConfig::from_paths("/ssl/cert.pem", "/ssl/key.pem")
);
jail.create_file(
"Rocket.toml",
r#"
[global.tls]
certs = "cert.pem"
key = "key.pem"
"#,
)?;
let config: TlsConfig = crate::Config::figment().extract_inner("tls")?;
assert_eq!(
config,
TlsConfig::from_paths(
jail.directory().join("cert.pem"),
jail.directory().join("key.pem")
)
);
jail.create_file(
"Rocket.toml",
r#"
[global.tls]
certs = "cert.pem"
key = "key.pem"
prefer_server_cipher_order = true
ciphers = [
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
]
"#,
)?;
let config: TlsConfig = crate::Config::figment().extract_inner("tls")?;
let cert_path = jail.directory().join("cert.pem");
let key_path = jail.directory().join("key.pem");
assert_eq!(
config,
TlsConfig::from_paths(cert_path, key_path)
.with_preferred_server_cipher_order(true)
.with_ciphers([
CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_AES_256_GCM_SHA384,
CipherSuite::TLS_AES_128_GCM_SHA256,
CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
])
);
Ok(())
});
}
macro_rules! tls_example_private_pem {
($k:expr) => {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../examples/tls/private/",
$k
)
};
}
#[test]
fn verify_load_private_keys_of_different_types() -> Result<()> {
let key_paths = [
tls_example_private_pem!("rsa_sha256_key.pem"),
tls_example_private_pem!("ecdsa_nistp256_sha256_key_pkcs8.pem"),
tls_example_private_pem!("ecdsa_nistp384_sha384_key_pkcs8.pem"),
tls_example_private_pem!("ed25519_key.pem"),
];
for key in key_paths {
TlsConfig::from_paths("", key).load_key()?;
}
Ok(())
}
#[test]
fn verify_load_certs_of_different_types() -> Result<()> {
let cert_paths = [
tls_example_private_pem!("rsa_sha256_cert.pem"),
tls_example_private_pem!("ecdsa_nistp256_sha256_cert.pem"),
tls_example_private_pem!("ecdsa_nistp384_sha384_cert.pem"),
tls_example_private_pem!("ed25519_cert.pem"),
];
for cert in cert_paths {
TlsConfig::from_paths(cert, "").load_certs()?;
}
Ok(())
}
}