use super::types::{AuthResult, CertificateAuth, User};
use crate::config::SecurityConfig;
use crate::error::{FusekiError, FusekiResult};
use chrono::{DateTime, Utc};
use oid_registry::{OID_X509_EXT_EXTENDED_KEY_USAGE, OID_X509_EXT_KEY_USAGE};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{debug, warn};
use x509_parser::pem;
use x509_parser::prelude::*;
pub struct CertificateAuthService {
config: Arc<SecurityConfig>,
}
impl CertificateAuthService {
pub fn new(config: Arc<SecurityConfig>) -> Self {
Self { config }
}
pub async fn authenticate_certificate(&self, cert_data: &[u8]) -> FusekiResult<AuthResult> {
let (_, cert) = X509Certificate::from_der(cert_data)
.map_err(|e| FusekiError::authentication(format!("Invalid certificate: {e}")))?;
if !self.validate_certificate_trust(&cert).await? {
warn!("Certificate validation failed - not trusted");
return Ok(AuthResult::CertificateInvalid);
}
let now = Utc::now();
let not_before = DateTime::from_timestamp(cert.validity().not_before.timestamp(), 0)
.unwrap_or_else(Utc::now);
let not_after = DateTime::from_timestamp(cert.validity().not_after.timestamp(), 0)
.unwrap_or_else(Utc::now);
if now < not_before || now > not_after {
warn!("Certificate is expired or not yet valid");
return Ok(AuthResult::CertificateInvalid);
}
let cert_auth = self.extract_certificate_info(&cert)?;
let user = self.map_certificate_to_user(&cert_auth).await?;
debug!(
"Successful certificate authentication for: {}",
user.username
);
Ok(AuthResult::Authenticated(user))
}
async fn validate_certificate_trust(&self, cert: &X509Certificate<'_>) -> FusekiResult<bool> {
let trust_store_path = self
.config
.certificate
.as_ref()
.map(|c| &c.trust_store)
.ok_or_else(|| FusekiError::configuration("Trust store not configured"))?;
let trust_certificate_data = self.load_trust_store_certificates(trust_store_path).await?;
let cert_fingerprint = self.compute_certificate_fingerprint(cert)?;
for trust_cert_data in &trust_certificate_data {
if let Ok((_, trust_cert)) = X509Certificate::from_der(trust_cert_data) {
let trust_fingerprint = self.compute_certificate_fingerprint(&trust_cert)?;
if cert_fingerprint == trust_fingerprint {
debug!("Certificate directly trusted");
return Ok(true);
}
}
}
for ca_cert_data in &trust_certificate_data {
if let Ok((_, ca_cert)) = X509Certificate::from_der(ca_cert_data) {
if self.verify_certificate_signature(cert, &ca_cert)? {
debug!("Certificate signed by trusted CA");
return Ok(true);
}
}
}
if let Some(trusted_issuers) = self
.config
.certificate
.as_ref()
.and_then(|c| c.trusted_issuers.as_ref())
{
let issuer_dn = cert.issuer().to_string();
for pattern in trusted_issuers {
if self.match_issuer_pattern(&issuer_dn, pattern)? {
debug!("Certificate issuer matches trusted pattern: {}", pattern);
return Ok(true);
}
}
}
Ok(false)
}
async fn load_trust_store_certificates(
&self,
trust_store_paths: &[PathBuf],
) -> FusekiResult<Vec<Vec<u8>>> {
let mut certificate_data = Vec::new();
for trust_store_path in trust_store_paths {
let data = tokio::fs::read(trust_store_path).await?;
if let Ok((_, pem_cert)) = pem::parse_x509_pem(&data) {
match X509Certificate::from_der(&pem_cert.contents) {
Ok(_) => {
certificate_data.push(pem_cert.contents.to_vec());
}
Err(e) => {
return Err(FusekiError::authentication(format!(
"Failed to parse PEM certificate contents from {trust_store_path:?}: {e}"
)));
}
}
} else {
match X509Certificate::from_der(&data) {
Ok(_) => {
certificate_data.push(data.clone());
}
Err(e) => {
return Err(FusekiError::authentication(format!(
"Failed to parse DER certificate from {trust_store_path:?}: {e}"
)));
}
}
}
}
Ok(certificate_data)
}
fn compute_certificate_fingerprint(&self, cert: &X509Certificate) -> FusekiResult<String> {
use sha2::{Digest, Sha256};
let serial = format!("{:x}", cert.serial);
let subject = cert.subject().to_string();
let combined = format!("{serial}:{subject}");
let fingerprint = Sha256::digest(combined.as_bytes())
.iter()
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(":");
Ok(fingerprint)
}
fn match_issuer_pattern(&self, issuer_dn: &str, pattern: &str) -> FusekiResult<bool> {
if pattern == "*" {
return Ok(true);
}
if pattern.contains('*') {
let regex_pattern = pattern.replace('*', ".*");
let regex = regex::Regex::new(®ex_pattern)
.map_err(|e| FusekiError::configuration(format!("Invalid issuer pattern: {e}")))?;
Ok(regex.is_match(issuer_dn))
} else {
Ok(issuer_dn == pattern)
}
}
fn verify_certificate_signature(
&self,
cert: &X509Certificate,
ca_cert: &X509Certificate,
) -> FusekiResult<bool> {
let cert_issuer = cert.issuer().to_string();
let ca_subject = ca_cert.subject().to_string();
Ok(cert_issuer == ca_subject)
}
fn extract_certificate_info(&self, cert: &X509Certificate) -> FusekiResult<CertificateAuth> {
let subject_dn = cert.subject().to_string();
let issuer_dn = cert.issuer().to_string();
let serial_number = cert.serial.to_string();
let fingerprint = self.compute_certificate_fingerprint(cert)?;
let not_before = DateTime::from_timestamp(cert.validity().not_before.timestamp(), 0)
.unwrap_or_else(Utc::now);
let not_after = DateTime::from_timestamp(cert.validity().not_after.timestamp(), 0)
.unwrap_or_else(Utc::now);
let mut key_usage = Vec::new();
let mut extended_key_usage = Vec::new();
for ext in cert.extensions() {
if ext.oid == OID_X509_EXT_KEY_USAGE {
key_usage.push("digital_signature".to_string());
key_usage.push("key_agreement".to_string());
} else if ext.oid == OID_X509_EXT_EXTENDED_KEY_USAGE {
extended_key_usage.push("client_auth".to_string());
}
}
Ok(CertificateAuth {
subject_dn,
issuer_dn,
serial_number,
fingerprint,
not_before,
not_after,
key_usage,
extended_key_usage,
})
}
async fn map_certificate_to_user(&self, cert_auth: &CertificateAuth) -> FusekiResult<User> {
let username = self.extract_username_from_dn(&cert_auth.subject_dn)?;
let user = User {
username,
roles: vec!["certificate_user".to_string()],
email: self.extract_email_from_dn(&cert_auth.subject_dn).ok(),
full_name: self.extract_common_name_from_dn(&cert_auth.subject_dn).ok(),
last_login: Some(Utc::now()),
permissions: vec![
super::types::Permission::Read,
super::types::Permission::QueryExecute,
],
};
Ok(user)
}
fn extract_username_from_dn(&self, dn: &str) -> FusekiResult<String> {
for component in dn.split(',') {
let component = component.trim();
if let Some(stripped) = component.strip_prefix("CN=") {
return Ok(stripped.to_string());
}
}
Err(FusekiError::authentication(
"No username found in certificate DN",
))
}
fn extract_email_from_dn(&self, dn: &str) -> FusekiResult<String> {
for component in dn.split(',') {
let component = component.trim();
if component.starts_with("emailAddress=") || component.starts_with("E=") {
let start_pos = if component.starts_with("emailAddress=") {
13
} else {
2
};
return Ok(component[start_pos..].to_string());
}
}
Err(FusekiError::authentication(
"No email found in certificate DN",
))
}
fn extract_common_name_from_dn(&self, dn: &str) -> FusekiResult<String> {
for component in dn.split(',') {
let component = component.trim();
if let Some(stripped) = component.strip_prefix("CN=") {
return Ok(stripped.to_string());
}
}
Err(FusekiError::authentication(
"No common name found in certificate DN",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn test_match_issuer_pattern_exact() {
let config = Arc::new(SecurityConfig::default());
let auth_service = CertificateAuthService::new(config);
let issuer_dn = "CN=Test CA,O=Test Corp,C=US";
let pattern = "CN=Test CA,O=Test Corp,C=US";
let result = auth_service
.match_issuer_pattern(issuer_dn, pattern)
.unwrap();
assert!(result);
}
#[test]
fn test_match_issuer_pattern_wildcard() {
let config = Arc::new(SecurityConfig::default());
let auth_service = CertificateAuthService::new(config);
let issuer_dn = "CN=Test CA,O=Test Corp,C=US";
let pattern = "CN=Test CA,O=*,C=US";
let result = auth_service
.match_issuer_pattern(issuer_dn, pattern)
.unwrap();
assert!(result);
}
#[test]
fn test_match_issuer_pattern_wildcard_all() {
let config = Arc::new(SecurityConfig::default());
let auth_service = CertificateAuthService::new(config);
let issuer_dn = "CN=Any CA,O=Any Corp,C=US";
let pattern = "*";
let result = auth_service
.match_issuer_pattern(issuer_dn, pattern)
.unwrap();
assert!(result);
}
#[test]
fn test_match_issuer_pattern_no_match() {
let config = Arc::new(SecurityConfig::default());
let auth_service = CertificateAuthService::new(config);
let issuer_dn = "CN=Test CA,O=Test Corp,C=US";
let pattern = "CN=Different CA,O=Test Corp,C=US";
let result = auth_service
.match_issuer_pattern(issuer_dn, pattern)
.unwrap();
assert!(!result);
}
#[test]
fn test_extract_username_from_dn() {
let config = Arc::new(SecurityConfig::default());
let auth_service = CertificateAuthService::new(config);
let dn = "CN=john.doe,O=Test Corp,C=US";
let username = auth_service.extract_username_from_dn(dn).unwrap();
assert_eq!(username, "john.doe");
}
#[test]
fn test_extract_email_from_dn() {
let config = Arc::new(SecurityConfig::default());
let auth_service = CertificateAuthService::new(config);
let dn = "CN=John Doe,emailAddress=john.doe@example.com,O=Test Corp,C=US";
let email = auth_service.extract_email_from_dn(dn).unwrap();
assert_eq!(email, "john.doe@example.com");
}
}