nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! SNI / multi-certificate resolution from a KDL file.

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;

/// Hostname → certificate resolver loaded from **`NAUTALID_TLS_SNI_PATH`**.
#[derive(Debug)]
pub struct SniResolver {
    default: Arc<CertifiedKey>,
    hosts: HashMap<String, Arc<CertifiedKey>>,
}

impl SniResolver {
    /// Load SNI map from a KDL file.
    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)
    }

    /// Parse SNI map 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))
    }
}

/// SNI host map with per-transport default certificate (split H2/H3 certs).
#[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,
    })
}

/// Whether **`NAUTALID_TLS_SNI_PATH`** points at a readable SNI map.
#[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())
}

/// Load SNI resolver from env when configured.
#[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))
}

/// Static single-host resolver (env PEM or split H2/H3 paths).
#[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)?)))
    }
}

/// Per-transport default certificate (split H2/H3 PEM env).
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");
    }
}