mannequin/src/pki/mod.rs
use crate::config::MannequinConfig;
use cert_helper::certificate::{
CertBuilder, Certificate, HashAlg, KeyType, Usage, UseesBuilderFields, X509Parts,
};
use std::sync::{Mutex, RwLock};
/// PKI service: manages a test CA and issues certificates on demand.
pub struct PkiService {
ca: RwLock<Certificate>,
issued: Mutex<Vec<IssuedCert>>,
}
pub struct IssuedCert {
pub common_name: String,
pub cert_pem: String,
pub key_pem: String,
}
fn create_ca() -> anyhow::Result<Certificate> {
let ca_builder = CertBuilder::new()
.common_name("Holger Mannequin CA")
.country_name("SE")
.organization("Holger Test")
.is_ca(true)
.key_type(KeyType::P256)
.signature_alg(HashAlg::SHA256)
.key_usage([Usage::certsign, Usage::crlsign].into_iter().collect());
ca_builder
.build_and_self_sign()
.map_err(|e| anyhow::anyhow!("{}", e))
}
fn load_ca_from_config(config: &MannequinConfig) -> anyhow::Result<Certificate> {
let cert_pem = config.ca_cert.as_ref().unwrap().resolve()?;
let key_pem = config.ca_key.as_ref().unwrap().resolve()?;
// Write to temp files (cert-helper API requires file paths)
let cert_tmp = tempfile::NamedTempFile::new()?;
let key_tmp = tempfile::NamedTempFile::new()?;
std::fs::write(cert_tmp.path(), &cert_pem)?;
std::fs::write(key_tmp.path(), &key_pem)?;
Certificate::load_cert_and_key(cert_tmp.path(), key_tmp.path())
.map_err(|e| anyhow::anyhow!("Failed to load CA from config: {}", e))
}
impl PkiService {
/// Create with a freshly generated CA.
pub fn new() -> anyhow::Result<Self> {
let ca = create_ca()?;
Ok(Self {
ca: RwLock::new(ca),
issued: Mutex::new(Vec::new()),
})
}
/// Create from config: use supplied certs if present, otherwise generate.
pub fn from_config(config: &MannequinConfig) -> anyhow::Result<Self> {
let ca = if config.has_ca() {
load_ca_from_config(config)?
} else {
create_ca()?
};
Ok(Self {
ca: RwLock::new(ca),
issued: Mutex::new(Vec::new()),
})
}
/// Regenerate the CA and clear all issued certs.
pub fn regenerate(&self) -> anyhow::Result<()> {
let new_ca = create_ca()?;
*self.ca.write().unwrap() = new_ca;
self.issued.lock().unwrap().clear();
Ok(())
}
/// Get CA certificate in PEM format.
pub fn ca_cert_pem(&self) -> String {
let ca = self.ca.read().unwrap();
String::from_utf8_lossy(&ca.get_pem().unwrap_or_default()).to_string()
}
/// Issue a server certificate signed by the CA.
pub fn issue_server_cert(&self, cn: &str, sans: Vec<&str>) -> anyhow::Result<IssuedCert> {
let ca = self.ca.read().unwrap();
let builder = CertBuilder::new()
.common_name(cn)
.key_type(KeyType::P256)
.signature_alg(HashAlg::SHA256)
.alternative_names(sans)
.key_usage(
[Usage::serverauth, Usage::encipherment]
.into_iter()
.collect(),
);
let cert = builder
.build_and_sign(&ca)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let issued = IssuedCert {
common_name: cn.to_string(),
cert_pem: String::from_utf8_lossy(
&cert.get_pem().map_err(|e| anyhow::anyhow!("{}", e))?,
)
.to_string(),
key_pem: String::from_utf8_lossy(
&cert
.get_private_key()
.map_err(|e| anyhow::anyhow!("{}", e))?,
)
.to_string(),
};
self.issued.lock().unwrap().push(IssuedCert {
common_name: cn.to_string(),
cert_pem: issued.cert_pem.clone(),
key_pem: issued.key_pem.clone(),
});
Ok(issued)
}
/// Issue a client certificate signed by the CA.
pub fn issue_client_cert(&self, cn: &str) -> anyhow::Result<IssuedCert> {
let ca = self.ca.read().unwrap();
let builder = CertBuilder::new()
.common_name(cn)
.key_type(KeyType::P256)
.signature_alg(HashAlg::SHA256)
.key_usage([Usage::clientauth].into_iter().collect());
let cert = builder
.build_and_sign(&ca)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let issued = IssuedCert {
common_name: cn.to_string(),
cert_pem: String::from_utf8_lossy(
&cert.get_pem().map_err(|e| anyhow::anyhow!("{}", e))?,
)
.to_string(),
key_pem: String::from_utf8_lossy(
&cert
.get_private_key()
.map_err(|e| anyhow::anyhow!("{}", e))?,
)
.to_string(),
};
self.issued.lock().unwrap().push(IssuedCert {
common_name: cn.to_string(),
cert_pem: issued.cert_pem.clone(),
key_pem: issued.key_pem.clone(),
});
Ok(issued)
}
/// List all issued certificates.
pub fn list_issued(&self) -> Vec<String> {
self.issued
.lock()
.unwrap()
.iter()
.map(|c| c.common_name.clone())
.collect()
}
}