use product_os_security::certificates::ManagedCa;
use product_os_utilities::ProductOSError;
use serde::{Deserialize, Serialize};
use crate::ca::install::TrustInstaller;
use crate::ca::probe::active_platform_https_self_test;
use crate::ca::trust::{needs_reinstall, needs_uninstall_before_install, TrustProbe, TrustStatus};
use crate::config::{NetworkProxyCertificateAuthorityTrust, NetworkProxyTrustTarget};
#[must_use]
pub fn default_browser_trust_targets() -> Vec<String> {
let mut targets = vec!["system".to_string(), "chromium-profile".to_string()];
#[cfg(target_os = "macos")]
targets.push("macos-user".to_string());
targets
}
#[must_use]
pub fn session_trust_targets() -> Vec<String> {
#[cfg(target_os = "macos")]
{
vec!["macos-user".to_string(), "system".to_string()]
}
#[cfg(not(target_os = "macos"))]
{
default_browser_trust_targets()
}
}
#[must_use]
pub fn trust_status_str(status: &TrustStatus) -> &'static str {
match status {
TrustStatus::Trusted => "trusted",
TrustStatus::InstalledNotTrusted => "installed-not-trusted",
TrustStatus::NotInstalled => "not-installed",
TrustStatus::FingerprintMismatch => "fingerprint-mismatch",
TrustStatus::BypassedViaFlag => "bypassed-via-flag",
TrustStatus::Unknown(_) => "unknown",
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetTrustStatus {
pub name: String,
pub status: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaTrustReport {
pub all_trusted: bool,
pub targets: Vec<TargetTrustStatus>,
pub ca_path: String,
pub fingerprint: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaTrustSetup {
pub all_trusted: bool,
pub targets: Vec<TargetTrustStatus>,
pub ca_path: String,
pub fingerprint: String,
pub setup_url: String,
pub manual_steps: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaSetupPrompt {
pub ok: bool,
pub code: &'static str,
pub message: String,
pub setup: CaTrustSetup,
pub next_operation: &'static str,
pub requires_user_consent: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnsureTrustOptions {
pub auto_install: bool,
#[serde(default)]
pub targets: Option<Vec<String>>,
#[serde(default)]
pub active_probe: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetInstallResult {
pub name: String,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnsureTrustResult {
pub all_trusted: bool,
pub report: CaTrustReport,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub install_results: Vec<TargetInstallResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_probe_ok: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_probe_error: Option<String>,
}
#[must_use]
pub fn probe_trust_report(managed: &ManagedCa, targets: &[String]) -> CaTrustReport {
let effective = if targets.is_empty() {
default_browser_trust_targets()
} else {
targets.to_vec()
};
let target_statuses: Vec<TargetTrustStatus> = effective
.iter()
.map(|name| TargetTrustStatus {
name: name.clone(),
status: trust_status_str(&TrustProbe::check_target(managed, name)).to_string(),
})
.collect();
let all_trusted = effective_all_trusted(&target_statuses);
CaTrustReport {
all_trusted,
targets: target_statuses,
ca_path: managed.cert_path().display().to_string(),
fingerprint: managed
.fingerprint_sha256()
.unwrap_or_else(|_| "unknown".into()),
}
}
#[must_use]
pub fn strict_all_trusted(targets: &[TargetTrustStatus]) -> bool {
targets.iter().all(|t| t.status == "trusted")
}
#[must_use]
pub fn effective_all_trusted(targets: &[TargetTrustStatus]) -> bool {
if targets.iter().all(|t| t.status == "trusted") {
return true;
}
#[cfg(target_os = "macos")]
{
let system_ok = targets
.iter()
.any(|t| t.name == "system" && t.status == "trusted");
let user_ok = targets.iter().any(|t| {
(t.name == "macos-user" || t.name == "user-keychain") && t.status == "trusted"
});
if system_ok && user_ok {
return targets
.iter()
.all(|t| t.status == "trusted" || ignorable_macos_nss_target(t));
}
}
false
}
#[cfg(target_os = "macos")]
fn ignorable_macos_nss_target(t: &TargetTrustStatus) -> bool {
matches!(t.name.as_str(), "chromium-profile" | "chromium" | "firefox") && t.status == "unknown"
}
#[cfg(not(target_os = "macos"))]
fn ignorable_macos_nss_target(_t: &TargetTrustStatus) -> bool {
false
}
#[must_use]
pub fn manual_trust_steps(managed: &ManagedCa) -> Vec<String> {
let mut steps = vec![
format!("CA file: {}", managed.cert_path().display()),
format!(
"Fingerprint: {}",
managed
.fingerprint_sha256()
.unwrap_or_else(|_| "unknown".into())
),
];
#[cfg(target_os = "macos")]
{
steps.push(
"Call ca_ensure_trust with auto_install: true — macOS will show an administrator \
password dialog to install into the System keychain (required for Chrome)."
.into(),
);
steps.push(
"Login keychain (no admin): pos-proxy cert install --target macos-user --trust".into(),
);
}
#[cfg(target_os = "windows")]
steps.push("certmgr.msc → Trusted Root Certification Authorities → Import".into());
#[cfg(target_os = "linux")]
steps.push("Copy to /usr/local/share/ca-certificates/ and run update-ca-certificates".into());
steps.push(
"Firefox (persistent profiles): pos-proxy cert install --target firefox --trust".into(),
);
steps.push(
"Firefox (agent ephemeral profiles): the browser engine seeds each --profile directory \
via distribution/certs + policies.Certificates.Install before launch — global NSS \
install is optional."
.into(),
);
steps.push("Chromium profile: pos-proxy cert install --target chromium-profile --trust".into());
steps
}
#[must_use]
pub fn build_setup_prompt(
report: &CaTrustReport,
proxy_host: &str,
proxy_port: u16,
) -> CaSetupPrompt {
let setup_url = format!("http://{proxy_host}:{proxy_port}/_product-os/setup");
let manual_steps = manual_trust_steps_from_path(&report.ca_path, &report.fingerprint);
CaSetupPrompt {
ok: false,
code: "ca_setup_required",
message: setup_required_message(report),
setup: CaTrustSetup {
all_trusted: report.all_trusted,
targets: report.targets.clone(),
ca_path: report.ca_path.clone(),
fingerprint: report.fingerprint.clone(),
setup_url,
manual_steps,
},
next_operation: "ca_ensure_trust",
requires_user_consent: true,
}
}
fn setup_required_message(report: &CaTrustReport) -> String {
let mismatched: Vec<&str> = report
.targets
.iter()
.filter(|t| t.status == "fingerprint-mismatch")
.map(|t| t.name.as_str())
.collect();
if !mismatched.is_empty() {
return format!(
"Stale MITM proxy root CA fingerprint in {}. Ask the user for permission to \
reinstall it, then call ca_ensure_trust with auto_install: true.",
mismatched.join(", ")
);
}
#[cfg(target_os = "macos")]
{
if report
.targets
.iter()
.any(|t| t.name == "system" && t.status == "not-installed")
{
return "Chrome requires the MITM root CA in the macOS System keychain. Ask the \
user for permission, then call ca_ensure_trust with auto_install: true — \
a macOS administrator password dialog will appear for System keychain install."
.into();
}
}
"MITM proxy root CA is not trusted. Ask the user for permission to install it, \
then call ca_ensure_trust with auto_install: true."
.into()
}
#[must_use]
pub fn system_trust_install_command(ca_path: &str) -> String {
format!(
"sudo security add-trusted-cert -d -r trustRoot -p ssl -k /Library/Keychains/System.keychain {ca_path}"
)
}
fn manual_trust_steps_from_path(ca_path: &str, fingerprint: &str) -> Vec<String> {
let mut steps = vec![
format!("CA file: {ca_path}"),
format!("Fingerprint: {fingerprint}"),
];
#[cfg(target_os = "macos")]
{
steps.push(
"Call ca_ensure_trust with auto_install: true — macOS will show an administrator \
password dialog to install into the System keychain (required for Chrome)."
.into(),
);
steps.push(
"Login keychain (no admin): pos-proxy cert install --target macos-user --trust".into(),
);
}
#[cfg(target_os = "windows")]
steps.push("certmgr.msc → Trusted Root Certification Authorities → Import".into());
#[cfg(target_os = "linux")]
steps.push("Copy to /usr/local/share/ca-certificates/ and run update-ca-certificates".into());
steps.push(
"Firefox (persistent profiles): pos-proxy cert install --target firefox --trust".into(),
);
steps.push(
"Firefox (agent ephemeral profiles): the browser engine seeds each --profile directory \
via distribution/certs + policies.Certificates.Install before launch — global NSS \
install is optional."
.into(),
);
steps.push("Chromium profile: pos-proxy cert install --target chromium-profile --trust".into());
steps
}
fn resolve_targets(
trust_config: &NetworkProxyCertificateAuthorityTrust,
override_targets: Option<Vec<String>>,
) -> Vec<String> {
override_targets.unwrap_or_else(|| {
if trust_config.targets.is_empty() {
default_browser_trust_targets()
} else {
trust_config.targets.clone()
}
})
}
pub(crate) fn auto_install_targets_internal(
managed: &ManagedCa,
targets: &[String],
) -> Vec<TargetInstallResult> {
let mut results = Vec::new();
for target in targets {
match NetworkProxyTrustTarget::parse(target) {
Some(parsed) => match TrustInstaller::install(managed, parsed, true) {
Ok(()) => results.push(TargetInstallResult {
name: target.clone(),
ok: true,
error: None,
action: Some("install".into()),
}),
Err(e) => results.push(TargetInstallResult {
name: target.clone(),
ok: false,
error: Some(format!("{e}")),
action: Some("install".into()),
}),
},
None => results.push(TargetInstallResult {
name: target.clone(),
ok: false,
error: Some(format!("unknown trust target: {target}")),
action: None,
}),
}
}
results
}
fn reinstall_target(
managed: &ManagedCa,
target: &str,
prior_status: TrustStatus,
) -> TargetInstallResult {
let Some(parsed) = NetworkProxyTrustTarget::parse(target) else {
return TargetInstallResult {
name: target.to_string(),
ok: false,
error: Some(format!("unknown trust target: {target}")),
action: None,
};
};
if needs_uninstall_before_install(&prior_status) {
let _ = TrustInstaller::uninstall(managed, parsed);
}
match TrustInstaller::install(managed, parsed, true) {
Ok(()) => TargetInstallResult {
name: target.to_string(),
ok: true,
error: None,
action: Some("reinstall".into()),
},
Err(e) => TargetInstallResult {
name: target.to_string(),
ok: false,
error: Some(format!("{e}")),
action: Some("reinstall".into()),
},
}
}
pub async fn ensure_ca_trust(
managed: &ManagedCa,
trust_config: &NetworkProxyCertificateAuthorityTrust,
proxy_host: &str,
proxy_port: u16,
opts: &EnsureTrustOptions,
) -> Result<EnsureTrustResult, ProductOSError> {
let targets = resolve_targets(trust_config, opts.targets.clone());
let mut install_results = Vec::new();
if opts.auto_install && trust_config.allow_auto_install {
for target in &targets {
let status = TrustProbe::check_target(managed, target);
if needs_reinstall(&status) {
install_results.push(reinstall_target(managed, target, status));
}
}
}
let report = probe_trust_report(managed, &targets);
let mut active_probe_ok = None;
let mut active_probe_error = None;
if opts.active_probe && report.all_trusted {
match active_platform_https_self_test(proxy_host, proxy_port).await {
Ok(()) => active_probe_ok = Some(true),
Err(err) => {
active_probe_ok = Some(false);
active_probe_error = Some(err);
}
}
} else if opts.active_probe && !report.all_trusted {
active_probe_ok = Some(false);
active_probe_error = Some(
"skipped platform HTTPS probe because fingerprint-matched trust is incomplete".into(),
);
}
let all_trusted = report.all_trusted && active_probe_ok.unwrap_or(true);
Ok(EnsureTrustResult {
all_trusted,
report,
install_results,
active_probe_ok,
active_probe_error,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_targets_include_chromium_profile() {
let t = default_browser_trust_targets();
assert!(t.iter().any(|x| x == "chromium-profile"));
assert!(t.iter().any(|x| x == "system"));
}
#[test]
fn session_trust_targets_macos_require_system_and_login() {
let t = session_trust_targets();
#[cfg(target_os = "macos")]
{
assert!(t.iter().any(|x| x == "system"));
assert!(t.iter().any(|x| x == "macos-user"));
assert!(!t.iter().any(|x| x == "chromium-profile"));
}
}
#[test]
fn strict_all_trusted_requires_every_target_trusted() {
let targets = vec![
TargetTrustStatus {
name: "system".into(),
status: "trusted".into(),
},
TargetTrustStatus {
name: "macos-user".into(),
status: "unknown".into(),
},
];
assert!(!strict_all_trusted(&targets));
}
#[test]
fn trust_status_str_maps_fingerprint_mismatch() {
assert_eq!(trust_status_str(&TrustStatus::Trusted), "trusted");
assert_eq!(
trust_status_str(&TrustStatus::NotInstalled),
"not-installed"
);
assert_eq!(
trust_status_str(&TrustStatus::FingerprintMismatch),
"fingerprint-mismatch"
);
}
#[test]
fn needs_uninstall_before_install_skips_not_installed() {
assert!(!needs_uninstall_before_install(&TrustStatus::NotInstalled));
assert!(needs_uninstall_before_install(
&TrustStatus::FingerprintMismatch
));
}
#[test]
fn needs_reinstall_detects_mismatch() {
assert!(needs_reinstall(&TrustStatus::FingerprintMismatch));
assert!(needs_reinstall(&TrustStatus::NotInstalled));
assert!(!needs_reinstall(&TrustStatus::Trusted));
}
#[test]
fn effective_all_trusted_macos_ignores_unknown_nss_when_keychains_trusted() {
let targets = vec![
TargetTrustStatus {
name: "system".into(),
status: "trusted".into(),
},
TargetTrustStatus {
name: "macos-user".into(),
status: "trusted".into(),
},
TargetTrustStatus {
name: "chromium-profile".into(),
status: "unknown".into(),
},
];
assert!(effective_all_trusted(&targets));
}
#[test]
fn effective_all_trusted_requires_all_when_keychains_missing() {
let targets = vec![
TargetTrustStatus {
name: "system".into(),
status: "trusted".into(),
},
TargetTrustStatus {
name: "macos-user".into(),
status: "not-installed".into(),
},
TargetTrustStatus {
name: "chromium-profile".into(),
status: "unknown".into(),
},
];
assert!(!effective_all_trusted(&targets));
}
#[test]
fn setup_required_message_mentions_fingerprint_mismatch() {
let report = CaTrustReport {
all_trusted: false,
targets: vec![TargetTrustStatus {
name: "system".into(),
status: "fingerprint-mismatch".into(),
}],
ca_path: "/tmp/ca.pem".into(),
fingerprint: "aa:bb".into(),
};
let msg = setup_required_message(&report);
assert!(msg.contains("Stale MITM proxy root CA fingerprint"));
assert!(msg.contains("system"));
}
}