nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Certificate material loading shared by static, SNI, and ACME resolvers.

use std::io::Cursor;
use std::sync::Arc;

use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::sign::CertifiedKey;

use super::TlsConfigError;

pub(crate) fn crypto_provider() -> Result<Arc<rustls::crypto::CryptoProvider>, TlsConfigError> {
    rustls::crypto::CryptoProvider::get_default()
        .ok_or(TlsConfigError::NoCryptoProvider)
        .map(Arc::clone)
}

/// Read cert/key PEM from `{prefix}_PEM` / `{prefix}_PATH` env pairs.
pub fn pem_from_env_pair(prefix: &str) -> Result<(String, String), TlsConfigError> {
    let cert = pem_from_env(
        &format!("{prefix}_CERT_PEM"),
        &format!("{prefix}_CERT_PATH"),
    )?;
    let key = pem_from_env(
        &format!("{prefix}_KEY_PEM"),
        &format!("{prefix}_KEY_PATH"),
    )?;
    Ok((cert, key))
}

pub fn pem_from_env(inline: &str, path_var: &str) -> Result<String, TlsConfigError> {
    if let Ok(pem) = std::env::var(inline) {
        if !pem.is_empty() {
            return Ok(pem);
        }
    }
    if let Ok(path) = std::env::var(path_var) {
        if !path.is_empty() {
            return std::fs::read_to_string(&path).map_err(|source| TlsConfigError::Io {
                path: path.clone(),
                source,
            });
        }
    }
    if inline.contains("CLIENT_CA") {
        Err(TlsConfigError::MissingClientCa)
    } else if inline.contains("CERT") || inline.ends_with("_PEM") && inline.contains("CERT") {
        Err(TlsConfigError::MissingCert)
    } else if inline.contains("KEY") {
        Err(TlsConfigError::MissingKey)
    } else if path_var.contains("CERT") {
        Err(TlsConfigError::MissingCert)
    } else {
        Err(TlsConfigError::MissingKey)
    }
}

pub fn load_certs(pem: &str) -> Result<Vec<CertificateDer<'static>>, TlsConfigError> {
    load_pem_certs(pem, TlsConfigError::BadCert)
}

pub fn load_ca_certs(pem: &str) -> Result<Vec<CertificateDer<'static>>, TlsConfigError> {
    load_pem_certs(pem, TlsConfigError::BadClientCa)
}

fn load_pem_certs(
    pem: &str,
    map_err: fn(String) -> TlsConfigError,
) -> Result<Vec<CertificateDer<'static>>, TlsConfigError> {
    let mut reader = Cursor::new(pem.as_bytes());
    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| map_err(e.to_string()))?;
    if certs.is_empty() {
        return Err(map_err("no certificates in PEM".into()));
    }
    Ok(certs)
}

pub fn load_private_key(pem: &str) -> Result<PrivateKeyDer<'static>, TlsConfigError> {
    let mut reader = Cursor::new(pem.as_bytes());
    for item in rustls_pemfile::read_all(&mut reader) {
        match item.map_err(|e| TlsConfigError::BadKey(e.to_string()))? {
            rustls_pemfile::Item::Pkcs1Key(key) => {
                return Ok(PrivateKeyDer::Pkcs1(key));
            }
            rustls_pemfile::Item::Pkcs8Key(key) => {
                return Ok(PrivateKeyDer::Pkcs8(key));
            }
            rustls_pemfile::Item::Sec1Key(key) => {
                return Ok(PrivateKeyDer::Sec1(key));
            }
            _ => {}
        }
    }
    Err(TlsConfigError::BadKey("no private key in PEM".into()))
}

pub fn certified_key_from_pem(
    cert_pem: &str,
    key_pem: &str,
    provider: Arc<rustls::crypto::CryptoProvider>,
) -> Result<CertifiedKey, TlsConfigError> {
    let certs = load_certs(cert_pem)?;
    let key = load_private_key(key_pem)?;
    let signing = provider
        .key_provider
        .load_private_key(key)
        .map_err(|e| TlsConfigError::BadKey(e.to_string()))?;
    Ok(CertifiedKey::new(certs, signing))
}

/// Env prefix for HTTP/2 cert material (`NAUTALID_TLS_H2_*` or fallback `NAUTALID_TLS_*`).
#[must_use]
pub fn h2_cert_env_prefix() -> &'static str {
    if split_cert_env_touched("NAUTALID_TLS_H2") {
        "NAUTALID_TLS_H2"
    } else {
        "NAUTALID_TLS"
    }
}

/// Env prefix for HTTP/3 cert material (`NAUTALID_TLS_H3_*` or fallback `NAUTALID_TLS_*`).
#[must_use]
pub fn h3_cert_env_prefix() -> &'static str {
    if split_cert_env_touched("NAUTALID_TLS_H3") {
        "NAUTALID_TLS_H3"
    } else {
        "NAUTALID_TLS"
    }
}

fn split_cert_env_touched(prefix: &str) -> bool {
    [
        format!("{prefix}_CERT_PEM"),
        format!("{prefix}_CERT_PATH"),
        format!("{prefix}_KEY_PEM"),
        format!("{prefix}_KEY_PATH"),
    ]
    .iter()
    .any(|name| {
        std::env::var(name)
            .ok()
            .is_some_and(|s| !s.trim().is_empty())
    })
}