#[cfg(feature = "tls")]
use std::sync::{Arc, RwLock};
#[cfg(feature = "tls")]
use tracing::{debug, info, warn};
#[cfg(feature = "tls")]
use rustls::server::WebPkiClientVerifier;
#[cfg(feature = "tls")]
use rustls::{RootCertStore, ServerConfig};
#[cfg(feature = "tls")]
use rustls_pki_types::pem::PemObject;
#[cfg(feature = "tls")]
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
#[cfg(feature = "tls")]
const ALPN_PROTOCOLS: [&[u8]; 2] = [b"h2", b"http/1.1"];
#[derive(Debug, thiserror::Error)]
pub enum TlsError {
#[error(
"TLS is half-configured: {supplied} was set but {missing} was not; \
set both to enable TLS, or neither to serve plaintext"
)]
IncompleteKeyPair {
supplied: &'static str,
missing: &'static str,
},
#[error(
"a TLS client CA was configured but no server certificate/key was; \
mutual TLS requires a TLS listener"
)]
ClientCaWithoutTls,
#[error("failed to read TLS certificate from {path}: {reason}")]
CertRead {
path: String,
reason: String,
},
#[error("failed to read TLS private key from {path}: {reason}")]
KeyRead {
path: String,
reason: String,
},
#[error("TLS private key from {path} was rejected: {message}")]
KeyParse {
path: String,
message: String,
},
#[error("failed to read TLS client CA bundle from {path}: {reason}")]
ClientCaRead {
path: String,
reason: String,
},
#[error("TLS client CA bundle from {path} yielded no usable certificates: {message}")]
ClientCaInvalid {
path: String,
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TlsSettings {
pub cert_path: String,
pub key_path: String,
pub client_ca_path: Option<String>,
}
impl TlsSettings {
#[must_use]
pub fn requires_client_auth(&self) -> bool {
self.client_ca_path.is_some()
}
}
fn non_empty(value: Option<String>) -> Option<String> {
value.filter(|v| !v.trim().is_empty())
}
pub fn resolve_tls_settings(
cert_path: Option<String>,
key_path: Option<String>,
client_ca_path: Option<String>,
) -> Result<Option<TlsSettings>, TlsError> {
let cert_path = non_empty(cert_path);
let key_path = non_empty(key_path);
let client_ca_path = non_empty(client_ca_path);
let (cert_path, key_path) = match (cert_path, key_path) {
(Some(cert), Some(key)) => (cert, key),
(Some(_), None) => {
return Err(TlsError::IncompleteKeyPair {
supplied: "--tls-cert / BIND_TLS_CERT",
missing: "--tls-key / BIND_TLS_KEY",
})
}
(None, Some(_)) => {
return Err(TlsError::IncompleteKeyPair {
supplied: "--tls-key / BIND_TLS_KEY",
missing: "--tls-cert / BIND_TLS_CERT",
})
}
(None, None) if client_ca_path.is_some() => return Err(TlsError::ClientCaWithoutTls),
(None, None) => return Ok(None),
};
Ok(Some(TlsSettings {
cert_path,
key_path,
client_ca_path,
}))
}
#[must_use]
pub fn scheme_for(settings: Option<&TlsSettings>) -> &'static str {
if settings.is_some() {
return "https";
}
"http"
}
#[cfg(feature = "tls")]
pub(crate) fn build_client_verifier(
path: &str,
) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, TlsError> {
let mut roots = RootCertStore::empty();
for cert in CertificateDer::pem_file_iter(path).map_err(|e| TlsError::ClientCaRead {
path: path.to_string(),
reason: e.to_string(),
})? {
let cert = cert.map_err(|e| TlsError::ClientCaRead {
path: path.to_string(),
reason: e.to_string(),
})?;
roots.add(cert).map_err(|e| TlsError::ClientCaInvalid {
path: path.to_string(),
message: e.to_string(),
})?;
}
if roots.is_empty() {
return Err(TlsError::ClientCaInvalid {
path: path.to_string(),
message: "bundle contained no certificates".to_string(),
});
}
WebPkiClientVerifier::builder(Arc::new(roots))
.build()
.map_err(|e| TlsError::ClientCaInvalid {
path: path.to_string(),
message: e.to_string(),
})
}
#[cfg(feature = "tls")]
pub fn build_server_config(settings: &TlsSettings) -> Result<ServerConfig, TlsError> {
let certs = CertificateDer::pem_file_iter(&settings.cert_path)
.map_err(|e| TlsError::CertRead {
path: settings.cert_path.clone(),
reason: e.to_string(),
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsError::CertRead {
path: settings.cert_path.clone(),
reason: e.to_string(),
})?;
let key = PrivateKeyDer::from_pem_file(&settings.key_path).map_err(|e| TlsError::KeyRead {
path: settings.key_path.clone(),
reason: e.to_string(),
})?;
let builder = ServerConfig::builder();
let mut config = match &settings.client_ca_path {
Some(ca_path) => builder
.with_client_cert_verifier(build_client_verifier(ca_path)?)
.with_single_cert(certs, key),
None => builder.with_no_client_auth().with_single_cert(certs, key),
}
.map_err(|e| TlsError::KeyParse {
path: settings.key_path.clone(),
message: e.to_string(),
})?;
config.alpn_protocols = ALPN_PROTOCOLS.iter().map(|p| p.to_vec()).collect();
Ok(config)
}
pub const DEFAULT_RELOAD_INTERVAL_SECS: u64 = 60;
#[cfg(feature = "tls")]
pub fn fingerprint(settings: &TlsSettings) -> Result<[u8; 32], TlsError> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
let cert = std::fs::read(&settings.cert_path).map_err(|e| TlsError::CertRead {
path: settings.cert_path.clone(),
reason: e.to_string(),
})?;
hasher.update(&cert);
let key = std::fs::read(&settings.key_path).map_err(|e| TlsError::KeyRead {
path: settings.key_path.clone(),
reason: e.to_string(),
})?;
hasher.update(&key);
if let Some(ca_path) = &settings.client_ca_path {
let ca = std::fs::read(ca_path).map_err(|e| TlsError::ClientCaRead {
path: ca_path.clone(),
reason: e.to_string(),
})?;
hasher.update(&ca);
}
Ok(hasher.finalize().into())
}
#[cfg(feature = "tls")]
#[derive(Debug)]
pub struct TlsReloader {
settings: TlsSettings,
config: RwLock<Arc<ServerConfig>>,
last_fingerprint: RwLock<[u8; 32]>,
}
#[cfg(feature = "tls")]
impl TlsReloader {
pub fn new(settings: TlsSettings) -> Result<Self, TlsError> {
let config = Arc::new(build_server_config(&settings)?);
let fp = fingerprint(&settings).unwrap_or([0u8; 32]);
Ok(Self {
settings,
config: RwLock::new(config),
last_fingerprint: RwLock::new(fp),
})
}
#[must_use]
pub fn current(&self) -> Arc<ServerConfig> {
self.config
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
#[must_use]
pub fn reloading_enabled(interval_secs: u64) -> bool {
interval_secs > 0
}
pub fn reload_if_changed(&self) -> bool {
let current_fp = match fingerprint(&self.settings) {
Ok(fp) => fp,
Err(e) => {
debug!("TLS material not readable while polling for changes: {}", e);
return false;
}
};
{
let last = self
.last_fingerprint
.read()
.unwrap_or_else(|p| p.into_inner());
if *last == current_fp {
return false;
}
}
let rebuilt = match build_server_config(&self.settings) {
Ok(config) => config,
Err(e) => {
warn!(
"TLS material changed but the new configuration is not usable, \
continuing with the previous certificate: {}",
e
);
return false;
}
};
*self.config.write().unwrap_or_else(|p| p.into_inner()) = Arc::new(rebuilt);
*self
.last_fingerprint
.write()
.unwrap_or_else(|p| p.into_inner()) = current_fp;
info!("reloaded TLS certificate material; new connections will use it");
true
}
}