2025-12-29 05:04:53 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2025-12-22 21:34:01 +00:00
|
|
|
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<String>, // Only for root CA and issued certs
|
|
|
|
|
pub issued_at: String,
|
|
|
|
|
pub expires_at: String,
|
|
|
|
|
pub common_name: String,
|
|
|
|
|
pub subject_alt_names: Vec<String>,
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 10:45:44 +00:00
|
|
|
/// PKI Secrets Engine for X.509 and PQC certificate management
|
2025-12-22 21:34:01 +00:00
|
|
|
pub struct PkiEngine {
|
|
|
|
|
storage: Arc<dyn StorageBackend>,
|
2026-01-21 10:45:44 +00:00
|
|
|
crypto: Arc<dyn crate::crypto::CryptoBackend>,
|
2025-12-22 21:34:01 +00:00
|
|
|
seal: Arc<tokio::sync::Mutex<SealMechanism>>,
|
|
|
|
|
mount_path: String,
|
|
|
|
|
root_ca_name: Arc<tokio::sync::Mutex<Option<String>>>,
|
|
|
|
|
revocations: Arc<tokio::sync::Mutex<Vec<RevocationEntry>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PkiEngine {
|
|
|
|
|
/// Create a new PKI engine instance
|
|
|
|
|
pub fn new(
|
|
|
|
|
storage: Arc<dyn StorageBackend>,
|
2026-01-21 10:45:44 +00:00
|
|
|
crypto: Arc<dyn crate::crypto::CryptoBackend>,
|
2025-12-22 21:34:01 +00:00
|
|
|
seal: Arc<tokio::sync::Mutex<SealMechanism>>,
|
|
|
|
|
mount_path: String,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
storage,
|
2026-01-21 10:45:44 +00:00
|
|
|
crypto,
|
2025-12-22 21:34:01 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 10:45:44 +00:00
|
|
|
/// Generate a self-signed root CA certificate
|
|
|
|
|
/// Supports classical (RSA/ECDSA via OpenSSL X.509) and post-quantum
|
|
|
|
|
/// (ML-DSA-65 via JSON)
|
2025-12-22 21:34:01 +00:00
|
|
|
pub async fn generate_root_ca(
|
|
|
|
|
&self,
|
|
|
|
|
name: &str,
|
2026-01-21 10:45:44 +00:00
|
|
|
key_type: KeyAlgorithm,
|
2025-12-22 21:34:01 +00:00
|
|
|
ttl_days: i64,
|
|
|
|
|
common_name: &str,
|
|
|
|
|
) -> Result<CertificateMetadata> {
|
2026-01-21 10:45:44 +00:00
|
|
|
// Delegate to PQC implementation if ML-DSA-65 requested
|
|
|
|
|
#[cfg(feature = "pqc")]
|
|
|
|
|
if key_type == KeyAlgorithm::MlDsa65 {
|
|
|
|
|
return self.generate_pqc_root_ca(name, ttl_days, common_name).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Classical certificate generation with OpenSSL X.509
|
2025-12-22 21:34:01 +00:00
|
|
|
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()))?;
|
2026-01-21 10:45:44 +00:00
|
|
|
|
|
|
|
|
// Update root CA name
|
|
|
|
|
let mut root_ca = self.root_ca_name.lock().await;
|
|
|
|
|
*root_ca = Some(name.to_string());
|
|
|
|
|
|
|
|
|
|
Ok(metadata)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate a post-quantum root CA certificate with ML-DSA-65
|
|
|
|
|
/// Uses SecretumVault-specific JSON format (not X.509 PEM) since ML-DSA is
|
|
|
|
|
/// not yet in X.509 standard
|
|
|
|
|
#[cfg(feature = "pqc")]
|
|
|
|
|
async fn generate_pqc_root_ca(
|
|
|
|
|
&self,
|
|
|
|
|
name: &str,
|
|
|
|
|
ttl_days: i64,
|
|
|
|
|
common_name: &str,
|
|
|
|
|
) -> Result<CertificateMetadata> {
|
|
|
|
|
// Generate ML-DSA-65 keypair
|
|
|
|
|
let keypair = self
|
|
|
|
|
.crypto
|
|
|
|
|
.generate_keypair(KeyAlgorithm::MlDsa65)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| VaultError::crypto(format!("ML-DSA-65 key generation failed: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
let expires_at = now + Duration::days(ttl_days);
|
|
|
|
|
let serial = now.timestamp() as u32;
|
|
|
|
|
|
|
|
|
|
use base64::Engine;
|
|
|
|
|
|
|
|
|
|
// Encode certificate as JSON (not X.509 since ML-DSA is not standardized yet)
|
|
|
|
|
let cert_json = json!({
|
|
|
|
|
"version": "SecretumVault-PQC-v1",
|
|
|
|
|
"algorithm": "ML-DSA-65",
|
|
|
|
|
"public_key": base64::engine::general_purpose::STANDARD.encode(&keypair.public_key.key_data),
|
|
|
|
|
"common_name": common_name,
|
|
|
|
|
"issued_at": now.to_rfc3339(),
|
|
|
|
|
"expires_at": expires_at.to_rfc3339(),
|
|
|
|
|
"serial_number": serial.to_string(),
|
|
|
|
|
"is_ca": true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let cert_pem = format!(
|
|
|
|
|
"-----BEGIN SECRETUMVAULT PQC CERTIFICATE-----\n{}\n-----END SECRETUMVAULT PQC \
|
|
|
|
|
CERTIFICATE-----",
|
|
|
|
|
base64::engine::general_purpose::STANDARD.encode(cert_json.to_string().as_bytes())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Encode private key
|
|
|
|
|
let privkey_pem = format!(
|
|
|
|
|
"-----BEGIN SECRETUMVAULT PQC PRIVATE KEY-----\n{}\n-----END SECRETUMVAULT PQC \
|
|
|
|
|
PRIVATE KEY-----",
|
|
|
|
|
base64::engine::general_purpose::STANDARD.encode(&keypair.private_key.key_data)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let metadata = CertificateMetadata {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
certificate_pem: cert_pem,
|
|
|
|
|
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: "ML-DSA-65".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()))?;
|
2025-12-22 21:34:01 +00:00
|
|
|
|
|
|
|
|
// 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<String>,
|
|
|
|
|
ttl_days: i64,
|
|
|
|
|
) -> Result<CertificateMetadata> {
|
|
|
|
|
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<Option<CertificateMetadata>> {
|
|
|
|
|
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<Option<Value>> {
|
|
|
|
|
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<String> = 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<Vec<String>> {
|
|
|
|
|
// 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<String> = 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 {
|
2025-12-29 05:04:53 +00:00
|
|
|
use tempfile::TempDir;
|
|
|
|
|
|
2025-12-22 21:34:01 +00:00
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
}
|