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.
//! `pos-proxy` CLI for MITM root CA management and browser proxy setup.

use std::path::PathBuf;

use clap::{Parser, Subcommand};
use product_os_configuration::ProductOSConfig;
use product_os_proxy::{
    load_certificate_authority, run_cert_guide_wizard, NetworkProxy, NetworkProxyTrustTarget,
    TrustInstaller, TrustProbe,
};
use product_os_security::certificates::{default_storage_dir, ManagedCa, ManagedCaConfig};

#[derive(Parser)]
#[command(name = "pos-proxy", about = "Product OS Proxy CA and trust tooling")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Root CA operations
    Cert {
        #[command(subcommand)]
        command: CertCommands,
    },
    /// Browser proxy setup helpers
    Browser {
        #[command(subcommand)]
        command: BrowserCommands,
    },
}

#[derive(Subcommand)]
enum CertCommands {
    /// Show trust status for configured or managed CA
    Status {
        #[arg(long)]
        config: Option<PathBuf>,
        #[arg(long)]
        cert: Option<PathBuf>,
        #[arg(long)]
        storage_path: Option<PathBuf>,
    },
    /// Generate or load the managed root CA
    Generate {
        #[arg(long)]
        storage_path: Option<PathBuf>,
    },
    /// Export root CA PEM
    Export {
        #[arg(long)]
        output: Option<PathBuf>,
        #[arg(long, default_value = "pem")]
        format: String,
        #[arg(long)]
        storage_path: Option<PathBuf>,
    },
    /// Install CA into a trust store
    Install {
        #[arg(long)]
        cert: Option<PathBuf>,
        #[arg(long)]
        trust: bool,
        #[arg(long, default_value = "system")]
        target: String,
        #[arg(long)]
        storage_path: Option<PathBuf>,
    },
    /// Remove CA from a trust store
    Uninstall {
        #[arg(long)]
        cert: Option<PathBuf>,
        #[arg(long, default_value = "system")]
        target: String,
        #[arg(long)]
        storage_path: Option<PathBuf>,
    },
    /// Interactive guided trust installation
    Guide {
        #[arg(long)]
        config: Option<PathBuf>,
        #[arg(long)]
        storage_path: Option<PathBuf>,
    },
}

#[derive(Subcommand)]
enum BrowserCommands {
    /// Print browser proxy and trust setup instructions
    Setup {
        #[arg(value_enum)]
        browser: BrowserKind,
        #[arg(long, default_value = "127.0.0.1")]
        proxy_host: String,
        #[arg(long, default_value_t = 8080)]
        proxy_port: u16,
    },
}

#[derive(Clone, Copy, clap::ValueEnum)]
enum BrowserKind {
    Chromium,
    Firefox,
}

fn main() {
    if let Err(err) = run() {
        eprintln!("error: {err}");
        std::process::exit(1);
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    match cli.command {
        Commands::Cert { command } => handle_cert(command),
        Commands::Browser { command } => handle_browser(command),
    }
}

fn managed_from_storage(
    storage_path: Option<PathBuf>,
) -> Result<ManagedCa, Box<dyn std::error::Error>> {
    let storage_dir = storage_path.unwrap_or_else(|| {
        default_storage_dir().unwrap_or_else(|_| PathBuf::from(".product-os/proxy"))
    });
    let config = ManagedCaConfig::with_storage_dir(storage_dir);
    Ok(ManagedCa::load_or_create(&config)?)
}

fn handle_cert(command: CertCommands) -> Result<(), Box<dyn std::error::Error>> {
    match command {
        CertCommands::Status {
            config,
            cert,
            storage_path,
        } => {
            let managed = if let Some(path) = cert {
                let key_path = path
                    .parent()
                    .unwrap_or(std::path::Path::new("."))
                    .join("ca.key");
                let loaded = product_os_proxy::LoadedCa::from_files(
                    path.to_string_lossy().into_owned(),
                    key_path.to_string_lossy().into_owned(),
                );
                let config = loaded
                    .to_managed_config()
                    .unwrap_or_else(|| ManagedCaConfig {
                        storage_dir: path
                            .parent()
                            .unwrap_or(std::path::Path::new("."))
                            .to_path_buf(),
                        cert_file: path
                            .file_name()
                            .and_then(|n| n.to_str())
                            .unwrap_or("ca.pem")
                            .to_string(),
                        key_file: "ca.key".to_string(),
                        ..ManagedCaConfig::default()
                    });
                ManagedCa::load_or_create(&config)?
            } else if let Some(config_path) = config {
                let path = config_path.to_string_lossy();
                let proxy: NetworkProxy = NetworkProxy::from_unified_file(path.as_ref())
                    .or_else(|_| NetworkProxy::from_file(path.as_ref()))?;
                let ca_config = proxy
                    .certificate_authority
                    .ok_or("proxy config missing certificateAuthority")?;
                let loaded = load_certificate_authority(&ca_config).map_err(|e| format!("{e}"))?;
                let config = loaded
                    .to_managed_config()
                    .ok_or("no cert/key paths in configuration")?;
                ManagedCa::load_or_create(&config)?
            } else {
                managed_from_storage(storage_path)?
            };

            println!("cert: {}", managed.cert_path().display());
            println!("fingerprint: {}", managed.fingerprint_sha256()?);
            let mut targets = vec!["system", "firefox", "chromium-profile"];
            #[cfg(target_os = "macos")]
            targets.push("macos-user");
            for target in targets {
                println!("{target}: {}", TrustProbe::check_target(&managed, target));
            }
        }
        CertCommands::Generate { storage_path } => {
            let managed = managed_from_storage(storage_path)?;
            println!("generated CA at {}", managed.cert_path().display());
            println!("fingerprint: {}", managed.fingerprint_sha256()?);
        }
        CertCommands::Export {
            output,
            format,
            storage_path,
        } => {
            let managed = managed_from_storage(storage_path)?;
            let bytes = if format == "der" {
                product_os_security::certificates::Certificates::new_ca_from_file(
                    managed.cert_path().display().to_string(),
                    managed.key_path().display().to_string(),
                    Some(product_os_security::certificates::CertificatesFormat::Der),
                )
                .certificates
                .first()
                .cloned()
                .unwrap_or_default()
            } else {
                managed.cert_pem().to_vec()
            };
            match output {
                Some(path) => std::fs::write(&path, bytes)?,
                None => std::io::Write::write_all(&mut std::io::stdout(), &bytes)?,
            }
        }
        CertCommands::Install {
            cert: _,
            trust,
            target,
            storage_path,
        } => {
            let managed = managed_from_storage(storage_path)?;
            let parsed = NetworkProxyTrustTarget::parse(&target)
                .ok_or_else(|| format!("unknown target: {target}"))?;
            TrustInstaller::install(&managed, parsed, trust).map_err(|e| format!("{e}"))?;
            println!("installed CA to {target}");
        }
        CertCommands::Uninstall {
            cert: _,
            target,
            storage_path,
        } => {
            let managed = managed_from_storage(storage_path)?;
            let parsed = NetworkProxyTrustTarget::parse(&target)
                .ok_or_else(|| format!("unknown target: {target}"))?;
            TrustInstaller::uninstall(&managed, parsed).map_err(|e| format!("{e}"))?;
            println!("uninstalled CA from {target}");
        }
        CertCommands::Guide {
            config: _,
            storage_path,
        } => {
            let managed = managed_from_storage(storage_path)?;
            let trust = product_os_proxy::NetworkProxyCertificateAuthorityTrust {
                verify_on_startup: true,
                ..Default::default()
            };
            let targets = trust.targets.clone();
            let rt = tokio::runtime::Runtime::new()?;
            rt.block_on(run_cert_guide_wizard(
                &managed,
                &trust,
                &targets,
                "127.0.0.1",
                8080,
            ))
            .map_err(|e| format!("{e}"))?;
        }
    }
    Ok(())
}

fn handle_browser(command: BrowserCommands) -> Result<(), Box<dyn std::error::Error>> {
    match command {
        BrowserCommands::Setup {
            browser,
            proxy_host,
            proxy_port,
        } => match browser {
            BrowserKind::Chromium => {
                println!("Chromium / Chrome setup:");
                println!("  --proxy-server={proxy_host}:{proxy_port}");
                println!("  Install system CA trust via: pos-proxy cert install --trust");
                println!("  Or use managed CA without --ignore-certificate-errors when trustProxyCa is enabled");
            }
            BrowserKind::Firefox => {
                println!("Firefox setup:");
                println!("  network.proxy.type = 1");
                println!("  network.proxy.http = \"{proxy_host}\"");
                println!("  network.proxy.http_port = {proxy_port}");
                println!("  network.proxy.ssl = \"{proxy_host}\"");
                println!("  network.proxy.ssl_port = {proxy_port}");
                println!("  Import CA: pos-proxy cert install --target firefox --trust");
            }
        },
    }
    Ok(())
}