Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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()
    }
}