product-os-proxy 0.0.19

Product OS : Proxy builds on the work of hudsucker, taking it to the next level with a man-in-the-middle proxy server that can tunnel traffic through a VPN utilising Product OS : VPN.
//! Resolve MITM root CA material from proxy configuration.

use std::path::PathBuf;

use product_os_security::certificates::{
    default_storage_dir, CaProfile, Certificates, CertificatesFormat, ManagedCa, ManagedCaConfig,
};
use product_os_utilities::ProductOSError;
use rcgen::KeyPair;

use crate::config::NetworkProxyCertificateAuthority;
use crate::error::ProxyError;

/// Loaded root CA material and optional managed metadata.
pub struct LoadedCa {
    /// Certificate key material for MITM signing.
    pub certificates: Certificates,
    /// PEM bytes for serving or trust installation.
    pub cert_pem: Vec<u8>,
    /// Paths when loaded from managed or file storage.
    pub cert_path: Option<PathBuf>,
    /// Path to the CA private key when loaded from disk.
    pub key_path: Option<PathBuf>,
}

impl LoadedCa {
    /// Build a [`ManagedCaConfig`] from on-disk cert/key paths when both are known.
    pub fn to_managed_config(&self) -> Option<ManagedCaConfig> {
        let cert_path = self.cert_path.as_ref()?;
        let key_path = self.key_path.as_ref()?;
        Some(ManagedCaConfig {
            storage_dir: cert_path
                .parent()
                .unwrap_or_else(|| std::path::Path::new("."))
                .to_path_buf(),
            cert_file: cert_path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("ca.pem")
                .to_string(),
            key_file: key_path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("ca.key")
                .to_string(),
            profile: CaProfile::development(),
        })
    }

    /// Load CA from explicit file paths.
    pub fn from_files(cert_file: String, key_file: String) -> Self {
        let certificates =
            Certificates::new_ca_from_file(cert_file.clone(), key_file.clone(), None);
        let cert_pem = certificates
            .certificates
            .first()
            .cloned()
            .unwrap_or_default();
        Self {
            certificates,
            cert_pem,
            cert_path: Some(PathBuf::from(cert_file)),
            key_path: Some(PathBuf::from(key_file)),
        }
    }

    /// Load or create a managed CA using proxy configuration.
    pub fn from_managed(
        managed: &crate::config::NetworkProxyCertificateAuthorityManaged,
    ) -> Result<Self, ProductOSError> {
        let storage_dir = managed
            .storage_path
            .as_ref()
            .map(|p| expand_tilde(p))
            .transpose()
            .map_err(|e| ProductOSError::from(ProxyError::CertLoadFailed(e)))?
            .unwrap_or_else(|| {
                default_storage_dir().unwrap_or_else(|_| PathBuf::from(".product-os/proxy"))
            });

        let mut profile = CaProfile::development();
        if let Some(cn) = &managed.common_name {
            profile.common_name = cn.clone();
        }

        let config = ManagedCaConfig {
            storage_dir,
            cert_file: "ca.pem".to_string(),
            key_file: "ca.key".to_string(),
            profile,
        };

        let managed_ca = ManagedCa::load_or_create(&config)
            .map_err(|e| ProductOSError::from(ProxyError::CertLoadFailed(e.to_string())))?;

        Ok(Self {
            certificates: managed_ca.certificates().clone(),
            cert_pem: managed_ca.cert_pem().to_vec(),
            cert_path: Some(managed_ca.cert_path().to_path_buf()),
            key_path: Some(managed_ca.key_path().to_path_buf()),
        })
    }
}

/// Resolve CA material from `certificateAuthority` configuration.
pub fn load_certificate_authority(
    ca_config: &NetworkProxyCertificateAuthority,
) -> Result<LoadedCa, ProductOSError> {
    let loaded = if let Some(files) = &ca_config.files {
        LoadedCa::from_files(files.cert_file.clone(), files.key_file.clone())
    } else if let Some(managed) = &ca_config.managed {
        if managed.enabled {
            LoadedCa::from_managed(managed)?
        } else {
            return Err(ProductOSError::from(ProxyError::NoCertsProvided));
        }
    } else {
        return Err(ProductOSError::from(ProxyError::NoCertsProvided));
    };

    Ok(LoadedCa {
        certificates: normalize_pkcs8(loaded.certificates),
        ..loaded
    })
}

/// Ensure CA cert and private key are DER/PKCS#8 for the MITM builder.
fn normalize_pkcs8(mut certificates: Certificates) -> Certificates {
    if matches!(
        certificates.format,
        CertificatesFormat::Pkcs8 | CertificatesFormat::Der
    ) {
        return certificates;
    }

    let Some(cert_bytes) = certificates.certificates.first() else {
        return certificates;
    };

    let Ok(cert_pem) = std::str::from_utf8(cert_bytes) else {
        return certificates;
    };
    let Ok(key_pem) = std::str::from_utf8(&certificates.private) else {
        return certificates;
    };

    if let (Ok(params), Ok(key_pair)) = (
        rcgen::CertificateParams::from_ca_cert_pem(cert_pem),
        KeyPair::from_pem(key_pem),
    ) {
        if let Ok(cert) = params.self_signed(&key_pair) {
            certificates.certificates[0] = cert.der().to_vec();
            certificates.private = key_pair.serialize_der();
            certificates.format = CertificatesFormat::Pkcs8;
        }
    }

    certificates
}

fn expand_tilde(path: &str) -> Result<PathBuf, String> {
    if let Some(rest) = path.strip_prefix("~/") {
        let home = std::env::var("HOME")
            .or_else(|_| std::env::var("USERPROFILE"))
            .map_err(|_| "could not expand ~ in path".to_string())?;
        return Ok(PathBuf::from(home).join(rest));
    }
    if path == "~" {
        let home = std::env::var("HOME")
            .or_else(|_| std::env::var("USERPROFILE"))
            .map_err(|_| "could not expand ~".to_string())?;
        return Ok(PathBuf::from(home));
    }
    Ok(PathBuf::from(path))
}