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.
//! Platform-native trust store probes (fingerprint + SSL policy).

use std::process::Command;

use product_os_security::certificates::ManagedCa;
use sha2::{Digest, Sha256};

use crate::ca::trust::TrustStatus;

/// Probe the macOS System keychain for the managed CA fingerprint with SSL trust.
#[cfg(target_os = "macos")]
pub fn check_macos_system_keychain(managed: &ManagedCa) -> TrustStatus {
    probe_macos_keychain(managed, "/Library/Keychains/System.keychain")
}

/// Probe the macOS login keychain for the managed CA fingerprint with SSL trust.
#[cfg(target_os = "macos")]
pub fn check_macos_user_keychain(managed: &ManagedCa) -> TrustStatus {
    let Ok(home) = std::env::var("HOME") else {
        return TrustStatus::Unknown("HOME not set".into());
    };
    let keychain = format!("{home}/Library/Keychains/login.keychain-db");
    probe_macos_keychain(managed, &keychain)
}

#[cfg(target_os = "macos")]
fn probe_macos_keychain(managed: &ManagedCa, keychain: &str) -> TrustStatus {
    let expected = match managed.fingerprint_sha256_compact() {
        Ok(fp) => fp.to_ascii_uppercase(),
        Err(e) => return TrustStatus::Unknown(e.to_string()),
    };

    let hashes = match keychain_sha256_hashes(keychain) {
        Ok(h) => h,
        Err(e) => return TrustStatus::Unknown(e),
    };

    // Stale duplicate with the same subject must trigger reinstall even when the expected
    // fingerprint is also present (common after CA rotation without uninstall).
    if stale_subject_fingerprint_in_keychain(managed, keychain, &expected) {
        return TrustStatus::FingerprintMismatch;
    }

    if hashes.iter().any(|h| h == &expected) {
        return match verify_macos_ssl_trust(managed, Some(keychain)) {
            Ok(true) => TrustStatus::Trusted,
            Ok(false) => TrustStatus::InstalledNotTrusted,
            Err(e) => TrustStatus::Unknown(e),
        };
    }

    TrustStatus::NotInstalled
}

#[cfg(target_os = "macos")]
fn keychain_sha256_hashes(keychain: &str) -> Result<Vec<String>, String> {
    let output = Command::new("security")
        .args(["find-certificate", "-a", "-Z", keychain])
        .output()
        .map_err(|e| format!("security find-certificate failed: {e}"))?;
    Ok(parse_sha256_hashes(&String::from_utf8_lossy(
        &output.stdout,
    )))
}

#[cfg(target_os = "macos")]
fn parse_sha256_hashes(text: &str) -> Vec<String> {
    text.lines()
        .filter_map(|line| {
            let trimmed = line.trim();
            let rest = trimmed.strip_prefix("SHA-256 hash:")?;
            Some(rest.trim().to_ascii_uppercase())
        })
        .collect()
}

#[cfg(target_os = "macos")]
fn stale_subject_fingerprint_in_keychain(
    managed: &ManagedCa,
    keychain: &str,
    expected: &str,
) -> bool {
    let cn = managed_ca_common_name(managed);
    let output = Command::new("security")
        .args(["find-certificate", "-a", "-c", &cn, "-Z", keychain])
        .output();
    let Ok(out) = output else {
        return false;
    };
    let hashes = parse_sha256_hashes(&String::from_utf8_lossy(&out.stdout));
    hashes.iter().any(|h| h != expected && !h.is_empty())
}

#[cfg(target_os = "macos")]
fn managed_ca_common_name(managed: &ManagedCa) -> String {
    use x509_parser::pem::parse_x509_pem;
    if let Ok((_, pem)) = parse_x509_pem(managed.cert_pem()) {
        if let Ok((_, cert)) = x509_parser::parse_x509_certificate(&pem.contents) {
            for rdn in cert.subject().iter_common_name() {
                if let Ok(cn) = rdn.as_str() {
                    return cn.to_string();
                }
            }
        }
    }
    "Product OS development CA".to_string()
}

#[cfg(target_os = "macos")]
pub(crate) fn verify_macos_ssl_trust(
    managed: &ManagedCa,
    keychain: Option<&str>,
) -> Result<bool, String> {
    let cert_path = managed.cert_path().display().to_string();
    let mut cmd = Command::new("security");
    cmd.args(["verify-cert", "-c", &cert_path, "-p", "ssl"]);
    if let Some(kc) = keychain {
        cmd.args(["-k", kc]);
    }
    let output = cmd
        .output()
        .map_err(|e| format!("security verify-cert failed: {e}"))?;
    let text = format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    Ok(output.status.success() && text.to_ascii_lowercase().contains("successful"))
}

/// Probe the OS system trust store for the managed CA (platform-specific).
pub fn check_system_native(managed: &ManagedCa) -> Option<TrustStatus> {
    #[cfg(target_os = "macos")]
    {
        return Some(check_macos_system_keychain(managed));
    }
    #[cfg(target_os = "windows")]
    {
        return check_windows_native(managed);
    }
    #[cfg(target_os = "linux")]
    {
        return check_linux_native(managed);
    }
    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
    {
        let _ = managed;
        None
    }
}

/// Probe the macOS login keychain (alias for macOS user target).
#[cfg(target_os = "macos")]
pub fn check_macos_user_native(managed: &ManagedCa) -> Option<TrustStatus> {
    Some(check_macos_user_keychain(managed))
}

#[cfg(not(target_os = "macos"))]
pub fn check_macos_user_native(_managed: &ManagedCa) -> Option<TrustStatus> {
    None
}

#[cfg(target_os = "linux")]
fn check_linux_native(managed: &ManagedCa) -> Option<TrustStatus> {
    let needle = managed.fingerprint_sha256_compact().ok()?;
    for dir in [
        "/etc/ssl/certs",
        "/usr/local/share/ca-certificates",
        "/etc/pki/ca-trust/source/anchors",
        "/etc/pki/tls/certs",
    ] {
        if scan_cert_dir_for_fingerprint(std::path::Path::new(dir), &needle) {
            return Some(TrustStatus::Trusted);
        }
    }
    Some(TrustStatus::NotInstalled)
}

#[cfg(target_os = "windows")]
fn check_windows_native(managed: &ManagedCa) -> Option<TrustStatus> {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;
    use windows_sys::Win32::Security::Cryptography::{
        CertCloseStore, CertEnumCertificatesInStore, CertOpenSystemStoreW,
    };

    let needle = managed
        .fingerprint_sha256_compact()
        .ok()?
        .to_ascii_uppercase();
    unsafe {
        let wide: Vec<u16> = OsStr::new("ROOT")
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();
        let store = CertOpenSystemStoreW(0, wide.as_ptr());
        if store.is_null() {
            return None;
        }

        let mut ctx = std::ptr::null_mut();
        let mut found = false;
        loop {
            ctx = CertEnumCertificatesInStore(store, ctx);
            if ctx.is_null() {
                break;
            }
            let cert = &*ctx;
            if cert.pbCertEncoded.is_null() || cert.cbCertEncoded == 0 {
                continue;
            }
            let slice = std::slice::from_raw_parts(cert.pbCertEncoded, cert.cbCertEncoded as usize);
            if certificate_matches_fingerprint(slice, &needle) {
                found = true;
                break;
            }
        }
        CertCloseStore(store, 0);
        if found {
            Some(TrustStatus::Trusted)
        } else {
            Some(TrustStatus::NotInstalled)
        }
    }
}

#[cfg(target_os = "linux")]
fn scan_cert_dir_for_fingerprint(dir: &std::path::Path, needle: &str) -> bool {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return false;
    };
    let upper = needle.to_ascii_uppercase();
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            if scan_cert_dir_for_fingerprint(&path, needle) {
                return true;
            }
            continue;
        }
        let Ok(data) = std::fs::read(&path) else {
            continue;
        };
        if cert_bytes_match_fingerprint(&data, &upper) {
            return true;
        }
    }
    false
}

#[allow(dead_code)]
fn certificate_matches_fingerprint(der: &[u8], needle: &str) -> bool {
    certificate_sha256_hex(der).is_some_and(|hex| hex == needle.to_ascii_uppercase())
}

#[allow(dead_code)]
fn cert_bytes_match_fingerprint(data: &[u8], needle: &str) -> bool {
    certificate_sha256_hex(data).is_some_and(|hex| hex == needle)
}

#[allow(dead_code)]
fn certificate_sha256_hex(data: &[u8]) -> Option<String> {
    if let Ok((_, pem)) = x509_parser::pem::parse_x509_pem(data) {
        let digest = Sha256::digest(pem.contents.as_slice());
        return Some(digest.iter().map(|b| format!("{b:02X}")).collect());
    }
    if x509_parser::parse_x509_certificate(data).is_ok() {
        let digest = Sha256::digest(data);
        return Some(digest.iter().map(|b| format!("{b:02X}")).collect());
    }
    None
}

/// Probe NSS trust via bundled scripts (fingerprint-aware).
pub fn check_nss_script(managed: &ManagedCa, script_name: &str) -> TrustStatus {
    let cert_path = managed.cert_path().display().to_string();
    let script = crate::ca::install::script_path(script_name);
    let output = std::process::Command::new("sh")
        .arg(&script)
        .arg("probe")
        .arg(&cert_path)
        .output();
    match output {
        Ok(out) if out.status.success() => TrustStatus::Trusted,
        Ok(out) if out.status.code() == Some(2) => TrustStatus::NotInstalled,
        Ok(out) if out.status.code() == Some(3) => TrustStatus::FingerprintMismatch,
        Ok(out) if out.status.code() == Some(4) => TrustStatus::InstalledNotTrusted,
        Ok(out) => TrustStatus::Unknown(format!(
            "nss probe failed: {}",
            String::from_utf8_lossy(&out.stderr)
        )),
        Err(e) => TrustStatus::Unknown(format!("nss probe failed: {e}")),
    }
}

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

    #[test]
    fn parse_sha256_hashes_extracts_fingerprints() {
        let text = "SHA-256 hash: ABCD0123\nSHA-256 hash: 97DEAB23DB9F\n";
        let hashes = parse_sha256_hashes(text);
        assert_eq!(hashes.len(), 2);
        assert_eq!(hashes[0], "ABCD0123");
        assert_eq!(hashes[1], "97DEAB23DB9F");
    }

    #[test]
    fn certificate_sha256_hex_parses_pem() {
        let dir = tempfile::tempdir().unwrap();
        let config = product_os_security::certificates::ManagedCaConfig::with_storage_dir(
            dir.path().to_path_buf(),
        );
        let managed =
            product_os_security::certificates::ManagedCa::load_or_create(&config).unwrap();
        let hex = certificate_sha256_hex(managed.cert_pem()).expect("fingerprint");
        assert_eq!(hex.len(), 64);
    }

    /// Live keychain probe against the user's managed CA (manual diagnostic).
    #[test]
    #[ignore]
    fn macos_keychain_probe_live() {
        use crate::ca::trust::{TrustProbe, TrustStatus};
        let config = product_os_security::certificates::ManagedCaConfig::default();
        let managed =
            product_os_security::certificates::ManagedCa::load_or_create(&config).unwrap();
        let system = TrustProbe::check_target(&managed, "system");
        let user = TrustProbe::check_target(&managed, "macos-user");
        println!("system={system:?} macos-user={user:?}");
        assert_eq!(system, TrustStatus::NotInstalled);
        assert_eq!(
            user,
            TrustStatus::FingerprintMismatch,
            "login keychain has stale duplicate CA"
        );
    }
}