use std::process::Command;
use product_os_security::certificates::ManagedCa;
use sha2::{Digest, Sha256};
use crate::ca::trust::TrustStatus;
#[cfg(target_os = "macos")]
pub fn check_macos_system_keychain(managed: &ManagedCa) -> TrustStatus {
probe_macos_keychain(managed, "/Library/Keychains/System.keychain")
}
#[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),
};
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"))
}
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
}
}
#[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
}
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);
}
#[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"
);
}
}