use crate::domain::error::Result;
use anyhow::Context;
use rcgen::{Certificate, CertificateParams, generate_simple_self_signed};
use std::path::PathBuf;
use time::{Duration, OffsetDateTime};
use tracing::{debug, info};
pub struct CertificateAuthority {
certificate: Certificate,
pem_cert: String,
pem_key: String,
}
impl CertificateAuthority {
pub fn new_or_load(cert_path: &PathBuf, key_path: &PathBuf) -> Result<Self> {
if cert_path.exists() && key_path.exists() {
debug!("Loading CA certificate from disk");
Self::load(cert_path, key_path)
} else {
debug!("Generating new CA certificate");
Self::generate()
}
}
pub fn generate() -> Result<Self> {
let subject_alt_names = vec!["pupoxide-ca".to_string()];
let certificate = generate_simple_self_signed(subject_alt_names)
.context("Failed to generate CA certificate")?;
let pem_cert = certificate
.serialize_pem()
.context("Failed to serialize CA certificate")?;
let pem_key = certificate.serialize_private_key_pem();
info!("Generated new CA certificate");
Ok(Self {
certificate,
pem_cert,
pem_key,
})
}
pub fn load(cert_path: &PathBuf, key_path: &PathBuf) -> Result<Self> {
let pem_cert =
std::fs::read_to_string(cert_path).context("Failed to read CA certificate")?;
let pem_key = std::fs::read_to_string(key_path).context("Failed to read CA private key")?;
let subject_alt_names = vec!["pupoxide-ca".to_string()];
let certificate = generate_simple_self_signed(subject_alt_names)
.context("Failed to regenerate CA certificate from key")?;
Ok(Self {
certificate,
pem_cert,
pem_key,
})
}
pub fn save(&self, cert_path: &PathBuf, key_path: &PathBuf) -> Result<()> {
std::fs::write(cert_path, &self.pem_cert).context("Failed to write CA certificate")?;
std::fs::write(key_path, &self.pem_key).context("Failed to write CA private key")?;
info!("Saved CA certificate to {:?}", cert_path);
Ok(())
}
pub fn cert_pem(&self) -> &str {
&self.pem_cert
}
pub fn key_pem(&self) -> &str {
&self.pem_key
}
pub fn sign_csr(&self, node_id: &str, days_valid: u32) -> Result<String> {
let subject_alt_names = vec![node_id.to_string()];
let mut agent_params = CertificateParams::new(subject_alt_names);
agent_params.is_ca = rcgen::IsCa::NoCa;
let now = OffsetDateTime::now_utc();
agent_params.not_before = now;
agent_params.not_after = now + Duration::days(days_valid as i64);
let agent_cert =
Certificate::from_params(agent_params).context("Failed to create agent certificate")?;
let signed_pem = agent_cert
.serialize_pem_with_signer(&self.certificate)
.context("Failed to serialize agent certificate with CA signature")?;
debug!("Signed certificate for {}", node_id);
Ok(signed_pem)
}
pub fn sign_csr_with_client_cert(
&self,
node_id: &str,
agent_cert_pem: &str,
days_valid: u32,
) -> Result<String> {
let _ = agent_cert_pem;
self.sign_csr(node_id, days_valid)
}
}
pub struct AgentCertificateRequest {
pub node_id: String,
pub csr_pem: String,
}
impl AgentCertificateRequest {
pub fn generate(node_id: &str) -> Result<(Self, String, String)> {
let subject_alt_names = vec![node_id.to_string()];
let private_key = generate_simple_self_signed(subject_alt_names)
.context("Failed to generate agent private key")?;
let csr_pem = private_key
.serialize_request_pem()
.context("Failed to serialize CSR")?;
let key_pem = private_key.serialize_private_key_pem();
let cert_pem = private_key
.serialize_pem()
.context("Failed to serialize self-signed certificate")?;
let req = AgentCertificateRequest {
node_id: node_id.to_string(),
csr_pem,
};
debug!("Generated CSR for agent {}", node_id);
Ok((req, key_pem, cert_pem))
}
}
pub fn save_agent_certificate(
cert_pem: &str,
key_pem: &str,
cert_path: &PathBuf,
key_path: &PathBuf,
) -> Result<()> {
std::fs::write(cert_path, cert_pem).context("Failed to write agent certificate")?;
std::fs::write(key_path, key_pem).context("Failed to write agent private key")?;
info!("Saved agent certificate to {:?}", cert_path);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ca_generation() {
let ca = CertificateAuthority::generate().expect("Failed to generate CA");
assert!(!ca.cert_pem().is_empty());
assert!(!ca.key_pem().is_empty());
}
#[test]
fn test_csr_generation() {
let (csr, key_pem, cert_pem) =
AgentCertificateRequest::generate("test-agent").expect("Failed to generate CSR");
assert!(!csr.csr_pem.is_empty());
assert!(!key_pem.is_empty());
assert!(!cert_pem.is_empty());
}
#[test]
fn test_sign_csr() {
let ca = CertificateAuthority::generate().expect("Failed to generate CA");
let signed = ca.sign_csr("test-agent", 365).expect("Failed to sign CSR");
assert!(!signed.is_empty());
assert!(signed.contains("BEGIN CERTIFICATE"));
}
}