use std::path::PathBuf;
use anyhow::{
Context,
Result,
};
use kftray_commons::models::settings_model::AppSettings;
use log::info;
use log::warn;
use serde::{
Deserialize,
Serialize,
};
use super::cert_generator::{
CertificateGenerator,
CertificatePair,
};
use super::cert_store::CertificateStore;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertificateInfo {
pub domain: String,
pub expires_at: Option<String>,
pub is_valid: bool,
pub file_path: Option<String>,
}
pub struct CertificateManager {
generator: CertificateGenerator,
store: CertificateStore,
settings: AppSettings,
}
impl CertificateManager {
pub fn new(settings: &AppSettings) -> Result<Self> {
Ok(Self {
generator: CertificateGenerator::new()?,
store: CertificateStore::new()?,
settings: settings.clone(),
})
}
pub fn with_components(
generator: CertificateGenerator, store: CertificateStore, settings: AppSettings,
) -> Self {
Self {
generator,
store,
settings,
}
}
pub async fn ensure_certificate(&self, alias: &str) -> Result<CertificatePair> {
if self.store.is_valid(alias).await {
match self.store.load(alias).await {
Ok(cert_pair) => {
info!("Using existing SSL certificate for alias: {}", alias);
return Ok(cert_pair);
}
Err(e) => {
info!("Failed to load existing certificate for {}: {}", alias, e);
}
}
}
info!("Generating SSL certificate for alias: {}", alias);
let cert_pair = self
.generator
.generate_for_alias_with_validity(alias, self.settings.ssl_cert_validity_days)
.await
.context("Failed to generate certificate")?;
self.store
.store(alias, &cert_pair)
.await
.context("Failed to store certificate")?;
info!(
"SSL certificate generated and stored successfully for: {}",
alias
);
Ok(cert_pair)
}
pub async fn ensure_global_certificate(&self, domains: &[String]) -> Result<CertificatePair> {
let cert_name = "global-ssl-cert";
let domains_hash = self.compute_domains_hash(domains);
if self.store.is_valid(cert_name).await {
match self.store.load(cert_name).await {
Ok(cert_pair) => {
let missing_domains: Vec<_> = domains
.iter()
.filter(|domain| !cert_pair.subject_alt_names.contains(domain))
.collect();
if missing_domains.is_empty() {
info!("Using existing global SSL certificate (all domains covered)");
return Ok(cert_pair);
} else {
info!(
"Certificate missing domains: {:?}, regenerating",
missing_domains
);
}
}
Err(e) => {
info!("Failed to load existing global certificate: {}", e);
}
}
}
info!(
"Generating global SSL certificate for domains: {:?}",
domains
);
let cert_pair = self
.generator
.generate_for_domains_with_validity(
domains.to_vec(),
self.settings.ssl_cert_validity_days,
)
.await
.context("Failed to generate global certificate")?;
self.store
.store(cert_name, &cert_pair)
.await
.context("Failed to store global certificate")?;
self.store_domains_hash(cert_name, &domains_hash).await?;
info!("Successfully generated global SSL certificate");
Ok(cert_pair)
}
fn compute_domains_hash(&self, domains: &[String]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{
Hash,
Hasher,
};
let mut sorted_domains = domains.to_vec();
sorted_domains.sort();
sorted_domains.dedup();
let mut hasher = DefaultHasher::new();
for domain in &sorted_domains {
domain.hash(&mut hasher);
}
format!("{:x}", hasher.finish())
}
async fn get_stored_domains_hash(&self, cert_name: &str) -> Result<String> {
let hash_file = self.get_domains_hash_path(cert_name);
tokio::fs::read_to_string(&hash_file)
.await
.map(|s| s.trim().to_string())
.context("Failed to read stored domains hash")
}
async fn store_domains_hash(&self, cert_name: &str, domains_hash: &str) -> Result<()> {
let hash_file = self.get_domains_hash_path(cert_name);
tokio::fs::write(&hash_file, domains_hash)
.await
.context("Failed to write domains hash")
}
fn get_domains_hash_path(&self, cert_name: &str) -> std::path::PathBuf {
self.store
.get_store_path()
.join(format!("{}.domains_hash", cert_name))
}
pub async fn domains_changed(&self, domains: &[String]) -> bool {
let cert_name = "global-ssl-cert";
let current_hash = self.compute_domains_hash(domains);
match self.get_stored_domains_hash(cert_name).await {
Ok(stored_hash) => stored_hash != current_hash,
Err(_) => true,
}
}
pub async fn cleanup_all_ssl_artifacts() -> Result<()> {
info!("Cleaning up all SSL artifacts due to SSL being disabled");
if std::env::var("KFTRAY_TEST_MODE").is_ok() {
info!("Test mode enabled, skipping system cleanup operations");
return Ok(());
}
let mut cleanup_errors = Vec::new();
if let Err(e) = Self::cleanup_system_ca_certificate().await {
warn!("Failed to cleanup system CA certificate: {}", e);
cleanup_errors.push(format!("system CA certificate: {}", e));
}
let base_dir = if let Ok(config_dir) = std::env::var("KFTRAY_CONFIG") {
PathBuf::from(config_dir)
} else if let Some(config_dir) = dirs::config_dir() {
config_dir.join("kftray")
} else {
warn!("No config directory found for SSL cleanup");
cleanup_errors.push("No config directory found".to_string());
PathBuf::new()
};
if !base_dir.as_os_str().is_empty() {
let ssl_certs_dir = base_dir.join("ssl-certs");
if ssl_certs_dir.exists() {
match tokio::fs::remove_dir_all(&ssl_certs_dir).await {
Ok(_) => info!("Removed SSL certificates directory: {:?}", ssl_certs_dir),
Err(e) => {
warn!("Failed to remove SSL certificates directory: {}", e);
cleanup_errors.push(format!("certificates directory: {}", e));
}
}
}
let ssl_ca_dir = base_dir.join("ssl-ca");
if ssl_ca_dir.exists() {
match tokio::fs::remove_dir_all(&ssl_ca_dir).await {
Ok(_) => info!("Removed SSL CA directory: {:?}", ssl_ca_dir),
Err(e) => {
warn!("Failed to remove SSL CA directory: {}", e);
cleanup_errors.push(format!("CA directory: {}", e));
}
}
}
}
if let Ok(store) = super::cert_store::CertificateStore::new()
&& let Err(e) = Self::cleanup_keychain_entries(&store).await
{
warn!("Failed to cleanup keychain entries: {}", e);
cleanup_errors.push(format!("keychain entries: {}", e));
}
if cleanup_errors.is_empty() {
info!("Successfully cleaned up all SSL artifacts");
Ok(())
} else {
warn!(
"SSL cleanup completed with some errors: {:?}",
cleanup_errors
);
Ok(())
}
}
async fn cleanup_keychain_entries(store: &super::cert_store::CertificateStore) -> Result<()> {
info!("Cleaning up SSL vault from keychain");
if let Err(e) = store.cleanup_ssl_vault().await {
warn!("Failed to cleanup SSL vault: {}", e);
return Err(e);
}
info!("Successfully cleaned up SSL vault from keychain");
Ok(())
}
async fn cleanup_system_ca_certificate() -> Result<()> {
if std::env::var("KFTRAY_SKIP_CA_INSTALL").is_ok() {
info!("Skipping system CA removal due to KFTRAY_SKIP_CA_INSTALL environment variable");
return Ok(());
}
let ca_cert_path = if let Ok(config_dir) = std::env::var("KFTRAY_CONFIG") {
PathBuf::from(config_dir)
.join("ssl-ca")
.join("kftray-ca.crt")
} else {
dirs::config_dir()
.context("No config directory found")?
.join("kftray")
.join("ssl-ca")
.join("kftray-ca.crt")
};
if !ca_cert_path.exists() {
info!("No CA certificate file found, skipping system trust store removal");
return Ok(());
}
match tokio::fs::read_to_string(&ca_cert_path).await {
Ok(ca_pem) => {
if let Ok(ca_der_vec) = pem::parse(&ca_pem) {
let ca_cert_der = ca_der_vec.contents();
match super::platform::remove_ca_certificate(ca_cert_der).await {
Ok(_) => {
info!(
"Successfully removed kftray CA certificate from system trust store"
);
}
Err(e) => {
warn!(
"Failed to remove CA certificate from system trust store: {}",
e
);
}
}
} else {
warn!("Failed to parse CA certificate PEM for removal");
}
}
Err(e) => {
warn!("Failed to read CA certificate file for removal: {}", e);
}
}
Ok(())
}
pub async fn regenerate_global_certificate(
&self, domains: &[String],
) -> Result<CertificatePair> {
let cert_name = "global-ssl-cert";
let domains_hash = self.compute_domains_hash(domains);
info!(
"Force regenerating global SSL certificate for domains: {:?}",
domains
);
if let Err(e) = self.store.remove(cert_name).await {
warn!("Failed to remove existing certificate: {}", e);
}
let hash_file = self.get_domains_hash_path(cert_name);
if let Err(e) = tokio::fs::remove_file(&hash_file).await {
warn!("Failed to remove existing domains hash file: {}", e);
}
let cert_pair = self
.generator
.generate_for_domains_with_validity(
domains.to_vec(),
self.settings.ssl_cert_validity_days,
)
.await
.context("Failed to generate global certificate")?;
self.store
.store(cert_name, &cert_pair)
.await
.context("Failed to store global certificate")?;
self.store_domains_hash(cert_name, &domains_hash).await?;
info!("Successfully regenerated global SSL certificate");
Ok(cert_pair)
}
pub async fn regenerate_certificate(&self, alias: &str) -> Result<CertificatePair> {
info!("Regenerating SSL certificate for alias: {}", alias);
let cert_pair = self
.generator
.generate_for_alias_with_validity(alias, self.settings.ssl_cert_validity_days)
.await
.context("Failed to regenerate certificate")?;
self.store
.store(alias, &cert_pair)
.await
.context("Failed to store regenerated certificate")?;
info!("SSL certificate regenerated successfully for: {}", alias);
Ok(cert_pair)
}
pub async fn get_certificate_info(&self, alias: &str) -> Result<CertificateInfo> {
let is_valid = self.store.is_valid(alias).await;
let exists = self.store.exists(alias).await;
if !exists {
let expected_domain = if alias.contains('.') {
alias.to_string()
} else {
format!("{}.local", alias)
};
return Ok(CertificateInfo {
domain: expected_domain,
expires_at: None,
is_valid: false,
file_path: None,
});
}
let domain = if let Ok(cert_pair) = self.store.load(alias).await {
cert_pair.domain
} else {
if alias.contains('.') {
alias.to_string()
} else {
format!("{}.local", alias)
}
};
Ok(CertificateInfo {
domain,
expires_at: None,
is_valid,
file_path: Some(format!("~/.config/kftray/ssl-certs/{}.crt", alias)),
})
}
pub async fn list_all_certificates(&self) -> Result<Vec<CertificateInfo>> {
let store_path = if let Ok(config_dir) = std::env::var("KFTRAY_CONFIG") {
PathBuf::from(config_dir).join("ssl-certs")
} else {
dirs::config_dir()
.context("No config directory found")?
.join("kftray")
.join("ssl-certs")
};
if !store_path.exists() {
return Ok(Vec::new());
}
let mut certificates = Vec::new();
let mut entries = tokio::fs::read_dir(store_path)
.await
.context("Failed to read SSL certificates directory")?;
while let Some(entry) = entries
.next_entry()
.await
.context("Failed to read directory entry")?
{
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if file_name_str.ends_with(".crt") {
let alias = file_name_str.trim_end_matches(".crt");
let cert_info = self.get_certificate_info(alias).await?;
certificates.push(cert_info);
}
}
Ok(certificates)
}
pub async fn remove_certificate(&self, alias: &str) -> Result<()> {
self.store
.remove(alias)
.await
.context("Failed to remove certificate")?;
info!("SSL certificate removed for alias: {}", alias);
Ok(())
}
pub async fn certificate_exists(&self, alias: &str) -> bool {
self.store.exists(alias).await
}
pub async fn is_certificate_valid(&self, alias: &str) -> bool {
self.store.is_valid(alias).await
}
pub async fn ensure_ca_installed_and_trusted(&self) -> Result<()> {
if std::env::var("KFTRAY_SKIP_CA_INSTALL").is_ok() {
info!("Skipping CA installation due to KFTRAY_SKIP_CA_INSTALL environment variable");
return Ok(());
}
static CA_INSTALL_ATTEMPTED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
if CA_INSTALL_ATTEMPTED.load(std::sync::atomic::Ordering::Relaxed) {
info!("CA installation already attempted this session, skipping redundant check");
return Ok(());
}
info!("Ensuring CA certificate is installed and trusted system-wide");
if let Ok(Some(_ca_info)) = self.generator.get_ca_info().await {
info!("CA certificate file exists, verifying system installation status");
let ca_cert_path = if let Ok(config_dir) = std::env::var("KFTRAY_CONFIG") {
PathBuf::from(config_dir)
.join("ssl-ca")
.join("kftray-ca.crt")
} else {
dirs::config_dir()
.context("No config directory found")?
.join("kftray")
.join("ssl-ca")
.join("kftray-ca.crt")
};
if ca_cert_path.exists()
&& let Ok(ca_pem) = tokio::fs::read_to_string(&ca_cert_path).await
&& let Ok(ca_der_vec) = pem::parse(&ca_pem)
{
let ca_cert_der = ca_der_vec.contents();
match super::platform::is_ca_installed(ca_cert_der).await {
Ok(true) => {
info!("CA certificate already installed in system trust store");
CA_INSTALL_ATTEMPTED.store(true, std::sync::atomic::Ordering::Relaxed);
return Ok(());
}
Ok(false) => {
info!("CA file exists but not installed in system trust store");
}
Err(e) => {
warn!("Failed to check CA installation status: {}", e);
}
}
}
info!("CA certificate exists but needs system installation");
if let Ok(ca_pem) = tokio::fs::read_to_string(&ca_cert_path).await {
if let Ok(ca_der_vec) = pem::parse(&ca_pem) {
let ca_cert_der = ca_der_vec.contents();
match super::platform::install_ca_certificate(ca_cert_der, &ca_pem).await {
Ok(_) => {
info!(
"Successfully installed existing CA certificate to system trust store"
);
}
Err(e) => {
warn!("Failed to install existing CA certificate: {}", e);
return Err(e);
}
}
} else {
return Err(anyhow::anyhow!("Failed to parse existing CA certificate"));
}
} else {
return Err(anyhow::anyhow!("Failed to read existing CA certificate"));
}
} else {
info!("No CA certificate found, creating and installing new one");
let _temp_cert = self
.generator
.generate_for_alias("_ca_install_temp")
.await?;
let _ = self.store.remove("_ca_install_temp").await;
let ca_cert_path = if let Ok(config_dir) = std::env::var("KFTRAY_CONFIG") {
PathBuf::from(config_dir)
.join("ssl-ca")
.join("kftray-ca.crt")
} else {
dirs::config_dir()
.context("No config directory found")?
.join("kftray")
.join("ssl-ca")
.join("kftray-ca.crt")
};
if let Ok(ca_pem) = tokio::fs::read_to_string(&ca_cert_path).await {
if let Ok(ca_der_vec) = pem::parse(&ca_pem) {
let ca_cert_der = ca_der_vec.contents();
match super::platform::install_ca_certificate(ca_cert_der, &ca_pem).await {
Ok(_) => {
info!(
"Successfully installed new CA certificate to system trust store"
);
}
Err(e) => {
warn!("Failed to install new CA certificate: {}", e);
return Err(e);
}
}
} else {
return Err(anyhow::anyhow!("Failed to parse new CA certificate"));
}
} else {
return Err(anyhow::anyhow!("Failed to read new CA certificate"));
}
}
CA_INSTALL_ATTEMPTED.store(true, std::sync::atomic::Ordering::Relaxed);
info!("CA certificate installation and trust completed");
Ok(())
}
pub async fn ensure_ca_installed(&self) -> Result<()> {
if let Ok(Some(_ca_info)) = self.generator.get_ca_info().await {
info!("CA certificate already installed, skipping installation");
return Ok(());
}
info!("CA certificate not found, installing...");
let _temp_cert = self
.generator
.generate_for_alias("_ca_install_temp")
.await?;
let _ = self.store.remove("_ca_install_temp").await;
info!("CA certificate installation completed");
Ok(())
}
pub async fn ensure_wildcard_certificate(&self) -> Result<CertificatePair> {
let wildcard_alias = "*.local";
if self.store.is_valid(wildcard_alias).await {
match self.store.load(wildcard_alias).await {
Ok(cert_pair) => {
info!("Using existing *.local wildcard certificate");
return Ok(cert_pair);
}
Err(e) => {
info!("Failed to load existing wildcard certificate: {}", e);
}
}
}
info!("Generating *.local wildcard certificate and installing CA");
let cert_pair = self
.generator
.generate_wildcard_certificate("local", self.settings.ssl_cert_validity_days)
.await
.context("Failed to generate wildcard certificate")?;
self.store
.store(wildcard_alias, &cert_pair)
.await
.context("Failed to store wildcard certificate")?;
info!("Successfully created *.local wildcard certificate and installed CA");
Ok(cert_pair)
}
pub async fn ensure_global_certificate_for_all_configs(&self) -> Result<CertificatePair> {
let domains = Self::collect_all_domains_from_configs().await?;
self.ensure_global_certificate(&domains).await
}
pub async fn collect_all_domains_from_configs() -> Result<Vec<String>> {
let mut domains = vec![
"localhost".to_string(),
"127.0.0.1".to_string(),
"::1".to_string(),
];
match kftray_commons::utils::config::get_configs().await {
Ok(configs) => {
let config_count = configs.len();
for config in configs {
if let Some(local_address) = &config.local_address
&& !local_address.is_empty()
&& !domains.contains(local_address)
{
domains.push(local_address.clone());
}
if let Some(alias) = &config.alias {
if !alias.is_empty() && !domains.contains(alias) {
domains.push(alias.clone());
}
let local_domain = format!("{}.local", alias);
if !domains.contains(&local_domain) {
domains.push(local_domain);
}
}
}
info!(
"Collected domains from {} configs: {:?}",
config_count, domains
);
}
Err(e) => {
warn!(
"Failed to get configs for domain collection, using defaults: {}",
e
);
}
}
domains.sort();
domains.dedup();
Ok(domains)
}
pub async fn regenerate_certificate_for_all_configs(&self) -> Result<CertificatePair> {
let domains = Self::collect_all_domains_from_configs().await?;
self.regenerate_global_certificate(&domains).await
}
pub async fn load_global_certificate(&self) -> Result<CertificatePair> {
let cert_name = "global-ssl-cert";
if !self.store.exists(cert_name).await {
return Err(anyhow::anyhow!(
"Global SSL certificate does not exist. SSL must be enabled in settings first."
));
}
if !self.store.is_valid(cert_name).await {
info!("Existing global certificate is invalid or expired, regenerating");
return self.ensure_global_certificate_for_all_configs().await;
}
match self.store.load(cert_name).await {
Ok(cert_pair) => {
let current_domains = Self::collect_all_domains_from_configs()
.await
.unwrap_or_else(|_| {
warn!("Failed to collect domains, using existing certificate");
vec![]
});
if !current_domains.is_empty() {
let missing_domains: Vec<_> = current_domains
.iter()
.filter(|domain| !cert_pair.subject_alt_names.contains(domain))
.collect();
if !missing_domains.is_empty() {
info!(
"Certificate missing domains: {:?}, regenerating",
missing_domains
);
info!(
"Current certificate domains: {:?}",
cert_pair.subject_alt_names
);
info!("Required domains: {:?}", current_domains);
if let Err(e) = self.store.remove(cert_name).await {
warn!("Failed to remove corrupted certificate: {}", e);
}
return self.ensure_global_certificate_for_all_configs().await;
}
}
info!("Successfully loaded existing global SSL certificate");
Ok(cert_pair)
}
Err(e) => {
warn!("Failed to load global certificate, regenerating: {}", e);
self.ensure_global_certificate_for_all_configs().await
}
}
}
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
async fn create_test_manager() -> (CertificateManager, TempDir) {
unsafe {
std::env::set_var("KFTRAY_SKIP_CA_INSTALL", "1");
std::env::set_var("KFTRAY_TEST_MODE", "1");
}
let temp_dir = TempDir::new().unwrap();
let settings = kftray_commons::models::settings_model::AppSettings {
ssl_enabled: true,
..Default::default()
};
let generator = CertificateGenerator::for_testing(temp_dir.path());
let store = super::CertificateStore::with_path(temp_dir.path().to_path_buf()).unwrap();
let manager = CertificateManager::with_components(generator, store, settings);
(manager, temp_dir)
}
#[tokio::test]
async fn test_ensure_certificate_generation() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (manager, _temp_dir) = create_test_manager().await;
let cert_pair = manager.ensure_certificate("test-service").await.unwrap();
assert_eq!(cert_pair.domain, "test-service.local");
assert!(!cert_pair.certificate.is_empty());
}
#[tokio::test]
async fn test_regenerate_certificate() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (manager, _temp_dir) = create_test_manager().await;
let cert_pair1 = manager.ensure_certificate("test-service").await.unwrap();
let cert_pair2 = manager
.regenerate_certificate("test-service")
.await
.unwrap();
assert_eq!(cert_pair1.domain, cert_pair2.domain);
assert_ne!(cert_pair1.certificate, cert_pair2.certificate);
}
#[tokio::test]
async fn test_certificate_info() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (manager, _temp_dir) = create_test_manager().await;
let info = manager.get_certificate_info("non-existent").await.unwrap();
assert!(!info.is_valid);
assert!(info.file_path.is_none());
manager.ensure_certificate("test-service").await.unwrap();
let info = manager.get_certificate_info("test-service").await.unwrap();
assert_eq!(info.domain, "test-service.local");
}
#[tokio::test]
async fn test_certificate_existence() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (manager, _temp_dir) = create_test_manager().await;
assert!(!manager.certificate_exists("test-service").await);
manager.ensure_certificate("test-service").await.unwrap();
assert!(manager.certificate_exists("test-service").await);
}
#[tokio::test]
async fn test_remove_certificate() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (manager, _temp_dir) = create_test_manager().await;
manager.ensure_certificate("test-service").await.unwrap();
assert!(manager.certificate_exists("test-service").await);
manager.remove_certificate("test-service").await.unwrap();
assert!(!manager.certificate_exists("test-service").await);
}
#[tokio::test]
async fn test_cleanup_ssl_artifacts() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
unsafe {
std::env::set_var("KFTRAY_SKIP_CA_INSTALL", "1");
}
let cleanup_result = CertificateManager::cleanup_all_ssl_artifacts().await;
assert!(cleanup_result.is_ok());
let cleanup_result2 = CertificateManager::cleanup_all_ssl_artifacts().await;
assert!(cleanup_result2.is_ok());
}
#[tokio::test]
async fn test_domains_change_detection() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (_manager, _temp_dir) = create_test_manager().await;
let manager = &_manager;
let domains1 = vec!["localhost".to_string(), "127.0.0.1".to_string()];
let domains2 = vec!["localhost".to_string(), "test.local".to_string()];
let domains3 = vec!["127.0.0.1".to_string(), "localhost".to_string()];
assert!(manager.domains_changed(&domains1).await);
manager.ensure_global_certificate(&domains1).await.unwrap();
assert!(!manager.domains_changed(&domains1).await);
assert!(!manager.domains_changed(&domains3).await);
assert!(manager.domains_changed(&domains2).await);
}
#[tokio::test]
async fn test_load_global_certificate() {
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (_manager, _temp_dir) = create_test_manager().await;
let manager = &_manager;
let result = manager.store.load("global-ssl-cert").await;
assert!(result.is_err());
let domains = vec!["localhost".to_string(), "test.local".to_string()];
let generated_cert = manager.ensure_global_certificate(&domains).await.unwrap();
let loaded_cert = manager.store.load("global-ssl-cert").await.unwrap();
assert_eq!(
generated_cert.certificate.len(),
loaded_cert.certificate.len()
);
assert!(
generated_cert
.subject_alt_names
.contains(&"localhost".to_string())
);
assert!(
loaded_cert
.subject_alt_names
.contains(&"localhost".to_string())
);
assert_eq!(
generated_cert.subject_alt_names.len(),
loaded_cert.subject_alt_names.len()
);
let loaded_cert2 = manager.store.load("global-ssl-cert").await.unwrap();
assert_eq!(loaded_cert.certificate, loaded_cert2.certificate);
}
#[tokio::test]
async fn test_ensure_global_certificate_for_all_configs() {
let _db = kftray_commons::test_utils::test_db().await;
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (_manager, _temp_dir) = create_test_manager().await;
let manager = &_manager;
let cert_pair = manager
.ensure_global_certificate_for_all_configs()
.await
.unwrap();
assert!(
cert_pair
.subject_alt_names
.contains(&"localhost".to_string())
);
assert!(
cert_pair
.subject_alt_names
.contains(&"127.0.0.1".to_string())
);
assert!(cert_pair.subject_alt_names.contains(&"::1".to_string()));
let loaded_cert = manager.load_global_certificate().await.unwrap();
assert_eq!(cert_pair.certificate.len(), loaded_cert.certificate.len());
}
#[tokio::test]
async fn test_load_global_certificate_replaces_expired_certificate() {
let _db = kftray_commons::test_utils::test_db().await;
use crate::ssl::cert_store::{
SSL_TEST_MUTEX,
TEST_SSL_VAULT,
self_signed_pair,
};
let _lock = SSL_TEST_MUTEX.lock().await;
*TEST_SSL_VAULT.lock().unwrap() = Default::default();
let (manager, _temp_dir) = create_test_manager().await;
let domains = CertificateManager::collect_all_domains_from_configs()
.await
.unwrap();
let now = time::OffsetDateTime::now_utc();
let expired = self_signed_pair(
domains,
now - time::Duration::days(400),
now - time::Duration::days(35),
);
manager
.store
.store("global-ssl-cert", &expired)
.await
.unwrap();
let loaded = manager.load_global_certificate().await.unwrap();
assert_ne!(loaded.certificate[0], expired.certificate[0]);
let leaf = openssl::x509::X509::from_der(loaded.certificate[0].as_ref()).unwrap();
let now = openssl::asn1::Asn1Time::days_from_now(0).unwrap();
assert!(leaf.not_after() > now);
assert!(manager.store.is_valid("global-ssl-cert").await);
}
}