use crate::{Error, Result};
use colored::*;
use rcgen::{
BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair,
RsaKeySize,
};
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use time::{Duration, OffsetDateTime};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
const ROOT_CERT_FILE: &str = "rootCA.pem";
const ROOT_KEY_FILE: &str = "rootCA-key.pem";
pub fn get_caroot() -> Result<String> {
let caroot = get_caroot_path()?;
Ok(caroot.display().to_string())
}
fn get_caroot_path() -> Result<PathBuf> {
if let Ok(caroot) = std::env::var("CAROOT") {
return Ok(PathBuf::from(caroot));
}
#[cfg(target_os = "macos")]
{
if let Some(home) = dirs::home_dir() {
return Ok(home
.join("Library")
.join("Application Support")
.join("fastcert"));
}
}
#[cfg(target_os = "windows")]
{
if let Some(local_app_data) = dirs::data_local_dir() {
return Ok(local_app_data.join("fastcert"));
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
if let Some(data_dir) = dirs::data_dir() {
return Ok(data_dir.join("fastcert"));
}
}
Err(Error::Certificate(
"Could not determine CAROOT directory".to_string(),
))
}
pub fn install() -> Result<()> {
let caroot = get_caroot_path()?;
let mut ca = CertificateAuthority::new(caroot);
ca.load_or_create()?;
#[cfg(target_os = "macos")]
{
crate::truststore::install_macos(&ca.cert_path())?;
}
#[cfg(target_os = "linux")]
{
crate::truststore::install_linux(&ca.cert_path())?;
}
#[cfg(target_os = "windows")]
{
crate::truststore::install_windows(&ca.cert_path())?;
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
println!("Note: System trust store installation not yet implemented for this platform.");
println!(
"You may need to manually import the CA certificate from: {}",
ca.cert_path().display()
);
}
Ok(())
}
pub fn uninstall() -> Result<()> {
let caroot = get_caroot_path()?;
let ca = CertificateAuthority::new(caroot);
if !ca.cert_exists() {
println!("No CA certificate found to uninstall.");
return Ok(());
}
#[cfg(target_os = "macos")]
{
crate::truststore::uninstall_macos(&ca.cert_path())?;
}
#[cfg(target_os = "linux")]
{
crate::truststore::uninstall_linux(&ca.cert_path())?;
}
#[cfg(target_os = "windows")]
{
crate::truststore::uninstall_windows(&ca.cert_path())?;
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
println!("Note: System trust store uninstallation not yet implemented for this platform.");
println!(
"You may need to manually remove the CA certificate from your system trust store."
);
}
Ok(())
}
pub fn get_ca() -> Result<CertificateAuthority> {
let caroot = get_caroot_path()?;
Ok(CertificateAuthority::new(caroot))
}
pub struct CertificateAuthority {
root_path: PathBuf,
cert: Option<Certificate>,
cert_pem: Option<String>,
key_pem: Option<String>,
}
impl CertificateAuthority {
pub fn new(root_path: PathBuf) -> Self {
Self {
root_path,
cert: None,
cert_pem: None,
key_pem: None,
}
}
pub fn root_path(&self) -> &Path {
&self.root_path
}
pub fn init(&self) -> Result<()> {
if !self.root_path.exists() {
fs::create_dir_all(&self.root_path)?;
}
Ok(())
}
pub fn cert_path(&self) -> PathBuf {
self.root_path.join(ROOT_CERT_FILE)
}
pub fn key_path(&self) -> PathBuf {
self.root_path.join(ROOT_KEY_FILE)
}
pub fn cert_exists(&self) -> bool {
self.cert_path().exists()
}
pub fn key_exists(&self) -> bool {
self.key_path().exists()
}
pub fn create_ca(&mut self) -> Result<()> {
eprintln!("{}", "Generating CA certificate...".cyan());
let key_pair = KeyPair::generate_rsa_for(&rcgen::PKCS_RSA_SHA256, RsaKeySize::_3072)
.map_err(|e| Error::Certificate(format!("Failed to generate CA key pair: {}", e)))?;
let params = create_ca_params()
.map_err(|e| Error::Certificate(format!("Failed to create CA parameters: {}", e)))?;
let cert = params
.self_signed(&key_pair)
.map_err(|e| Error::Certificate(format!("Failed to generate CA certificate: {}", e)))?;
let cert_pem = cert.pem();
let key_pem = key_pair.serialize_pem();
self.cert = Some(cert);
self.cert_pem = Some(cert_pem);
self.key_pem = Some(key_pem);
Ok(())
}
pub fn save(&self) -> Result<()> {
let cert_pem = self
.cert_pem
.as_ref()
.ok_or_else(|| Error::Certificate("No certificate available to save".to_string()))?;
let key_pem = self
.key_pem
.as_ref()
.ok_or_else(|| Error::Certificate("No private key available to save".to_string()))?;
let cert_path = self.cert_path();
let mut file = File::create(&cert_path).map_err(|e| {
Error::Certificate(format!(
"Failed to create certificate file at {:?}: {}",
cert_path, e
))
})?;
file.write_all(cert_pem.as_bytes()).map_err(|e| {
Error::Certificate(format!(
"Failed to write certificate to {:?}: {}",
cert_path, e
))
})?;
#[cfg(unix)]
fs::set_permissions(&cert_path, fs::Permissions::from_mode(0o644)).map_err(|e| {
Error::Certificate(format!(
"Failed to set permissions on {:?}: {}",
cert_path, e
))
})?;
let key_path = self.key_path();
let mut file = File::create(&key_path).map_err(|e| {
Error::Certificate(format!(
"Failed to create key file at {:?}: {}",
key_path, e
))
})?;
file.write_all(key_pem.as_bytes()).map_err(|e| {
Error::Certificate(format!("Failed to write key to {:?}: {}", key_path, e))
})?;
#[cfg(unix)]
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o400)).map_err(|e| {
Error::Certificate(format!(
"Failed to set permissions on {:?}: {}",
key_path, e
))
})?;
Ok(())
}
pub fn load(&mut self) -> Result<()> {
let cert_path = self.cert_path();
if !cert_path.exists() {
return Err(Error::Certificate("CA certificate not found".to_string()));
}
let key_path = self.key_path();
if !key_path.exists() {
return Err(Error::Certificate("CA private key not found".to_string()));
}
let cert_pem = fs::read_to_string(&cert_path)?;
let key_pem = fs::read_to_string(&key_path)?;
self.cert_pem = Some(cert_pem);
self.key_pem = Some(key_pem);
Ok(())
}
pub fn load_or_create(&mut self) -> Result<()> {
self.init()?;
if self.cert_exists() {
self.load()?;
} else {
self.create_ca()?;
self.save()?;
println!("{}", "Created a new local CA".green().bold());
}
Ok(())
}
pub fn unique_name(&self) -> Result<String> {
let cert_pem = fs::read_to_string(self.cert_path())?;
let pem_data = pem::parse(&cert_pem)
.map_err(|e| Error::Certificate(format!("Failed to parse PEM: {}", e)))?;
let cert = x509_parser::parse_x509_certificate(pem_data.contents())
.map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?
.1;
let serial = cert.serial.to_str_radix(10);
Ok(format!("fastcert development CA {}", serial))
}
pub fn get_serial_number(&self) -> Result<String> {
let cert_pem = fs::read_to_string(self.cert_path())?;
let pem_data = pem::parse(&cert_pem)
.map_err(|e| Error::Certificate(format!("Failed to parse PEM: {}", e)))?;
let cert = x509_parser::parse_x509_certificate(pem_data.contents())
.map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?
.1;
Ok(cert.serial.to_str_radix(16))
}
}
pub fn is_serial_unique(serial: &str, ca_path: &Path) -> Result<bool> {
if !ca_path.exists() {
return Ok(true);
}
let ca = CertificateAuthority::new(ca_path.to_path_buf());
if !ca.cert_exists() {
return Ok(true);
}
let existing_serial = ca.get_serial_number()?;
Ok(existing_serial != serial)
}
fn get_user_and_hostname() -> String {
let username = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown".to_string());
let hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "unknown".to_string());
format!("{}@{}", username, hostname)
}
fn create_ca_params() -> Result<CertificateParams> {
let user_host = get_user_and_hostname();
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::OrganizationName, "fastcert development CA");
dn.push(DnType::OrganizationalUnitName, &user_host);
dn.push(DnType::CommonName, format!("fastcert {}", user_host));
params.distinguished_name = dn;
let now = OffsetDateTime::now_utc();
params.not_before = now;
params.not_after = now + Duration::days(3650);
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
rcgen::KeyUsagePurpose::KeyCertSign,
rcgen::KeyUsagePurpose::CrlSign,
];
Ok(params)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_ca_paths() {
let temp_dir = std::env::temp_dir().join("fastcert_test_ca");
let ca = CertificateAuthority::new(temp_dir.clone());
assert_eq!(ca.root_path(), temp_dir.as_path());
assert_eq!(ca.cert_path(), temp_dir.join("rootCA.pem"));
assert_eq!(ca.key_path(), temp_dir.join("rootCA-key.pem"));
let _ = fs::remove_dir_all(temp_dir);
}
#[test]
fn test_ca_init() {
let temp_dir = std::env::temp_dir().join("fastcert_test_init");
let _ = fs::remove_dir_all(&temp_dir);
let ca = CertificateAuthority::new(temp_dir.clone());
assert!(!temp_dir.exists());
ca.init().unwrap();
assert!(temp_dir.exists());
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_ca_lifecycle() {
let temp_dir = std::env::temp_dir().join("fastcert_test_lifecycle");
let _ = fs::remove_dir_all(&temp_dir);
let mut ca = CertificateAuthority::new(temp_dir.clone());
ca.load_or_create().unwrap();
assert!(ca.cert_exists());
assert!(ca.key_exists());
let mut ca2 = CertificateAuthority::new(temp_dir.clone());
ca2.load_or_create().unwrap();
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_ca_install_integration() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
unsafe {
std::env::set_var("CAROOT", temp_dir.path().to_str().unwrap());
}
let mut ca = CertificateAuthority::new(temp_dir.path().to_path_buf());
ca.load_or_create().unwrap();
assert!(ca.cert_exists(), "CA certificate should be created");
assert!(ca.key_exists(), "CA key should be created");
let cert_pem = fs::read_to_string(ca.cert_path()).unwrap();
assert!(
cert_pem.contains("BEGIN CERTIFICATE"),
"Certificate should be in PEM format"
);
unsafe {
std::env::remove_var("CAROOT");
}
}
#[test]
fn test_ca_uninstall_integration() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let mut ca = CertificateAuthority::new(temp_dir.path().to_path_buf());
ca.load_or_create().unwrap();
assert!(
ca.cert_exists(),
"CA certificate should exist before uninstall"
);
assert!(ca.key_exists(), "CA key should exist before uninstall");
let cert_exists_after = ca.cert_exists();
assert!(
cert_exists_after,
"Certificate should still exist after uninstall call"
);
}
#[test]
fn test_ca_serial_uniqueness() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let mut ca = CertificateAuthority::new(temp_dir.path().to_path_buf());
ca.load_or_create().unwrap();
let serial = ca.get_serial_number().unwrap();
assert!(!serial.is_empty(), "Serial number should not be empty");
assert!(!is_serial_unique(&serial, temp_dir.path()).unwrap());
let temp_dir2 = TempDir::new().unwrap();
let mut ca2 = CertificateAuthority::new(temp_dir2.path().to_path_buf());
ca2.load_or_create().unwrap();
let serial2 = ca2.get_serial_number().unwrap();
assert_ne!(
serial, serial2,
"Different CAs should have different serials"
);
assert!(is_serial_unique(&serial, temp_dir2.path()).unwrap());
}
}