mirror of https://github.com/rwf2/Rocket.git
Upgrade 'rustls' to '0.22'.
In the process, the following improvements were also made: * Error messages related to TLS were improved. * 'Redirector' in 'tls' example was improved.
This commit is contained in:
parent
a59f3c4c1f
commit
9c2b74b23c
|
@ -30,9 +30,9 @@ percent-encoding = "2"
|
||||||
http = "0.2"
|
http = "0.2"
|
||||||
time = { version = "0.3", features = ["formatting", "macros"] }
|
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||||
indexmap = "2"
|
indexmap = "2"
|
||||||
rustls = { version = "0.21", optional = true }
|
rustls = { version = "0.22", optional = true }
|
||||||
tokio-rustls = { version = "0.24", optional = true }
|
tokio-rustls = { version = "0.25", optional = true }
|
||||||
rustls-pemfile = { version = "1.0.2", optional = true }
|
rustls-pemfile = { version = "2.0.0", optional = true }
|
||||||
tokio = { version = "1.6.1", features = ["net", "sync", "time"] }
|
tokio = { version = "1.6.1", features = ["net", "sync", "time"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
ref-cast = "1.0"
|
ref-cast = "1.0"
|
||||||
|
|
|
@ -17,36 +17,45 @@ use state::InitCell;
|
||||||
pub use tokio::net::TcpListener;
|
pub use tokio::net::TcpListener;
|
||||||
|
|
||||||
/// A thin wrapper over raw, DER-encoded X.509 client certificate data.
|
/// A thin wrapper over raw, DER-encoded X.509 client certificate data.
|
||||||
// NOTE: `rustls::Certificate` is exactly isomorphic to `CertificateData`.
|
#[cfg(not(feature = "tls"))]
|
||||||
#[doc(inline)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
#[cfg(feature = "tls")]
|
pub struct CertificateDer(pub(crate) Vec<u8>);
|
||||||
pub use rustls::Certificate as CertificateData;
|
|
||||||
|
|
||||||
/// A thin wrapper over raw, DER-encoded X.509 client certificate data.
|
/// A thin wrapper over raw, DER-encoded X.509 client certificate data.
|
||||||
#[cfg(not(feature = "tls"))]
|
#[cfg(feature = "tls")]
|
||||||
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub struct CertificateData(pub Vec<u8>);
|
#[repr(transparent)]
|
||||||
|
pub struct CertificateDer(pub(crate) rustls::pki_types::CertificateDer<'static>);
|
||||||
|
|
||||||
/// A collection of raw certificate data.
|
/// A collection of raw certificate data.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct Certificates(Arc<InitCell<Vec<CertificateData>>>);
|
pub struct Certificates(Arc<InitCell<Vec<CertificateDer>>>);
|
||||||
|
|
||||||
impl From<Vec<CertificateData>> for Certificates {
|
impl From<Vec<CertificateDer>> for Certificates {
|
||||||
fn from(value: Vec<CertificateData>) -> Self {
|
fn from(value: Vec<CertificateDer>) -> Self {
|
||||||
Certificates(Arc::new(value.into()))
|
Certificates(Arc::new(value.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
impl From<Vec<rustls::pki_types::CertificateDer<'static>>> for Certificates {
|
||||||
|
fn from(value: Vec<rustls::pki_types::CertificateDer<'static>>) -> Self {
|
||||||
|
let value: Vec<_> = value.into_iter().map(CertificateDer).collect();
|
||||||
|
Certificates(Arc::new(value.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
impl Certificates {
|
impl Certificates {
|
||||||
/// Set the the raw certificate chain data. Only the first call actually
|
/// Set the the raw certificate chain data. Only the first call actually
|
||||||
/// sets the data; the remaining do nothing.
|
/// sets the data; the remaining do nothing.
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
pub(crate) fn set(&self, data: Vec<CertificateData>) {
|
pub(crate) fn set(&self, data: Vec<CertificateDer>) {
|
||||||
self.0.set(data);
|
self.0.set(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the raw certificate chain data, if any is available.
|
/// Returns the raw certificate chain data, if any is available.
|
||||||
pub fn chain_data(&self) -> Option<&[CertificateData]> {
|
pub fn chain_data(&self) -> Option<&[CertificateDer]> {
|
||||||
self.0.try_get().map(|v| v.as_slice())
|
self.0.try_get().map(|v| v.as_slice())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum KeyError {
|
||||||
|
BadKeyCount(usize),
|
||||||
|
Io(std::io::Error),
|
||||||
|
Unsupported(rustls::Error),
|
||||||
|
BadItem(rustls_pemfile::Item),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Io(std::io::Error),
|
||||||
|
Tls(rustls::Error),
|
||||||
|
Mtls(rustls::server::VerifierBuilderError),
|
||||||
|
CertChain(std::io::Error),
|
||||||
|
PrivKey(KeyError),
|
||||||
|
CertAuth(rustls::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
use Error::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Io(e) => write!(f, "i/o error during tls binding: {e}"),
|
||||||
|
Tls(e) => write!(f, "tls configuration error: {e}"),
|
||||||
|
Mtls(e) => write!(f, "mtls verifier error: {e}"),
|
||||||
|
CertChain(e) => write!(f, "failed to process certificate chain: {e}"),
|
||||||
|
PrivKey(e) => write!(f, "failed to process private key: {e}"),
|
||||||
|
CertAuth(e) => write!(f, "failed to process certificate authority: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for KeyError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
use KeyError::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Io(e) => write!(f, "error reading key file: {e}"),
|
||||||
|
BadKeyCount(0) => write!(f, "no valid keys found. is the file malformed?"),
|
||||||
|
BadKeyCount(n) => write!(f, "expected exactly 1 key, found {n}"),
|
||||||
|
Unsupported(e) => write!(f, "key is valid but is unsupported: {e}"),
|
||||||
|
BadItem(i) => write!(f, "found unexpected item in key file: {i:#?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for KeyError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
KeyError::Io(e) => Some(e),
|
||||||
|
KeyError::Unsupported(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Error::Io(e) => Some(e),
|
||||||
|
Error::Tls(e) => Some(e),
|
||||||
|
Error::Mtls(e) => Some(e),
|
||||||
|
Error::CertChain(e) => Some(e),
|
||||||
|
Error::PrivKey(e) => Some(e),
|
||||||
|
Error::CertAuth(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
Error::Io(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rustls::Error> for Error {
|
||||||
|
fn from(e: rustls::Error) -> Self {
|
||||||
|
Error::Tls(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rustls::server::VerifierBuilderError> for Error {
|
||||||
|
fn from(value: rustls::server::VerifierBuilderError) -> Self {
|
||||||
|
Error::Mtls(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KeyError> for Error {
|
||||||
|
fn from(value: KeyError) -> Self {
|
||||||
|
Error::PrivKey(value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,9 +8,10 @@ use std::net::SocketAddr;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio_rustls::{Accept, TlsAcceptor, server::TlsStream as BareTlsStream};
|
use tokio_rustls::{Accept, TlsAcceptor, server::TlsStream as BareTlsStream};
|
||||||
|
use rustls::server::{ServerSessionMemoryCache, ServerConfig, WebPkiClientVerifier};
|
||||||
|
|
||||||
use crate::tls::util::{load_certs, load_private_key, load_ca_certs};
|
use crate::tls::util::{load_cert_chain, load_key, load_ca_certs};
|
||||||
use crate::listener::{Connection, Listener, Certificates};
|
use crate::listener::{Connection, Listener, Certificates, CertificateDer};
|
||||||
|
|
||||||
/// A TLS listener over TCP.
|
/// A TLS listener over TCP.
|
||||||
pub struct TlsListener {
|
pub struct TlsListener {
|
||||||
|
@ -40,7 +41,7 @@ pub struct TlsListener {
|
||||||
///
|
///
|
||||||
/// To work around this, we "lie" when `peer_certificates()` are requested and
|
/// To work around this, we "lie" when `peer_certificates()` are requested and
|
||||||
/// always return `Some(Certificates)`. Internally, `Certificates` is an
|
/// always return `Some(Certificates)`. Internally, `Certificates` is an
|
||||||
/// `Arc<InitCell<Vec<CertificateData>>>`, effectively a shared, thread-safe,
|
/// `Arc<InitCell<Vec<CertificateDer>>>`, effectively a shared, thread-safe,
|
||||||
/// `OnceCell`. The cell is initially empty and is filled as soon as the
|
/// `OnceCell`. The cell is initially empty and is filled as soon as the
|
||||||
/// handshake is complete. If the certificate data were to be requested prior to
|
/// handshake is complete. If the certificate data were to be requested prior to
|
||||||
/// this point, it would be empty. However, in Rocket, we only request
|
/// this point, it would be empty. However, in Rocket, we only request
|
||||||
|
@ -72,49 +73,43 @@ pub struct Config<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TlsListener {
|
impl TlsListener {
|
||||||
pub async fn bind<R>(addr: SocketAddr, mut c: Config<R>) -> io::Result<TlsListener>
|
pub async fn bind<R>(addr: SocketAddr, mut c: Config<R>) -> crate::tls::Result<TlsListener>
|
||||||
where R: io::BufRead
|
where R: io::BufRead
|
||||||
{
|
{
|
||||||
use rustls::server::{AllowAnyAuthenticatedClient, AllowAnyAnonymousOrAuthenticatedClient};
|
let provider = rustls::crypto::CryptoProvider {
|
||||||
use rustls::server::{NoClientAuth, ServerSessionMemoryCache, ServerConfig};
|
cipher_suites: c.ciphersuites,
|
||||||
|
..rustls::crypto::ring::default_provider()
|
||||||
let cert_chain = load_certs(&mut c.cert_chain)
|
|
||||||
.map_err(|e| io::Error::new(e.kind(), format!("bad TLS cert chain: {}", e)))?;
|
|
||||||
|
|
||||||
let key = load_private_key(&mut c.private_key)
|
|
||||||
.map_err(|e| io::Error::new(e.kind(), format!("bad TLS private key: {}", e)))?;
|
|
||||||
|
|
||||||
let client_auth = match c.ca_certs {
|
|
||||||
Some(ref mut ca_certs) => match load_ca_certs(ca_certs) {
|
|
||||||
Ok(ca) if c.mandatory_mtls => AllowAnyAuthenticatedClient::new(ca).boxed(),
|
|
||||||
Ok(ca) => AllowAnyAnonymousOrAuthenticatedClient::new(ca).boxed(),
|
|
||||||
Err(e) => return Err(io::Error::new(e.kind(), format!("bad CA cert(s): {}", e))),
|
|
||||||
},
|
|
||||||
None => NoClientAuth::boxed(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut tls_config = ServerConfig::builder()
|
let verifier = match c.ca_certs {
|
||||||
.with_cipher_suites(&c.ciphersuites)
|
Some(ref mut ca_certs) => {
|
||||||
.with_safe_default_kx_groups()
|
let ca_roots = Arc::new(load_ca_certs(ca_certs)?);
|
||||||
.with_safe_default_protocol_versions()
|
let verifier = WebPkiClientVerifier::builder(ca_roots);
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS config: {}", e)))?
|
match c.mandatory_mtls {
|
||||||
.with_client_cert_verifier(client_auth)
|
true => verifier.build()?,
|
||||||
.with_single_cert(cert_chain, key)
|
false => verifier.allow_unauthenticated().build()?,
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS config: {}", e)))?;
|
}
|
||||||
|
},
|
||||||
|
None => WebPkiClientVerifier::no_client_auth(),
|
||||||
|
};
|
||||||
|
|
||||||
tls_config.ignore_client_order = c.prefer_server_order;
|
let key = load_key(&mut c.private_key)?;
|
||||||
|
let cert_chain = load_cert_chain(&mut c.cert_chain)?;
|
||||||
|
let mut config = ServerConfig::builder_with_provider(Arc::new(provider))
|
||||||
|
.with_safe_default_protocol_versions()?
|
||||||
|
.with_client_cert_verifier(verifier)
|
||||||
|
.with_single_cert(cert_chain, key)?;
|
||||||
|
|
||||||
tls_config.alpn_protocols = vec![b"http/1.1".to_vec()];
|
config.ignore_client_order = c.prefer_server_order;
|
||||||
|
config.session_storage = ServerSessionMemoryCache::new(1024);
|
||||||
|
config.ticketer = rustls::crypto::ring::Ticketer::new()?;
|
||||||
|
config.alpn_protocols = vec![b"http/1.1".to_vec()];
|
||||||
if cfg!(feature = "http2") {
|
if cfg!(feature = "http2") {
|
||||||
tls_config.alpn_protocols.insert(0, b"h2".to_vec());
|
config.alpn_protocols.insert(0, b"h2".to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
tls_config.session_storage = ServerSessionMemoryCache::new(1024);
|
|
||||||
tls_config.ticketer = rustls::Ticketer::new()
|
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS ticketer: {}", e)))?;
|
|
||||||
|
|
||||||
let listener = TcpListener::bind(addr).await?;
|
let listener = TcpListener::bind(addr).await?;
|
||||||
let acceptor = TlsAcceptor::from(Arc::new(tls_config));
|
let acceptor = TlsAcceptor::from(Arc::new(config));
|
||||||
Ok(TlsListener { listener, acceptor })
|
Ok(TlsListener { listener, acceptor })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,8 +174,10 @@ impl TlsStream {
|
||||||
TlsState::Handshaking(ref mut accept) => {
|
TlsState::Handshaking(ref mut accept) => {
|
||||||
match futures::ready!(Pin::new(accept).poll(cx)) {
|
match futures::ready!(Pin::new(accept).poll(cx)) {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
if let Some(cert_chain) = stream.get_ref().1.peer_certificates() {
|
if let Some(peer_certs) = stream.get_ref().1.peer_certificates() {
|
||||||
self.certs.set(cert_chain.to_vec());
|
self.certs.set(peer_certs.into_iter()
|
||||||
|
.map(|v| CertificateDer(v.clone().into_owned()))
|
||||||
|
.collect());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state = TlsState::Streaming(stream);
|
self.state = TlsState::Streaming(stream);
|
||||||
|
|
|
@ -6,3 +6,6 @@ pub mod mtls;
|
||||||
pub use rustls;
|
pub use rustls;
|
||||||
pub use listener::{TlsListener, Config};
|
pub use listener::{TlsListener, Config};
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub use error::Result;
|
||||||
|
|
|
@ -41,7 +41,7 @@ use x509_parser::nom;
|
||||||
use x509::{ParsedExtension, X509Name, X509Certificate, TbsCertificate, X509Error, FromDer};
|
use x509::{ParsedExtension, X509Name, X509Certificate, TbsCertificate, X509Error, FromDer};
|
||||||
use oid::OID_X509_EXT_SUBJECT_ALT_NAME as SUBJECT_ALT_NAME;
|
use oid::OID_X509_EXT_SUBJECT_ALT_NAME as SUBJECT_ALT_NAME;
|
||||||
|
|
||||||
use crate::listener::CertificateData;
|
use crate::listener::CertificateDer;
|
||||||
|
|
||||||
/// A type alias for [`Result`](std::result::Result) with the error type set to
|
/// A type alias for [`Result`](std::result::Result) with the error type set to
|
||||||
/// [`Error`].
|
/// [`Error`].
|
||||||
|
@ -144,7 +144,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Certificate<'a> {
|
pub struct Certificate<'a> {
|
||||||
x509: X509Certificate<'a>,
|
x509: X509Certificate<'a>,
|
||||||
data: &'a CertificateData,
|
data: &'a CertificateDer,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An X.509 Distinguished Name (DN) found in a [`Certificate`].
|
/// An X.509 Distinguished Name (DN) found in a [`Certificate`].
|
||||||
|
@ -224,7 +224,7 @@ impl<'a> Certificate<'a> {
|
||||||
|
|
||||||
/// PRIVATE: For internal Rocket use only!
|
/// PRIVATE: For internal Rocket use only!
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn parse(chain: &[CertificateData]) -> Result<Certificate<'_>> {
|
pub fn parse(chain: &[CertificateDer]) -> Result<Certificate<'_>> {
|
||||||
let data = chain.first().ok_or_else(|| Error::Empty)?;
|
let data = chain.first().ok_or_else(|| Error::Empty)?;
|
||||||
let x509 = Certificate::parse_one(&data.0)?;
|
let x509 = Certificate::parse_one(&data.0)?;
|
||||||
Ok(Certificate { x509, data })
|
Ok(Certificate { x509, data })
|
||||||
|
|
|
@ -1,55 +1,47 @@
|
||||||
use std::io::{self, Cursor, Read};
|
use std::io;
|
||||||
|
|
||||||
use rustls::{Certificate, PrivateKey, RootCertStore};
|
use rustls::RootCertStore;
|
||||||
|
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||||
|
|
||||||
fn err(message: impl Into<std::borrow::Cow<'static, str>>) -> io::Error {
|
use crate::tls::error::{Result, Error, KeyError};
|
||||||
io::Error::new(io::ErrorKind::Other, message.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads certificates from `reader`.
|
/// Loads certificates from `reader`.
|
||||||
pub fn load_certs(reader: &mut dyn io::BufRead) -> io::Result<Vec<Certificate>> {
|
pub fn load_cert_chain(reader: &mut dyn io::BufRead) -> Result<Vec<CertificateDer<'static>>> {
|
||||||
let certs = rustls_pemfile::certs(reader).map_err(|_| err("invalid certificate"))?;
|
rustls_pemfile::certs(reader)
|
||||||
Ok(certs.into_iter().map(Certificate).collect())
|
.collect::<Result<_, _>>()
|
||||||
|
.map_err(Error::CertChain)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load and decode the private key from `reader`.
|
/// Load and decode the private key from `reader`.
|
||||||
pub fn load_private_key(reader: &mut dyn io::BufRead) -> io::Result<PrivateKey> {
|
pub fn load_key(reader: &mut dyn io::BufRead) -> Result<PrivateKeyDer<'static>> {
|
||||||
// "rsa" (PKCS1) PEM files have a different first-line header than PKCS8
|
use rustls_pemfile::Item::*;
|
||||||
// PEM files, use that to determine the parse function to use.
|
|
||||||
let mut header = String::new();
|
|
||||||
let private_keys_fn = loop {
|
|
||||||
header.clear();
|
|
||||||
if reader.read_line(&mut header)? == 0 {
|
|
||||||
return Err(err("failed to find key header; supported formats are: RSA, PKCS8, SEC1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
break match header.trim_end() {
|
let mut keys: Vec<PrivateKeyDer<'static>> = rustls_pemfile::read_all(reader)
|
||||||
"-----BEGIN RSA PRIVATE KEY-----" => rustls_pemfile::rsa_private_keys,
|
.map(|result| result.map_err(KeyError::Io)
|
||||||
"-----BEGIN PRIVATE KEY-----" => rustls_pemfile::pkcs8_private_keys,
|
.and_then(|item| match item {
|
||||||
"-----BEGIN EC PRIVATE KEY-----" => rustls_pemfile::ec_private_keys,
|
Pkcs1Key(key) => Ok(key.into()),
|
||||||
_ => continue,
|
Pkcs8Key(key) => Ok(key.into()),
|
||||||
};
|
Sec1Key(key) => Ok(key.into()),
|
||||||
};
|
_ => Err(KeyError::BadItem(item))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
let key = private_keys_fn(&mut Cursor::new(header).chain(reader))
|
if keys.len() != 1 {
|
||||||
.map_err(|_| err("invalid key file"))
|
return Err(KeyError::BadKeyCount(keys.len()).into());
|
||||||
.and_then(|mut keys| match keys.len() {
|
}
|
||||||
0 => Err(err("no valid keys found; is the file malformed?")),
|
|
||||||
1 => Ok(PrivateKey(keys.remove(0))),
|
|
||||||
n => Err(err(format!("expected 1 key, found {}", n))),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Ensure we can use the key.
|
// Ensure we can use the key.
|
||||||
rustls::sign::any_supported_type(&key)
|
let key = keys.remove(0);
|
||||||
.map_err(|_| err("key parsed but is unusable"))
|
rustls::crypto::ring::sign::any_supported_type(&key).map_err(KeyError::Unsupported)?;
|
||||||
.map(|_| key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load and decode CA certificates from `reader`.
|
/// Load and decode CA certificates from `reader`.
|
||||||
pub fn load_ca_certs(reader: &mut dyn io::BufRead) -> io::Result<RootCertStore> {
|
pub fn load_ca_certs(reader: &mut dyn io::BufRead) -> Result<RootCertStore> {
|
||||||
let mut roots = rustls::RootCertStore::empty();
|
let mut roots = rustls::RootCertStore::empty();
|
||||||
for cert in load_certs(reader)? {
|
for cert in load_cert_chain(reader)? {
|
||||||
roots.add(&cert).map_err(|e| err(format!("CA cert error: {}", e)))?;
|
roots.add(cert).map_err(Error::CertAuth)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(roots)
|
Ok(roots)
|
||||||
|
@ -66,31 +58,31 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_load_private_keys_of_different_types() -> io::Result<()> {
|
fn verify_load_private_keys_of_different_types() -> Result<()> {
|
||||||
let rsa_sha256_key = tls_example_key!("rsa_sha256_key.pem");
|
let rsa_sha256_key = tls_example_key!("rsa_sha256_key.pem");
|
||||||
let ecdsa_nistp256_sha256_key = tls_example_key!("ecdsa_nistp256_sha256_key_pkcs8.pem");
|
let ecdsa_nistp256_sha256_key = tls_example_key!("ecdsa_nistp256_sha256_key_pkcs8.pem");
|
||||||
let ecdsa_nistp384_sha384_key = tls_example_key!("ecdsa_nistp384_sha384_key_pkcs8.pem");
|
let ecdsa_nistp384_sha384_key = tls_example_key!("ecdsa_nistp384_sha384_key_pkcs8.pem");
|
||||||
let ed2551_key = tls_example_key!("ed25519_key.pem");
|
let ed2551_key = tls_example_key!("ed25519_key.pem");
|
||||||
|
|
||||||
load_private_key(&mut Cursor::new(rsa_sha256_key))?;
|
load_key(&mut &rsa_sha256_key[..])?;
|
||||||
load_private_key(&mut Cursor::new(ecdsa_nistp256_sha256_key))?;
|
load_key(&mut &ecdsa_nistp256_sha256_key[..])?;
|
||||||
load_private_key(&mut Cursor::new(ecdsa_nistp384_sha384_key))?;
|
load_key(&mut &ecdsa_nistp384_sha384_key[..])?;
|
||||||
load_private_key(&mut Cursor::new(ed2551_key))?;
|
load_key(&mut &ed2551_key[..])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_load_certs_of_different_types() -> io::Result<()> {
|
fn verify_load_certs_of_different_types() -> Result<()> {
|
||||||
let rsa_sha256_cert = tls_example_key!("rsa_sha256_cert.pem");
|
let rsa_sha256_cert = tls_example_key!("rsa_sha256_cert.pem");
|
||||||
let ecdsa_nistp256_sha256_cert = tls_example_key!("ecdsa_nistp256_sha256_cert.pem");
|
let ecdsa_nistp256_sha256_cert = tls_example_key!("ecdsa_nistp256_sha256_cert.pem");
|
||||||
let ecdsa_nistp384_sha384_cert = tls_example_key!("ecdsa_nistp384_sha384_cert.pem");
|
let ecdsa_nistp384_sha384_cert = tls_example_key!("ecdsa_nistp384_sha384_cert.pem");
|
||||||
let ed2551_cert = tls_example_key!("ed25519_cert.pem");
|
let ed2551_cert = tls_example_key!("ed25519_cert.pem");
|
||||||
|
|
||||||
load_certs(&mut Cursor::new(rsa_sha256_cert))?;
|
load_cert_chain(&mut &rsa_sha256_cert[..])?;
|
||||||
load_certs(&mut Cursor::new(ecdsa_nistp256_sha256_cert))?;
|
load_cert_chain(&mut &ecdsa_nistp256_sha256_cert[..])?;
|
||||||
load_certs(&mut Cursor::new(ecdsa_nistp384_sha384_cert))?;
|
load_cert_chain(&mut &ecdsa_nistp384_sha384_cert[..])?;
|
||||||
load_certs(&mut Cursor::new(ed2551_cert))?;
|
load_cert_chain(&mut &ed2551_cert[..])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -631,7 +631,7 @@ mod with_tls_feature {
|
||||||
|
|
||||||
use crate::http::tls::Config;
|
use crate::http::tls::Config;
|
||||||
use crate::http::tls::rustls::SupportedCipherSuite as RustlsCipher;
|
use crate::http::tls::rustls::SupportedCipherSuite as RustlsCipher;
|
||||||
use crate::http::tls::rustls::cipher_suite;
|
use crate::http::tls::rustls::crypto::ring::cipher_suite;
|
||||||
|
|
||||||
use yansi::Paint;
|
use yansi::Paint;
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,9 @@ pub struct Error {
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
/// Binding to the provided address/port failed.
|
/// Binding to the provided address/port failed.
|
||||||
Bind(io::Error),
|
Bind(io::Error),
|
||||||
|
/// Binding via TLS to the provided address/port failed.
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
TlsBind(crate::http::tls::error::Error),
|
||||||
/// An I/O error occurred during launch.
|
/// An I/O error occurred during launch.
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
/// A valid [`Config`](crate::Config) could not be extracted from the
|
/// A valid [`Config`](crate::Config) could not be extracted from the
|
||||||
|
@ -234,6 +237,12 @@ impl Error {
|
||||||
|
|
||||||
"aborting due to failed shutdown"
|
"aborting due to failed shutdown"
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
ErrorKind::TlsBind(e) => {
|
||||||
|
error!("Rocket failed to bind via TLS to network socket.");
|
||||||
|
info_!("{}", e);
|
||||||
|
"aborting due to TLS bind error"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,15 +253,17 @@ impl fmt::Display for ErrorKind {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ErrorKind::Bind(e) => write!(f, "binding failed: {}", e),
|
ErrorKind::Bind(e) => write!(f, "binding failed: {e}"),
|
||||||
ErrorKind::Io(e) => write!(f, "I/O error: {}", e),
|
ErrorKind::Io(e) => write!(f, "I/O error: {e}"),
|
||||||
ErrorKind::Collisions(_) => "collisions detected".fmt(f),
|
ErrorKind::Collisions(_) => "collisions detected".fmt(f),
|
||||||
ErrorKind::FailedFairings(_) => "launch fairing(s) failed".fmt(f),
|
ErrorKind::FailedFairings(_) => "launch fairing(s) failed".fmt(f),
|
||||||
ErrorKind::InsecureSecretKey(_) => "insecure secret key config".fmt(f),
|
ErrorKind::InsecureSecretKey(_) => "insecure secret key config".fmt(f),
|
||||||
ErrorKind::Config(_) => "failed to extract configuration".fmt(f),
|
ErrorKind::Config(_) => "failed to extract configuration".fmt(f),
|
||||||
ErrorKind::SentinelAborts(_) => "sentinel(s) aborted".fmt(f),
|
ErrorKind::SentinelAborts(_) => "sentinel(s) aborted".fmt(f),
|
||||||
ErrorKind::Shutdown(_, Some(e)) => write!(f, "shutdown failed: {}", e),
|
ErrorKind::Shutdown(_, Some(e)) => write!(f, "shutdown failed: {e}"),
|
||||||
ErrorKind::Shutdown(_, None) => "shutdown failed".fmt(f),
|
ErrorKind::Shutdown(_, None) => "shutdown failed".fmt(f),
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
ErrorKind::TlsBind(e) => write!(f, "TLS bind failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,10 +228,10 @@ macro_rules! pub_request_impl {
|
||||||
#[cfg(feature = "mtls")]
|
#[cfg(feature = "mtls")]
|
||||||
#[cfg_attr(nightly, doc(cfg(feature = "mtls")))]
|
#[cfg_attr(nightly, doc(cfg(feature = "mtls")))]
|
||||||
pub fn identity<C: std::io::Read>(mut self, reader: C) -> Self {
|
pub fn identity<C: std::io::Read>(mut self, reader: C) -> Self {
|
||||||
use crate::http::{tls::util::load_certs, private::Certificates};
|
use crate::http::{tls::util::load_cert_chain, private::Certificates};
|
||||||
|
|
||||||
let mut reader = std::io::BufReader::new(reader);
|
let mut reader = std::io::BufReader::new(reader);
|
||||||
let certs = load_certs(&mut reader).map(Certificates::from);
|
let certs = load_cert_chain(&mut reader).map(Certificates::from);
|
||||||
self._request_mut().connection.client_certificates = certs.ok();
|
self._request_mut().connection.client_certificates = certs.ok();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -427,7 +427,7 @@ impl Rocket<Orbit> {
|
||||||
use crate::http::tls::TlsListener;
|
use crate::http::tls::TlsListener;
|
||||||
|
|
||||||
let conf = config.to_native_config().map_err(ErrorKind::Io)?;
|
let conf = config.to_native_config().map_err(ErrorKind::Io)?;
|
||||||
let l = TlsListener::bind(addr, conf).await.map_err(ErrorKind::Bind)?;
|
let l = TlsListener::bind(addr, conf).await.map_err(ErrorKind::TlsBind)?;
|
||||||
addr = l.local_addr().unwrap_or(addr);
|
addr = l.local_addr().unwrap_or(addr);
|
||||||
self.config.address = addr.ip();
|
self.config.address = addr.ip();
|
||||||
self.config.port = addr.port();
|
self.config.port = addr.port();
|
||||||
|
|
|
@ -7,3 +7,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib", features = ["tls", "mtls", "secrets"] }
|
rocket = { path = "../../core/lib", features = ["tls", "mtls", "secrets"] }
|
||||||
|
yansi = "1.0.0-rc.1"
|
||||||
|
|
|
@ -22,5 +22,5 @@ fn rocket() -> _ {
|
||||||
// Run `./private/gen_certs.sh` to generate a CA and key pairs.
|
// Run `./private/gen_certs.sh` to generate a CA and key pairs.
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", routes![hello, mutual])
|
.mount("/", routes![hello, mutual])
|
||||||
.attach(redirector::Redirector { port: 3000 })
|
.attach(redirector::Redirector::on(3000))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,35 @@
|
||||||
//! Redirect all HTTP requests to HTTPs.
|
//! Redirect all HTTP requests to HTTPs.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::log::LogLevel;
|
use rocket::log::LogLevel;
|
||||||
use rocket::{route, Error, Request, Data, Route, Orbit, Rocket, Ignite, Config};
|
use rocket::{route, Error, Request, Data, Route, Orbit, Rocket, Ignite, Config};
|
||||||
use rocket::fairing::{Fairing, Info, Kind};
|
use rocket::fairing::{Fairing, Info, Kind};
|
||||||
use rocket::response::Redirect;
|
use rocket::response::Redirect;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Redirector {
|
pub struct Redirector {
|
||||||
pub port: u16
|
pub listen_port: u16,
|
||||||
|
pub tls_port: OnceLock<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Redirector {
|
impl Redirector {
|
||||||
// Route function that gets call on every single request.
|
pub fn on(port: u16) -> Self {
|
||||||
|
Redirector { listen_port: port, tls_port: OnceLock::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route function that gets called on every single request.
|
||||||
fn redirect<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> {
|
fn redirect<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> {
|
||||||
// FIXME: Check the host against a whitelist!
|
// FIXME: Check the host against a whitelist!
|
||||||
|
let redirector = req.rocket().state::<Self>().expect("managed Self");
|
||||||
if let Some(host) = req.host() {
|
if let Some(host) = req.host() {
|
||||||
let https_uri = format!("https://{}{}", host, req.uri());
|
let domain = host.domain();
|
||||||
|
let https_uri = match redirector.tls_port.get() {
|
||||||
|
Some(443) | None => format!("https://{domain}{}", req.uri()),
|
||||||
|
Some(port) => format!("https://{domain}:{port}{}", req.uri()),
|
||||||
|
};
|
||||||
|
|
||||||
route::Outcome::from(req, Redirect::permanent(https_uri)).pin()
|
route::Outcome::from(req, Redirect::permanent(https_uri)).pin()
|
||||||
} else {
|
} else {
|
||||||
route::Outcome::from(req, Status::BadRequest).pin()
|
route::Outcome::from(req, Status::BadRequest).pin()
|
||||||
|
@ -25,13 +38,21 @@ impl Redirector {
|
||||||
|
|
||||||
// Launch an instance of Rocket than handles redirection on `self.port`.
|
// Launch an instance of Rocket than handles redirection on `self.port`.
|
||||||
pub async fn try_launch(self, mut config: Config) -> Result<Rocket<Ignite>, Error> {
|
pub async fn try_launch(self, mut config: Config) -> Result<Rocket<Ignite>, Error> {
|
||||||
|
use yansi::Paint;
|
||||||
use rocket::http::Method::*;
|
use rocket::http::Method::*;
|
||||||
|
|
||||||
|
// Determine the port TLS is being served on.
|
||||||
|
let tls_port = self.tls_port.get_or_init(|| config.port);
|
||||||
|
|
||||||
// Adjust config for redirector: disable TLS, set port, disable logging.
|
// Adjust config for redirector: disable TLS, set port, disable logging.
|
||||||
config.tls = None;
|
config.tls = None;
|
||||||
config.port = self.port;
|
config.port = self.listen_port;
|
||||||
config.log_level = LogLevel::Critical;
|
config.log_level = LogLevel::Critical;
|
||||||
|
|
||||||
|
info!("{}{}", "🔒 ".mask(), "HTTP -> HTTPS Redirector:".magenta());
|
||||||
|
info_!("redirecting on insecure port {} to TLS port {}",
|
||||||
|
self.listen_port.yellow(), tls_port.green());
|
||||||
|
|
||||||
// Build a vector of routes to `redirect` on `<path..>` for each method.
|
// Build a vector of routes to `redirect` on `<path..>` for each method.
|
||||||
let redirects = [Get, Put, Post, Delete, Options, Head, Trace, Connect, Patch]
|
let redirects = [Get, Put, Post, Delete, Options, Head, Trace, Connect, Patch]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -39,6 +60,7 @@ impl Redirector {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
rocket::custom(config)
|
rocket::custom(config)
|
||||||
|
.manage(self)
|
||||||
.mount("/", redirects)
|
.mount("/", redirects)
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
|
@ -48,11 +70,14 @@ impl Redirector {
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl Fairing for Redirector {
|
impl Fairing for Redirector {
|
||||||
fn info(&self) -> Info {
|
fn info(&self) -> Info {
|
||||||
Info { name: "HTTP -> HTTPS Redirector", kind: Kind::Liftoff }
|
Info {
|
||||||
|
name: "HTTP -> HTTPS Redirector",
|
||||||
|
kind: Kind::Liftoff | Kind::Singleton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_liftoff(&self, rkt: &Rocket<Orbit>) {
|
async fn on_liftoff(&self, rkt: &Rocket<Orbit>) {
|
||||||
let (this, shutdown, config) = (*self, rkt.shutdown(), rkt.config().clone());
|
let (this, shutdown, config) = (self.clone(), rkt.shutdown(), rkt.config().clone());
|
||||||
let _ = rocket::tokio::spawn(async move {
|
let _ = rocket::tokio::spawn(async move {
|
||||||
if let Err(e) = this.try_launch(config).await {
|
if let Err(e) = this.try_launch(config).await {
|
||||||
error!("Failed to start HTTP -> HTTPS redirector.");
|
error!("Failed to start HTTP -> HTTPS redirector.");
|
||||||
|
|
Loading…
Reference in New Issue