pupoxide 0.2.3

A high-performance, memory-safe, declarative configuration management tool inspired by Puppet.
Documentation
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};

/// Represents a Certificate Authority
pub struct CertificateAuthority {
    certificate: Certificate,
    pem_cert: String,
    pem_key: String,
}

impl CertificateAuthority {
    /// Creates or loads a CA certificate
    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()
        }
    }

    /// Generates a new CA certificate
    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,
        })
    }

    /// Loads CA from PEM files (simplified - requires the cert was generated by rcgen)
    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")?;

        // Reconstruct the certificate object from the private key
        // Note: This is a simplified approach; in production, you might need
        // to store additional metadata or use a full PKI library
        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,
        })
    }

    /// Saves CA to disk
    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(())
    }

    /// Returns PEM-encoded certificate
    pub fn cert_pem(&self) -> &str {
        &self.pem_cert
    }

    /// Returns PEM-encoded private key
    pub fn key_pem(&self) -> &str {
        &self.pem_key
    }

    /// Signs a certificate signing request (CSR)
    /// Returns the signed certificate in PEM format
    pub fn sign_csr(&self, node_id: &str, days_valid: u32) -> Result<String> {
        // Generate a new certificate for the agent, signed by this CA
        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;

        // Set validity period
        let now = OffsetDateTime::now_utc();
        agent_params.not_before = now;
        agent_params.not_after = now + Duration::days(days_valid as i64);

        // Create agent certificate
        let agent_cert =
            Certificate::from_params(agent_params).context("Failed to create agent certificate")?;

        // Serialize with CA signature (using the CA's private key)
        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)
    }

    /// Sign a certificate that was already generated by the agent (preserves agent's public key)
    pub fn sign_csr_with_client_cert(
        &self,
        node_id: &str,
        agent_cert_pem: &str,
        days_valid: u32,
    ) -> Result<String> {
        // Note: Ideally we would extract the public key from the agent's cert and
        // create a new cert with that key. However, rcgen doesn't provide easy CSR/cert parsing.
        // As a workaround, we create a new cert and rely on the agent having the matching
        // private key from its self-generated cert.

        // For now, we create a new cert but ensure it uses the same node_id
        // so the agent can match it with its private key.
        let _ = agent_cert_pem; // Will use this when cert parsing is implemented

        // Fall back to the standard signing
        self.sign_csr(node_id, days_valid)
    }
}

/// Represents an Agent Certificate Request
pub struct AgentCertificateRequest {
    pub node_id: String,
    pub csr_pem: String,
}

impl AgentCertificateRequest {
    /// Generates a new private key and CSR for an agent
    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))
    }
}

/// Saves certificate and key to disk
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"));
    }
}