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 {
Cert {
#[command(subcommand)]
command: CertCommands,
},
Browser {
#[command(subcommand)]
command: BrowserCommands,
},
}
#[derive(Subcommand)]
enum CertCommands {
Status {
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
cert: Option<PathBuf>,
#[arg(long)]
storage_path: Option<PathBuf>,
},
Generate {
#[arg(long)]
storage_path: Option<PathBuf>,
},
Export {
#[arg(long)]
output: Option<PathBuf>,
#[arg(long, default_value = "pem")]
format: String,
#[arg(long)]
storage_path: Option<PathBuf>,
},
Install {
#[arg(long)]
cert: Option<PathBuf>,
#[arg(long)]
trust: bool,
#[arg(long, default_value = "system")]
target: String,
#[arg(long)]
storage_path: Option<PathBuf>,
},
Uninstall {
#[arg(long)]
cert: Option<PathBuf>,
#[arg(long, default_value = "system")]
target: String,
#[arg(long)]
storage_path: Option<PathBuf>,
},
Guide {
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
storage_path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum BrowserCommands {
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(())
}