use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
use tokio_rustls::rustls::ServerConfig;
use tokio_rustls::TlsAcceptor;
use waf_core::TlsConfig;
pub struct TlsMaterial {
pub cert_chain: Vec<CertificateDer<'static>>,
pub private_key: PrivateKeyDer<'static>,
}
pub trait TlsCertSource: Send + Sync {
fn load(&self) -> Result<TlsMaterial, TlsError>;
}
#[derive(Debug)]
pub enum TlsError {
Io { path: PathBuf, source: std::io::Error },
NoCertificates(PathBuf),
NoPrivateKey(PathBuf),
Rustls(tokio_rustls::rustls::Error),
}
impl std::fmt::Display for TlsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } =>
write!(f, "cannot read TLS file {}: {source}", path.display()),
Self::NoCertificates(p) =>
write!(f, "no PEM certificates found in {}", p.display()),
Self::NoPrivateKey(p) =>
write!(f, "no PEM private key found in {}", p.display()),
Self::Rustls(e) => write!(f, "TLS configuration error: {e}"),
}
}
}
impl std::error::Error for TlsError {}
pub struct FileCertSource {
cert_path: PathBuf,
key_path: PathBuf,
}
impl FileCertSource {
pub fn new(cert_path: impl Into<PathBuf>, key_path: impl Into<PathBuf>) -> Self {
Self { cert_path: cert_path.into(), key_path: key_path.into() }
}
}
impl TlsCertSource for FileCertSource {
fn load(&self) -> Result<TlsMaterial, TlsError> {
Ok(TlsMaterial {
cert_chain: load_certs(&self.cert_path)?,
private_key: load_key(&self.key_path)?,
})
}
}
fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsError> {
let data = std::fs::read(path).map_err(|source| TlsError::Io { path: path.to_path_buf(), source })?;
let mut reader: &[u8] = &data;
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
.collect::<Result<_, _>>()
.map_err(|source| TlsError::Io { path: path.to_path_buf(), source })?;
if certs.is_empty() {
return Err(TlsError::NoCertificates(path.to_path_buf()));
}
Ok(certs)
}
fn load_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsError> {
let data = std::fs::read(path).map_err(|source| TlsError::Io { path: path.to_path_buf(), source })?;
let mut reader: &[u8] = &data;
rustls_pemfile::private_key(&mut reader)
.map_err(|source| TlsError::Io { path: path.to_path_buf(), source })?
.ok_or_else(|| TlsError::NoPrivateKey(path.to_path_buf()))
}
pub fn build_server_config(source: &dyn TlsCertSource, alpn: &[String]) -> Result<ServerConfig, TlsError> {
let material = source.load()?;
let provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider());
let mut cfg = ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(TlsError::Rustls)?
.with_no_client_auth()
.with_single_cert(material.cert_chain, material.private_key)
.map_err(TlsError::Rustls)?;
cfg.alpn_protocols = alpn.iter().map(|p| p.as_bytes().to_vec()).collect();
Ok(cfg)
}
pub fn acceptor_from_source(
tls: &TlsConfig,
source: Option<Arc<dyn TlsCertSource>>,
) -> Result<Option<TlsAcceptor>, TlsError> {
if !tls.enabled {
return Ok(None);
}
let cfg = match source {
Some(s) => build_server_config(s.as_ref(), &tls.alpn)?,
None => build_server_config(&FileCertSource::new(&tls.cert_path, &tls.key_path), &tls.alpn)?,
};
Ok(Some(TlsAcceptor::from(Arc::new(cfg))))
}
pub fn acceptor_from_config(tls: &TlsConfig) -> Result<Option<TlsAcceptor>, TlsError> {
acceptor_from_source(tls, None)
}