use std::collections::HashMap;
use std::sync::Arc;
use kdl::{KdlDocument, KdlNode};
use rustls::server::{ClientHello, ResolvesServerCert};
use rustls::sign::CertifiedKey;
use super::material::{certified_key_from_pem, crypto_provider, pem_from_env_pair};
use super::ocsp;
use super::TlsConfigError;
#[derive(Debug)]
pub struct SniResolver {
default: Arc<CertifiedKey>,
hosts: HashMap<String, Arc<CertifiedKey>>,
}
impl SniResolver {
pub fn from_kdl_path(path: &str) -> Result<Arc<Self>, TlsConfigError> {
let text = std::fs::read_to_string(path).map_err(|source| TlsConfigError::Io {
path: path.to_owned(),
source,
})?;
Self::from_kdl(&text)
}
pub fn from_kdl(text: &str) -> Result<Arc<Self>, TlsConfigError> {
let doc: KdlDocument = text
.parse::<KdlDocument>()
.map_err(|e: kdl::KdlError| TlsConfigError::BadCert(e.to_string()))?;
let sni = doc
.get("sni")
.ok_or_else(|| TlsConfigError::BadCert("KDL missing `sni` node".into()))?;
let mut default = None;
let mut hosts = HashMap::new();
if let Some(children) = sni.children() {
for child in children.nodes() {
match child.name().value() {
"default" => default = Some(load_entry(child, "default")?),
"host" => {
let hostname = child
.entries()
.first()
.and_then(|e| e.value().as_string())
.ok_or_else(|| {
TlsConfigError::BadCert("sni host missing hostname".into())
})?
.to_owned();
let key = load_entry(child, &hostname)?;
hosts.insert(hostname, key);
}
other => {
return Err(TlsConfigError::BadCert(format!(
"unknown sni child node `{other}`"
)));
}
}
}
}
if default.is_none() && attr_string(sni, "cert").is_some() {
default = Some(load_entry(sni, "default")?);
}
let default = default
.ok_or_else(|| TlsConfigError::BadCert("sni missing default certificate".into()))?;
Ok(Arc::new(Self { default, hosts }))
}
fn host_cert(&self, hostname: &str) -> Option<Arc<CertifiedKey>> {
self.hosts.get(hostname).cloned()
}
}
impl ResolvesServerCert for SniResolver {
fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
if let Some(name) = client_hello.server_name() {
if let Some(key) = self.hosts.get(name) {
return Some(Arc::clone(key));
}
}
Some(Arc::clone(&self.default))
}
}
#[derive(Debug)]
pub struct CompositeResolver {
sni: Arc<SniResolver>,
transport_default: Arc<CertifiedKey>,
}
impl CompositeResolver {
pub fn into_resolver(
sni: Arc<SniResolver>,
transport_default: Arc<CertifiedKey>,
) -> Arc<dyn ResolvesServerCert> {
Arc::new(Self {
sni,
transport_default,
})
}
}
impl ResolvesServerCert for CompositeResolver {
fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
if let Some(name) = client_hello.server_name() {
if let Some(key) = self.sni.host_cert(name) {
return Some(key);
}
}
Some(Arc::clone(&self.transport_default))
}
}
fn load_entry(node: &KdlNode, label: &str) -> Result<Arc<CertifiedKey>, TlsConfigError> {
let cert_path = attr_string(node, "cert")
.ok_or_else(|| TlsConfigError::BadCert(format!("sni {label}: missing cert")))?;
let key_path = attr_string(node, "key")
.ok_or_else(|| TlsConfigError::BadCert(format!("sni {label}: missing key")))?;
let cert_pem = read_pem_path(&cert_path)?;
let key_pem = read_pem_path(&key_path)?;
let ocsp = node
.get("ocsp_response")
.and_then(|v| v.as_string())
.and_then(|p| std::fs::read(p).ok())
.or_else(|| ocsp::resolve_staple(&cert_pem, "NAUTALID_TLS"));
let provider = crypto_provider()?;
let key = certified_key_from_pem(&cert_pem, &key_pem, provider)?;
Ok(Arc::new(ocsp::apply_staple(key, ocsp)))
}
fn attr_string(node: &KdlNode, name: &str) -> Option<String> {
node.get(name)
.and_then(|v| v.as_string())
.map(str::to_owned)
}
fn read_pem_path(path: &str) -> Result<String, TlsConfigError> {
std::fs::read_to_string(path).map_err(|source| TlsConfigError::Io {
path: path.to_owned(),
source,
})
}
#[must_use]
pub fn sni_configured_from_env() -> bool {
std::env::var("NAUTALID_TLS_SNI_PATH")
.ok()
.is_some_and(|p| !p.trim().is_empty())
}
#[cfg(test)]
pub fn resolver_from_env() -> Result<Option<Arc<dyn ResolvesServerCert>>, TlsConfigError> {
let Some(path) = std::env::var("NAUTALID_TLS_SNI_PATH")
.ok()
.filter(|p| !p.trim().is_empty())
else {
return Ok(None);
};
let resolver: Arc<dyn ResolvesServerCert> = SniResolver::from_kdl_path(&path)?;
Ok(Some(resolver))
}
#[derive(Debug)]
pub struct StaticResolver(Arc<CertifiedKey>);
impl StaticResolver {
pub fn from_env(prefix: &str) -> Result<Arc<dyn ResolvesServerCert>, TlsConfigError> {
Self::from_env_inner(prefix)
}
pub fn from_pem(
cert_pem: &str,
key_pem: &str,
) -> Result<Arc<dyn ResolvesServerCert>, TlsConfigError> {
let provider = crypto_provider()?;
let staple = ocsp::resolve_staple(cert_pem, "NAUTALID_TLS");
let key = certified_key_from_pem(cert_pem, key_pem, provider)?;
Ok(Arc::new(Self(Arc::new(ocsp::apply_staple(key, staple)))))
}
fn from_env_inner(prefix: &str) -> Result<Arc<dyn ResolvesServerCert>, TlsConfigError> {
Ok(Arc::new(Self(transport_cert_from_env(prefix)?)))
}
}
pub fn transport_cert_from_env(prefix: &str) -> Result<Arc<CertifiedKey>, TlsConfigError> {
let (cert_pem, key_pem) = pem_from_env_pair(prefix)?;
let provider = crypto_provider()?;
let staple = ocsp::resolve_staple(&cert_pem, prefix);
let key = certified_key_from_pem(&cert_pem, &key_pem, provider)?;
Ok(Arc::new(ocsp::apply_staple(key, staple)))
}
impl ResolvesServerCert for StaticResolver {
fn resolve(&self, _client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
Some(Arc::clone(&self.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn install_crypto() {
let _ = crate::platform::install_defaults();
}
fn generated_pem_pair() -> (String, String) {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
(cert.cert.pem(), cert.signing_key.serialize_pem())
}
#[test]
fn parses_sni_kdl() {
install_crypto();
let dir = tempfile::tempdir().unwrap();
let cert_path = dir.path().join("cert.pem");
let key_path = dir.path().join("key.pem");
let (cert, key) = generated_pem_pair();
std::fs::write(&cert_path, &cert).unwrap();
std::fs::write(&key_path, &key).unwrap();
let kdl = format!(
r#"sni {{
default cert="{}" key="{}"
host "example.com" cert="{}" key="{}"
}}"#,
cert_path.display(),
key_path.display(),
cert_path.display(),
key_path.display()
);
SniResolver::from_kdl(&kdl).expect("parse sni kdl");
}
}