#[cfg(not(feature = "interactive"))]
use std::io::Write;
use std::io::{self, IsTerminal};
#[cfg(feature = "interactive")]
use std::path::PathBuf;
use product_os_security::certificates::ManagedCa;
use product_os_utilities::ProductOSError;
use crate::ca::probe::{active_https_self_test, wait_for_proxy_ready};
use crate::ca::trust::TrustProbe;
use crate::ca::trust_api::{manual_trust_steps, probe_trust_report};
use crate::config::{NetworkProxyCertificateAuthorityTrust, NetworkProxyOnMissingTrust};
use crate::error::ProxyError;
pub async fn run_startup_trust_guide(
managed: &ManagedCa,
trust_config: &NetworkProxyCertificateAuthorityTrust,
proxy_host: &str,
proxy_port: u16,
) -> Result<(), ProductOSError> {
if !trust_config.verify_on_startup {
return Ok(());
}
let targets = &trust_config.targets;
if probe_trust_report(managed, targets).all_trusted {
tracing::info!("MITM root CA is trusted in configured stores");
return Ok(());
}
match trust_config.on_missing_trust {
NetworkProxyOnMissingTrust::Off => Ok(()),
NetworkProxyOnMissingTrust::Warn => {
log_missing_trust(managed, proxy_host, proxy_port, targets);
Ok(())
}
NetworkProxyOnMissingTrust::Guide => {
if io::stdin().is_terminal() && io::stdout().is_terminal() {
run_cert_guide_wizard(managed, trust_config, targets, proxy_host, proxy_port).await
} else {
log_missing_trust(managed, proxy_host, proxy_port, targets);
Ok(())
}
}
NetworkProxyOnMissingTrust::Block => {
log_missing_trust(managed, proxy_host, proxy_port, targets);
Err(ProductOSError::from(ProxyError::Generic(
"MITM root CA is not trusted; set onMissingTrust to warn or run pos-proxy cert guide"
.into(),
)))
}
}
}
pub async fn run_post_startup_active_probe(
managed: &ManagedCa,
trust_config: &NetworkProxyCertificateAuthorityTrust,
proxy_host: &str,
proxy_port: u16,
) -> Result<(), ProductOSError> {
if !trust_config.active_https_probe {
return Ok(());
}
let targets = &trust_config.targets;
if !probe_trust_report(managed, targets).all_trusted {
tracing::debug!(
"skipping active HTTPS probe because configured trust stores are incomplete"
);
return Ok(());
}
wait_for_proxy_ready(proxy_host, proxy_port)
.await
.map_err(|e| ProductOSError::from(ProxyError::Generic(e)))?;
match active_https_self_test(managed, proxy_host, proxy_port).await {
Ok(()) => {
tracing::info!("MITM root CA active HTTPS self-test passed");
Ok(())
}
Err(err) => {
tracing::warn!(error = %err, "MITM active HTTPS self-test failed");
if trust_config.on_missing_trust == NetworkProxyOnMissingTrust::Block {
Err(ProductOSError::from(ProxyError::Generic(err)))
} else {
Ok(())
}
}
}
}
pub async fn run_cert_guide_wizard(
managed: &ManagedCa,
trust_config: &NetworkProxyCertificateAuthorityTrust,
targets: &[String],
proxy_host: &str,
proxy_port: u16,
) -> Result<(), ProductOSError> {
print_status_header(managed, targets);
#[cfg(feature = "interactive")]
{
return run_dialoguer_wizard(managed, trust_config, targets, proxy_host, proxy_port).await;
}
#[cfg(not(feature = "interactive"))]
{
let _ = (proxy_host, proxy_port);
run_simple_prompt_guide(managed, trust_config, targets)
}
}
#[cfg(feature = "interactive")]
async fn run_dialoguer_wizard(
managed: &ManagedCa,
trust_config: &NetworkProxyCertificateAuthorityTrust,
targets: &[String],
proxy_host: &str,
proxy_port: u16,
) -> Result<(), ProductOSError> {
use dialoguer::{Confirm, Select};
loop {
let options = [
"Auto-install and trust (requires administrator permission)",
"Show manual installation steps",
"Export CA to Desktop",
"Re-check trust status",
"Run active HTTPS self-test through proxy",
"Skip for now",
];
let choice = Select::new()
.with_prompt("Product OS Proxy CA setup")
.items(&options)
.default(0)
.interact()
.map_err(|e| ProductOSError::from(ProxyError::Generic(e.to_string())))?;
match choice {
0 if trust_config.allow_auto_install => {
let proceed = Confirm::new()
.with_prompt("Proceed with automatic trust installation?")
.default(false)
.interact()
.map_err(|e| ProductOSError::from(ProxyError::Generic(e.to_string())))?;
if proceed {
auto_install_all(managed, targets)?;
if probe_trust_report(managed, targets).all_trusted {
println!("CA installed and trusted.");
return Ok(());
}
println!("Trust incomplete — try manual steps or export the CA.");
}
}
0 => println!("Auto-install disabled in configuration (allowAutoInstall=false)."),
1 => print_manual_steps(managed),
2 => export_ca_to_desktop(managed)?,
3 => print_status_header(managed, targets),
4 => {
if let Err(e) = wait_for_proxy_ready(proxy_host, proxy_port).await {
println!("Proxy not reachable: {e}");
} else {
match active_https_self_test(managed, proxy_host, proxy_port).await {
Ok(()) => println!("Active HTTPS self-test passed."),
Err(e) => println!("Active HTTPS self-test failed: {e}"),
}
}
}
_ => return Ok(()),
}
}
}
#[cfg(not(feature = "interactive"))]
fn run_simple_prompt_guide(
managed: &ManagedCa,
trust_config: &NetworkProxyCertificateAuthorityTrust,
targets: &[String],
) -> Result<(), ProductOSError> {
let stdout = io::stdout();
let mut out = stdout.lock();
if trust_config.allow_auto_install {
writeln!(
out,
"\nAuto-install requires administrator permission. Proceed? [y/N]: "
)
.ok();
out.flush().ok();
let mut line = String::new();
if io::stdin().read_line(&mut line).is_ok() && line.trim().eq_ignore_ascii_case("y") {
auto_install_all(managed, targets)?;
if probe_trust_report(managed, targets).all_trusted {
writeln!(out, "CA installed and trusted.").ok();
return Ok(());
}
}
}
print_manual_steps(managed);
Ok(())
}
fn auto_install_all(managed: &ManagedCa, targets: &[String]) -> Result<(), ProductOSError> {
let results = crate::ca::trust_api::auto_install_targets_internal(managed, targets);
for r in &results {
if !r.ok {
return Err(ProductOSError::from(ProxyError::Generic(
r.error
.clone()
.unwrap_or_else(|| format!("install failed for {}", r.name)),
)));
}
}
Ok(())
}
#[cfg(feature = "interactive")]
fn export_ca_to_desktop(managed: &ManagedCa) -> Result<(), ProductOSError> {
let desktop = std::env::var("HOME")
.map(|h| PathBuf::from(h).join("Desktop"))
.or_else(|_| std::env::var("USERPROFILE").map(|h| PathBuf::from(h).join("Desktop")))
.map_err(|_| {
ProductOSError::from(ProxyError::Generic("could not resolve Desktop".into()))
})?;
let dest = desktop.join("product-os-proxy-ca.pem");
std::fs::write(&dest, managed.cert_pem()).map_err(|e| {
ProductOSError::from(ProxyError::Generic(format!("failed to export CA: {e}")))
})?;
println!("Exported CA to {}", dest.display());
println!(
"Fingerprint: {}",
managed
.fingerprint_sha256()
.unwrap_or_else(|_| "unknown".into())
);
Ok(())
}
fn print_status_header(managed: &ManagedCa, targets: &[String]) {
println!("CA path: {}", managed.cert_path().display());
println!(
"Fingerprint: {}",
managed
.fingerprint_sha256()
.unwrap_or_else(|_| "unknown".into())
);
for target in targets {
println!("{target}: {}", TrustProbe::check_target(managed, target));
}
}
fn log_missing_trust(managed: &ManagedCa, proxy_host: &str, proxy_port: u16, targets: &[String]) {
let fingerprint = managed
.fingerprint_sha256()
.unwrap_or_else(|_| "unknown".into());
tracing::warn!(
cert_path = %managed.cert_path().display(),
fingerprint = %fingerprint,
setup_url = %format!("http://{proxy_host}:{proxy_port}/_product-os/setup"),
"MITM root CA is not trusted"
);
for target in targets {
let status = TrustProbe::check_target(managed, target);
tracing::warn!(target = %target, status = %status, "trust status");
}
tracing::warn!("Run `pos-proxy cert guide` to install and trust the CA");
}
fn print_manual_steps(managed: &ManagedCa) {
println!("\nManual installation steps:");
for step in manual_trust_steps(managed) {
println!("{step}");
}
}