use std::sync::Arc; use async_trait::async_trait; use chrono::{Duration, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use super::Engine as SecretEngine; use crate::core::SealMechanism; use crate::crypto::KeyAlgorithm; use crate::error::{Result, VaultError}; use crate::storage::StorageBackend; /// Certificate metadata for storage #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertificateMetadata { pub name: String, pub certificate_pem: String, pub private_key_pem: Option, // Only for root CA and issued certs pub issued_at: String, pub expires_at: String, pub common_name: String, pub subject_alt_names: Vec, pub key_algorithm: String, pub revoked: bool, pub serial_number: String, } /// Revocation entry for CRL #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RevocationEntry { pub serial_number: String, pub revoked_at: String, pub reason: String, } /// PKI Secrets Engine for X.509 certificate management pub struct PkiEngine { storage: Arc, seal: Arc>, mount_path: String, root_ca_name: Arc>>, revocations: Arc>>, } impl PkiEngine { /// Create a new PKI engine instance pub fn new( storage: Arc, _crypto: Arc, seal: Arc>, mount_path: String, ) -> Self { Self { storage, seal, mount_path, root_ca_name: Arc::new(tokio::sync::Mutex::new(None)), revocations: Arc::new(tokio::sync::Mutex::new(Vec::new())), } } /// Get storage key for certificate fn cert_storage_key(&self, cert_name: &str) -> String { format!("{}certs/{}", self.mount_path, cert_name) } /// Generate a self-signed root CA certificate using OpenSSL pub async fn generate_root_ca( &self, name: &str, _key_type: KeyAlgorithm, ttl_days: i64, common_name: &str, ) -> Result { use openssl::asn1::Asn1Time; use openssl::bn::BigNum; use openssl::pkey::PKey; use openssl::rsa::Rsa; use openssl::x509::{X509Builder, X509Name}; // Generate RSA keypair (2048-bit) let rsa = Rsa::generate(2048) .map_err(|e| VaultError::crypto(format!("Failed to generate RSA key: {}", e)))?; let pkey = PKey::from_rsa(rsa) .map_err(|e| VaultError::crypto(format!("Failed to create PKey: {}", e)))?; // Create X.509 certificate builder let mut cert_builder = X509Builder::new() .map_err(|e| VaultError::crypto(format!("Failed to create X509Builder: {}", e)))?; // Set version (v3) cert_builder .set_version(2) .map_err(|e| VaultError::crypto(format!("Failed to set version: {}", e)))?; // Set serial number (use timestamp-based) let serial = Utc::now().timestamp() as u32; let mut serial_bn = BigNum::new() .map_err(|e| VaultError::crypto(format!("Failed to create BigNum: {}", e)))?; serial_bn .add_word(serial) .map_err(|e| VaultError::crypto(format!("Failed to add to BigNum: {}", e)))?; let serial_asn1 = openssl::asn1::Asn1Integer::from_bn(&serial_bn).map_err(|e| { VaultError::crypto(format!("Failed to convert BigNum to Asn1Integer: {}", e)) })?; cert_builder .set_serial_number(&serial_asn1) .map_err(|e| VaultError::crypto(format!("Failed to set serial number: {}", e)))?; // Set subject name let mut subject = X509Name::builder() .map_err(|e| VaultError::crypto(format!("Failed to create X509Name builder: {}", e)))?; subject .append_entry_by_text("CN", common_name) .map_err(|e| VaultError::crypto(format!("Failed to set CN: {}", e)))?; let subject_name = subject.build(); cert_builder .set_subject_name(&subject_name) .map_err(|e| VaultError::crypto(format!("Failed to set subject: {}", e)))?; // Set issuer (self-signed, same as subject) cert_builder .set_issuer_name(&subject_name) .map_err(|e| VaultError::crypto(format!("Failed to set issuer: {}", e)))?; // Set validity period let not_before = Asn1Time::days_from_now(0) .map_err(|e| VaultError::crypto(format!("Failed to set not_before: {}", e)))?; let not_after = Asn1Time::days_from_now(ttl_days as u32) .map_err(|e| VaultError::crypto(format!("Failed to set not_after: {}", e)))?; cert_builder .set_not_before(¬_before) .map_err(|e| VaultError::crypto(format!("Failed to set not_before: {}", e)))?; cert_builder .set_not_after(¬_after) .map_err(|e| VaultError::crypto(format!("Failed to set not_after: {}", e)))?; // Set public key cert_builder .set_pubkey(&pkey) .map_err(|e| VaultError::crypto(format!("Failed to set pubkey: {}", e)))?; // Self-sign the certificate cert_builder .sign(&pkey, openssl::hash::MessageDigest::sha256()) .map_err(|e| VaultError::crypto(format!("Failed to sign certificate: {}", e)))?; let cert = cert_builder.build(); // Convert certificate to PEM let cert_pem = String::from_utf8(cert.to_pem().map_err(|e| { VaultError::crypto(format!("Failed to convert cert to PEM: {}", e)) })?) .map_err(|e| VaultError::crypto(format!("Failed to convert PEM to string: {}", e)))?; // Convert private key to PEM let privkey_pem = String::from_utf8( pkey.private_key_to_pem_pkcs8() .map_err(|e| VaultError::crypto(format!("Failed to convert key to PEM: {}", e)))?, ) .map_err(|e| VaultError::crypto(format!("Failed to convert key PEM to string: {}", e)))?; let now = Utc::now(); let expires_at = now + Duration::days(ttl_days); let metadata = CertificateMetadata { name: name.to_string(), certificate_pem: cert_pem.clone(), private_key_pem: Some(privkey_pem), issued_at: now.to_rfc3339(), expires_at: expires_at.to_rfc3339(), common_name: common_name.to_string(), subject_alt_names: vec![], key_algorithm: "RSA-2048".to_string(), revoked: false, serial_number: serial.to_string(), }; // Store certificate let storage_key = self.cert_storage_key(name); let metadata_json = serde_json::to_vec(&metadata) .map_err(|e| VaultError::storage(format!("Failed to serialize metadata: {}", e)))?; self.storage .store_secret( &storage_key, &crate::storage::EncryptedData { ciphertext: metadata_json, nonce: vec![], algorithm: "aes-256-gcm".to_string(), }, ) .await .map_err(|e| VaultError::storage(e.to_string()))?; // Update root CA name let mut root_ca = self.root_ca_name.lock().await; *root_ca = Some(name.to_string()); Ok(metadata) } /// Issue a certificate signed by the root CA pub async fn issue_certificate( &self, name: &str, common_name: &str, subject_alt_names: Vec, ttl_days: i64, ) -> Result { use openssl::asn1::Asn1Time; use openssl::pkey::PKey; use openssl::rsa::Rsa; use openssl::x509::X509Builder; // Get root CA let root_ca_name = self.root_ca_name.lock().await; let ca_name = root_ca_name .as_ref() .ok_or_else(|| VaultError::crypto("Root CA not configured".to_string()))? .clone(); drop(root_ca_name); let root_cert_key = self.cert_storage_key(&ca_name); let root_cert_data = self .storage .get_secret(&root_cert_key) .await .map_err(|e| VaultError::storage(format!("Failed to get root CA: {}", e)))?; let root_metadata: CertificateMetadata = serde_json::from_slice(&root_cert_data.ciphertext) .map_err(|e| VaultError::storage(format!("Failed to parse root CA: {}", e)))?; // Generate RSA keypair for the new certificate let rsa = Rsa::generate(2048) .map_err(|e| VaultError::crypto(format!("Failed to generate RSA key: {}", e)))?; let pkey = PKey::from_rsa(rsa) .map_err(|e| VaultError::crypto(format!("Failed to create PKey: {}", e)))?; // Create certificate builder use openssl::bn::BigNum; use openssl::x509::X509Name; let mut cert_builder = X509Builder::new() .map_err(|e| VaultError::crypto(format!("Failed to create X509Builder: {}", e)))?; // Set version (v3) cert_builder .set_version(2) .map_err(|e| VaultError::crypto(format!("Failed to set version: {}", e)))?; // Set serial number let serial = Utc::now().timestamp() as u32; let mut serial_bn = BigNum::new() .map_err(|e| VaultError::crypto(format!("Failed to create BigNum: {}", e)))?; serial_bn .add_word(serial) .map_err(|e| VaultError::crypto(format!("Failed to add to BigNum: {}", e)))?; let serial_asn1 = openssl::asn1::Asn1Integer::from_bn(&serial_bn).map_err(|e| { VaultError::crypto(format!("Failed to convert BigNum to Asn1Integer: {}", e)) })?; cert_builder .set_serial_number(&serial_asn1) .map_err(|e| VaultError::crypto(format!("Failed to set serial number: {}", e)))?; // Set subject let mut subject = X509Name::builder() .map_err(|e| VaultError::crypto(format!("Failed to create X509Name builder: {}", e)))?; subject .append_entry_by_text("CN", common_name) .map_err(|e| VaultError::crypto(format!("Failed to set CN: {}", e)))?; let subject_name = subject.build(); cert_builder .set_subject_name(&subject_name) .map_err(|e| VaultError::crypto(format!("Failed to set subject: {}", e)))?; // Parse root CA certificate for issuer let root_cert_pem = root_metadata.certificate_pem.as_bytes(); let root_x509 = openssl::x509::X509::from_pem(root_cert_pem) .map_err(|e| VaultError::crypto(format!("Failed to parse root cert: {}", e)))?; let issuer = root_x509.issuer_name(); cert_builder .set_issuer_name(issuer) .map_err(|e| VaultError::crypto(format!("Failed to set issuer: {}", e)))?; // Set validity period let not_before = Asn1Time::days_from_now(0) .map_err(|e| VaultError::crypto(format!("Failed to set not_before: {}", e)))?; let not_after = Asn1Time::days_from_now(ttl_days as u32) .map_err(|e| VaultError::crypto(format!("Failed to set not_after: {}", e)))?; cert_builder .set_not_before(¬_before) .map_err(|e| VaultError::crypto(format!("Failed to set not_before: {}", e)))?; cert_builder .set_not_after(¬_after) .map_err(|e| VaultError::crypto(format!("Failed to set not_after: {}", e)))?; // Set public key cert_builder .set_pubkey(&pkey) .map_err(|e| VaultError::crypto(format!("Failed to set pubkey: {}", e)))?; // Sign with root CA private key let root_privkey_pem = root_metadata .private_key_pem .ok_or_else(|| VaultError::crypto("Root CA has no private key".to_string()))?; let root_privkey = openssl::pkey::PKey::private_key_from_pem(root_privkey_pem.as_bytes()) .map_err(|e| VaultError::crypto(format!("Failed to parse root CA key: {}", e)))?; cert_builder .sign(&root_privkey, openssl::hash::MessageDigest::sha256()) .map_err(|e| VaultError::crypto(format!("Failed to sign certificate: {}", e)))?; let cert = cert_builder.build(); // Convert to PEM let cert_pem = String::from_utf8(cert.to_pem().map_err(|e| { VaultError::crypto(format!("Failed to convert cert to PEM: {}", e)) })?) .map_err(|e| VaultError::crypto(format!("Failed to convert PEM to string: {}", e)))?; let privkey_pem = String::from_utf8( pkey.private_key_to_pem_pkcs8() .map_err(|e| VaultError::crypto(format!("Failed to convert key to PEM: {}", e)))?, ) .map_err(|e| VaultError::crypto(format!("Failed to convert key PEM to string: {}", e)))?; let now = Utc::now(); let expires_at = now + Duration::days(ttl_days); let metadata = CertificateMetadata { name: name.to_string(), certificate_pem: cert_pem.clone(), private_key_pem: Some(privkey_pem), issued_at: now.to_rfc3339(), expires_at: expires_at.to_rfc3339(), common_name: common_name.to_string(), subject_alt_names, key_algorithm: "RSA-2048".to_string(), revoked: false, serial_number: serial.to_string(), }; // Store certificate let storage_key = self.cert_storage_key(name); let metadata_json = serde_json::to_vec(&metadata) .map_err(|e| VaultError::storage(format!("Failed to serialize metadata: {}", e)))?; self.storage .store_secret( &storage_key, &crate::storage::EncryptedData { ciphertext: metadata_json, nonce: vec![], algorithm: "aes-256-gcm".to_string(), }, ) .await .map_err(|e| VaultError::storage(e.to_string()))?; Ok(metadata) } /// Revoke a certificate pub async fn revoke_certificate(&self, name: &str, reason: &str) -> Result<()> { let storage_key = self.cert_storage_key(name); // Get the certificate let cert_data = self .storage .get_secret(&storage_key) .await .map_err(|e| VaultError::storage(format!("Certificate not found: {}", e)))?; let mut metadata: CertificateMetadata = serde_json::from_slice(&cert_data.ciphertext) .map_err(|e| VaultError::storage(format!("Failed to parse certificate: {}", e)))?; // Mark as revoked metadata.revoked = true; // Update storage let metadata_json = serde_json::to_vec(&metadata) .map_err(|e| VaultError::storage(format!("Failed to serialize metadata: {}", e)))?; self.storage .store_secret( &storage_key, &crate::storage::EncryptedData { ciphertext: metadata_json, nonce: vec![], algorithm: "aes-256-gcm".to_string(), }, ) .await .map_err(|e| VaultError::storage(e.to_string()))?; // Add to revocation list let mut revocations = self.revocations.lock().await; revocations.push(RevocationEntry { serial_number: metadata.serial_number.clone(), revoked_at: Utc::now().to_rfc3339(), reason: reason.to_string(), }); Ok(()) } /// Read certificate metadata pub async fn read_certificate(&self, name: &str) -> Result> { let storage_key = self.cert_storage_key(name); match self.storage.get_secret(&storage_key).await { Ok(cert_data) => { let metadata: CertificateMetadata = serde_json::from_slice(&cert_data.ciphertext) .map_err(|e| { VaultError::storage(format!("Failed to parse certificate: {}", e)) })?; Ok(Some(metadata)) } Err(_) => Ok(None), } } } #[async_trait] impl SecretEngine for PkiEngine { fn name(&self) -> &str { "pki" } fn engine_type(&self) -> &str { "pki" } async fn read(&self, path: &str) -> Result> { if let Some(cert_name) = path.strip_prefix("certs/") { match self.read_certificate(cert_name).await? { Some(cert) => Ok(Some(json!({ "name": cert.name, "common_name": cert.common_name, "certificate": cert.certificate_pem, "issued_at": cert.issued_at, "expires_at": cert.expires_at, "serial_number": cert.serial_number, "revoked": cert.revoked, }))), None => Ok(None), } } else { Ok(None) } } async fn write(&self, path: &str, data: &Value) -> Result<()> { if let Some(cert_name) = path.strip_prefix("issue/") { let common_name = data .get("common_name") .and_then(|v| v.as_str()) .ok_or_else(|| VaultError::storage("Missing common_name".to_string()))?; let subject_alt_names: Vec = data .get("subject_alt_names") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); let ttl_days = data.get("ttl_days").and_then(|v| v.as_u64()).unwrap_or(365) as i64; let _cert = self .issue_certificate(cert_name, common_name, subject_alt_names, ttl_days) .await?; } else if let Some(ca_name) = path.strip_prefix("root/") { let common_name = data .get("common_name") .and_then(|v| v.as_str()) .ok_or_else(|| VaultError::storage("Missing common_name".to_string()))?; let ttl_days = data .get("ttl_days") .and_then(|v| v.as_u64()) .unwrap_or(3650) as i64; let _cert = self .generate_root_ca( ca_name, crate::crypto::KeyAlgorithm::Rsa2048, ttl_days, common_name, ) .await?; } Ok(()) } async fn delete(&self, path: &str) -> Result<()> { if let Some(cert_name) = path.strip_prefix("certs/") { let reason = "Manual revocation".to_string(); self.revoke_certificate(cert_name, &reason).await?; } Ok(()) } async fn list(&self, prefix: &str) -> Result> { // List all certificates with given prefix from storage let storage_prefix = format!("{}certs/", self.mount_path); let all_certs = self .storage .list_secrets(&storage_prefix) .await .map_err(|e| VaultError::storage(e.to_string()))?; let filtered: Vec = all_certs .iter() .filter(|cert| cert.starts_with(prefix)) .map(|cert| cert.to_string()) .collect(); Ok(filtered) } async fn health_check(&self) -> Result<()> { self.storage .health_check() .await .map_err(|e| VaultError::storage(e.to_string()))?; let seal = self.seal.lock().await; if seal.is_sealed() { return Err(VaultError::crypto("Vault is sealed".to_string())); } Ok(()) } } #[cfg(test)] mod tests { use tempfile::TempDir; use super::*; use crate::config::{FilesystemStorageConfig, SealConfig, ShamirSealConfig, StorageConfig}; use crate::crypto::CryptoRegistry; use crate::storage::StorageRegistry; async fn setup_engine() -> Result<(PkiEngine, TempDir)> { let temp_dir = TempDir::new().map_err(|e| VaultError::storage(e.to_string()))?; let fs_config = FilesystemStorageConfig { path: temp_dir.path().to_path_buf(), }; let storage_config = StorageConfig { backend: "filesystem".to_string(), filesystem: fs_config, surrealdb: Default::default(), etcd: Default::default(), postgresql: Default::default(), }; let storage = StorageRegistry::create(&storage_config).await?; let crypto = CryptoRegistry::create("openssl", &Default::default())?; let seal_config = SealConfig { seal_type: "shamir".to_string(), shamir: ShamirSealConfig { threshold: 2, shares: 3, }, auto_unseal: Default::default(), }; let mut seal = crate::core::SealMechanism::new(&seal_config)?; let _init_result = seal.init(crypto.as_ref(), storage.as_ref()).await?; let seal_arc = Arc::new(tokio::sync::Mutex::new(seal)); let engine = PkiEngine::new(storage, crypto.clone(), seal_arc, "pki/".to_string()); Ok((engine, temp_dir)) } #[tokio::test] async fn test_pki_engine_creation() -> Result<()> { let (_engine, _temp) = setup_engine().await?; Ok(()) } #[tokio::test] async fn test_generate_root_ca() -> Result<()> { let (engine, _temp) = setup_engine().await?; let cert = engine .generate_root_ca("root-ca", KeyAlgorithm::Rsa2048, 3650, "example.com") .await?; assert_eq!(cert.name, "root-ca"); assert_eq!(cert.common_name, "example.com"); assert!(cert.certificate_pem.contains("BEGIN CERTIFICATE")); assert!(cert.private_key_pem.is_some()); Ok(()) } #[tokio::test] async fn test_issue_certificate() -> Result<()> { let (engine, _temp) = setup_engine().await?; // Generate root CA first let _root_cert = engine .generate_root_ca("root-ca", KeyAlgorithm::Rsa2048, 3650, "example.com") .await?; // Issue a certificate let cert = engine .issue_certificate( "server-cert", "server.example.com", vec!["www.example.com".to_string()], 365, ) .await?; assert_eq!(cert.name, "server-cert"); assert_eq!(cert.common_name, "server.example.com"); assert_eq!(cert.subject_alt_names, vec!["www.example.com"]); assert!(cert.certificate_pem.contains("BEGIN CERTIFICATE")); assert!(!cert.revoked); Ok(()) } #[tokio::test] async fn test_revoke_certificate() -> Result<()> { let (engine, _temp) = setup_engine().await?; // Generate root CA and issue cert let _root_cert = engine .generate_root_ca("root-ca", KeyAlgorithm::Rsa2048, 3650, "example.com") .await?; let _cert = engine .issue_certificate("server-cert", "server.example.com", vec![], 365) .await?; // Revoke the certificate engine .revoke_certificate("server-cert", "Test revocation") .await?; // Read it back and verify revocation let revoked = engine.read_certificate("server-cert").await?; assert!(revoked.is_some()); assert!(revoked.unwrap().revoked); Ok(()) } #[tokio::test] async fn test_read_certificate() -> Result<()> { let (engine, _temp) = setup_engine().await?; let root = engine .generate_root_ca("root-ca", KeyAlgorithm::Rsa2048, 3650, "example.com") .await?; let read_result = engine.read_certificate("root-ca").await?; assert!(read_result.is_some()); assert_eq!(read_result.unwrap().name, root.name); Ok(()) } #[tokio::test] async fn test_pki_health_check() -> Result<()> { let (engine, _temp) = setup_engine().await?; engine.health_check().await?; Ok(()) } }