nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Optional OCSP stapling when loading server certificates.

use std::path::Path;

use tracing::warn;

use super::TlsConfigError;

/// Upper bound on OCSP bytes before PKIX parse (defense-in-depth for RUSTSEC-2026-0009).
const MAX_OCSP_PARSE_BYTES: usize = 64 * 1024;

/// Load an OCSP staple from env when **`NAUTALID_TLS_OCSP=1`**.
///
/// Checks **`{prefix}_OCSP_RESPONSE_PATH`** then **`NAUTALID_TLS_OCSP_RESPONSE_PATH`**.
#[must_use]
pub fn staple_from_env(prefix: &str) -> Option<Vec<u8>> {
    if !crate::surfaces::env_enabled("NAUTALID_TLS_OCSP") {
        return None;
    }
    let path = std::env::var(format!("{prefix}_OCSP_RESPONSE_PATH"))
        .ok()
        .filter(|p| !p.is_empty())
        .or_else(|| {
            std::env::var("NAUTALID_TLS_OCSP_RESPONSE_PATH")
                .ok()
                .filter(|p| !p.is_empty())
        })?;
    read_staple_file(&path).ok()
}

/// Resolve staple from file env or optional live fetch.
pub fn resolve_staple(cert_pem: &str, prefix: &str) -> Option<Vec<u8>> {
    if let Some(staple) = staple_from_env(prefix) {
        return Some(staple);
    }
    if fetch_enabled() {
        match fetch_live_staple(cert_pem) {
            Ok(staple) => return Some(staple),
            Err(err) => warn!(%err, "live OCSP staple fetch failed"),
        }
    }
    None
}

fn fetch_enabled() -> bool {
    crate::surfaces::env_enabled("NAUTALID_TLS_OCSP_FETCH")
}

fn read_staple_file(path: &str) -> Result<Vec<u8>, TlsConfigError> {
    let bytes = std::fs::read(path).map_err(|source| TlsConfigError::Io {
        path: path.to_owned(),
        source,
    })?;
    if bytes.is_empty() {
        return Err(TlsConfigError::Rustls(format!(
            "empty OCSP staple at {}",
            Path::new(path).display()
        )));
    }
    validate_ocsp_bytes(&bytes)?;
    Ok(bytes)
}

fn validate_ocsp_bytes(bytes: &[u8]) -> Result<(), TlsConfigError> {
    if bytes.len() > MAX_OCSP_PARSE_BYTES {
        return Err(TlsConfigError::Rustls(format!(
            "OCSP response exceeds {} byte parse cap",
            MAX_OCSP_PARSE_BYTES
        )));
    }
    Ok(())
}

fn fetch_live_staple(cert_pem: &str) -> Result<Vec<u8>, TlsConfigError> {
    let (leaf_der, issuer_der) = pem_chain_ders(cert_pem)?;
    if let Some(url) = ocsp_url_from_env() {
        let request = ocsp_staple::build_ocsp_request(&leaf_der, &issuer_der)
            .map_err(map_ocsp_err)?;
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .map_err(|e| TlsConfigError::Rustls(e.to_string()))?;
        let bytes = rt
            .block_on(ocsp_staple::fetch_ocsp(
                &url,
                request,
                ocsp_staple::FETCH_TIMEOUT,
            ))
            .map_err(map_ocsp_err)?;
        validate_ocsp_bytes(&bytes)?;
        let staple = ocsp_staple::parse_ocsp_response(&bytes).map_err(map_ocsp_err)?;
        return Ok(staple.staple);
    }
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .map_err(|e| TlsConfigError::Rustls(e.to_string()))?;
    let staple = rt
        .block_on(ocsp_staple::fetch_ocsp_for_cert(
            &leaf_der,
            &issuer_der,
            ocsp_staple::FETCH_TIMEOUT,
        ))
        .map_err(map_ocsp_err)?;
    validate_ocsp_bytes(&staple.staple)?;
    Ok(staple.staple)
}

fn map_ocsp_err(err: ocsp_staple::OcspError) -> TlsConfigError {
    TlsConfigError::Rustls(err.to_string())
}

fn ocsp_url_from_env() -> Option<String> {
    std::env::var("NAUTALID_TLS_OCSP_URL")
        .ok()
        .filter(|u| !u.trim().is_empty())
        .map(|u| u.trim().to_string())
}

fn pem_chain_ders(cert_pem: &str) -> Result<(Vec<u8>, Vec<u8>), TlsConfigError> {
    let mut reader = std::io::Cursor::new(cert_pem.as_bytes());
    let certs: Vec<Vec<u8>> = rustls_pemfile::certs(&mut reader)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| TlsConfigError::BadCert(e.to_string()))?
        .into_iter()
        .map(|c| c.to_vec())
        .collect();
    let leaf = certs
        .first()
        .cloned()
        .ok_or_else(|| TlsConfigError::BadCert("no certificate in PEM".into()))?;
    let issuer = certs.get(1).cloned().unwrap_or_else(|| leaf.clone());
    Ok((leaf, issuer))
}

/// Attach an OCSP staple to a certified key when stapling is enabled.
pub fn apply_staple(
    mut key: rustls::sign::CertifiedKey,
    staple: Option<Vec<u8>>,
) -> rustls::sign::CertifiedKey {
    if let Some(ocsp) = staple {
        key.ocsp = Some(ocsp);
    }
    key
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn disabled_without_env() {
        unsafe {
            std::env::remove_var("NAUTALID_TLS_OCSP");
        }
        assert!(staple_from_env("NAUTALID_TLS").is_none());
    }

    #[test]
    fn rejects_oversized_ocsp_bytes() {
        let oversized = vec![0u8; MAX_OCSP_PARSE_BYTES + 1];
        assert!(validate_ocsp_bytes(&oversized).is_err());
        assert!(validate_ocsp_bytes(&[1, 2, 3]).is_ok());
    }
}