use super::parser::{CertificateChain, CertificateInfo};
use super::trust_stores::{TrustStoreValidator, TrustValidationResult};
use crate::Result;
use crate::data::CA_STORES;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
fn parse_cert_date(date_str: &str) -> Option<DateTime<Utc>> {
const FORMATS: &[&str] = &[
"%b %d %H:%M:%S %Y %Z", "%Y-%m-%d %H:%M:%S %z", ];
for format in FORMATS {
if let Ok(dt) = DateTime::parse_from_str(date_str, format) {
return Some(dt.with_timezone(&Utc));
}
}
None
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub valid: bool,
pub issues: Vec<ValidationIssue>,
pub trust_chain_valid: bool,
pub hostname_match: bool,
pub not_expired: bool,
pub signature_valid: bool,
pub trusted_ca: Option<String>,
pub platform_trust: Option<TrustValidationResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationIssue {
pub severity: IssueSeverity,
pub issue_type: IssueType,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IssueSeverity {
Critical,
High,
Medium,
Low,
Info,
}
impl IssueSeverity {
pub fn colored_display(&self) -> colored::ColoredString {
use colored::Colorize;
match self {
Self::Critical => "CRITICAL".red().bold(),
Self::High => "HIGH".red(),
Self::Medium => "MEDIUM".yellow(),
Self::Low => "LOW".normal(),
Self::Info => "INFO".cyan(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IssueType {
Expired,
NotYetValid,
HostnameMismatch,
SelfSigned,
UntrustedCA,
ChainIncomplete,
WeakSignature,
ShortKeyLength,
MissingExtension,
}
pub struct CertificateValidator {
hostname: String,
skip_warnings: bool,
trust_validator: Option<TrustStoreValidator>,
}
impl CertificateValidator {
pub fn new(hostname: String) -> Self {
Self {
hostname,
skip_warnings: false,
trust_validator: None,
}
}
pub fn with_skip_warnings(hostname: String, skip: bool) -> Self {
Self {
hostname,
skip_warnings: skip,
trust_validator: None,
}
}
pub fn with_platform_trust(hostname: String) -> Result<Self> {
Ok(Self {
hostname,
skip_warnings: false,
trust_validator: Some(TrustStoreValidator::new()?),
})
}
pub fn with_config(
hostname: String,
skip_warnings: bool,
enable_platform_trust: bool,
) -> Result<Self> {
Ok(Self {
hostname,
skip_warnings,
trust_validator: if enable_platform_trust {
Some(TrustStoreValidator::new()?)
} else {
None
},
})
}
pub fn validate_chain(&self, chain: &CertificateChain) -> Result<ValidationResult> {
let mut issues = Vec::new();
let mut valid = true;
let leaf = match chain.leaf() {
Some(cert) => cert,
None => {
issues.push(ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::ChainIncomplete,
description: "No leaf certificate found".to_string(),
});
return Ok(ValidationResult {
valid: false,
issues,
trust_chain_valid: false,
hostname_match: false,
not_expired: false,
signature_valid: false,
trusted_ca: None,
platform_trust: None,
});
}
};
let not_expired = self.check_expiration(leaf, &mut issues);
valid &= not_expired;
let hostname_match = self.check_hostname(leaf, &mut issues);
valid &= hostname_match;
self.check_key_strength(leaf, &mut issues);
self.check_signature_algorithm(leaf, &mut issues);
let (trust_chain_valid, trusted_ca) = self.validate_trust_chain(chain, &mut issues);
valid &= trust_chain_valid;
if !chain.is_complete() {
issues.push(ValidationIssue {
severity: IssueSeverity::Medium,
issue_type: IssueType::ChainIncomplete,
description: "Certificate chain may be incomplete (no root CA)".to_string(),
});
}
let platform_trust = if let Some(ref validator) = self.trust_validator {
match validator.validate_chain(chain) {
Ok(result) => Some(result),
Err(e) => {
issues.push(ValidationIssue {
severity: IssueSeverity::Info,
issue_type: IssueType::UntrustedCA,
description: format!("Platform trust validation failed: {}", e),
});
None
}
}
} else {
None
};
Ok(ValidationResult {
valid,
issues,
trust_chain_valid,
hostname_match,
not_expired,
signature_valid: true, trusted_ca,
platform_trust,
})
}
fn check_expiration(&self, cert: &CertificateInfo, issues: &mut Vec<ValidationIssue>) -> bool {
let now = Utc::now();
let Some(not_before) = parse_cert_date(&cert.not_before) else {
return true;
};
let Some(not_after) = parse_cert_date(&cert.not_after) else {
return true;
};
if now < not_before {
issues.push(ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::NotYetValid,
description: format!(
"Certificate not yet valid (valid from: {})",
cert.not_before
),
});
return false;
}
if now > not_after {
issues.push(ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::Expired,
description: format!("Certificate expired (valid until: {})", cert.not_after),
});
return false;
}
let days_until_expiry = (not_after - now).num_days();
if days_until_expiry < 30 && !self.skip_warnings {
issues.push(ValidationIssue {
severity: IssueSeverity::Medium,
issue_type: IssueType::Expired,
description: format!(
"Certificate expires soon ({} days remaining)",
days_until_expiry
),
});
}
true
}
fn check_hostname(&self, cert: &CertificateInfo, issues: &mut Vec<ValidationIssue>) -> bool {
let hostname_lower = self.hostname.to_lowercase();
for san in &cert.san {
if san.to_lowercase() == hostname_lower {
return true;
}
if let Some(san_domain) = san.strip_prefix("*.")
&& hostname_lower.ends_with(san_domain)
{
let prefix_len = hostname_lower.len().saturating_sub(san_domain.len());
let prefix = &hostname_lower[..prefix_len];
if prefix.ends_with('.') {
let label = prefix.trim_end_matches('.');
if !label.is_empty() && !label.contains('.') {
return true;
}
}
}
}
if cert
.subject
.to_lowercase()
.contains(&format!("cn={}", hostname_lower))
{
return true;
}
issues.push(ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::HostnameMismatch,
description: format!(
"Certificate hostname mismatch. Expected: {}, Got SANs: {:?}",
self.hostname, cert.san
),
});
false
}
fn check_key_strength(&self, cert: &CertificateInfo, issues: &mut Vec<ValidationIssue>) {
if self.skip_warnings {
return;
}
if let Some(key_size) = cert.public_key_size {
if key_size < 2048 {
issues.push(ValidationIssue {
severity: IssueSeverity::High,
issue_type: IssueType::ShortKeyLength,
description: format!(
"Weak public key: {} bits (minimum recommended: 2048)",
key_size
),
});
} else if key_size < 3072 {
issues.push(ValidationIssue {
severity: IssueSeverity::Low,
issue_type: IssueType::ShortKeyLength,
description: format!(
"Public key size {} bits is acceptable but 3072+ recommended",
key_size
),
});
}
}
}
fn check_signature_algorithm(&self, cert: &CertificateInfo, issues: &mut Vec<ValidationIssue>) {
if self.skip_warnings {
return;
}
let sig_alg = cert.signature_algorithm.to_lowercase();
if sig_alg.contains("md5") {
issues.push(ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::WeakSignature,
description: "Certificate uses MD5 signature (broken)".to_string(),
});
} else if sig_alg.contains("sha1") {
issues.push(ValidationIssue {
severity: IssueSeverity::High,
issue_type: IssueType::WeakSignature,
description: "Certificate uses SHA-1 signature (deprecated)".to_string(),
});
}
}
fn validate_trust_chain(
&self,
chain: &CertificateChain,
issues: &mut Vec<ValidationIssue>,
) -> (bool, Option<String>) {
let ca_stores = CA_STORES.as_ref();
let find_store_for_subject = |subject: &str| -> Option<String> {
for store in ca_stores.all_stores() {
for ca_cert in &store.certificates {
if ca_cert.subject == subject {
return Some(store.name.clone());
}
}
}
None
};
let Some(last_cert) = chain.certificates.last() else {
issues.push(ValidationIssue {
severity: IssueSeverity::High,
issue_type: IssueType::UntrustedCA,
description: "No issuer certificate in chain".to_string(),
});
return (false, None);
};
if chain.certificates.len() == 1 && last_cert.subject == last_cert.issuer {
if let Some(store_name) = find_store_for_subject(&last_cert.subject) {
return (true, Some(store_name));
}
issues.push(ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::SelfSigned,
description: "Certificate is self-signed and not a known root CA".to_string(),
});
return (false, None);
}
if last_cert.subject == last_cert.issuer {
if let Some(store_name) = find_store_for_subject(&last_cert.subject) {
return (true, Some(store_name));
}
}
if let Some(store_name) = find_store_for_subject(&last_cert.issuer) {
return (true, Some(store_name));
}
if let Some(store_name) = find_store_for_subject(&last_cert.subject) {
return (true, Some(store_name));
}
issues.push(ValidationIssue {
severity: IssueSeverity::High,
issue_type: IssueType::UntrustedCA,
description: format!(
"Issuer not found in trusted CA stores: {}",
last_cert.issuer
),
});
(false, None)
}
}
impl ValidationResult {
pub fn summary(&self) -> String {
if self.valid {
"Certificate is valid and trusted".to_string()
} else {
format!(
"Certificate validation failed ({} issues)",
self.issues.len()
)
}
}
pub fn critical_issues(&self) -> Vec<&ValidationIssue> {
self.issues
.iter()
.filter(|i| matches!(i.severity, IssueSeverity::Critical))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hostname_matching() {
let cert = CertificateInfo {
subject: "CN=example.com".to_string(),
issuer: "CN=CA".to_string(),
serial_number: "123".to_string(),
not_before: "2024-01-01".to_string(),
not_after: "2025-01-01".to_string(),
expiry_countdown: None,
signature_algorithm: "sha256WithRSAEncryption".to_string(),
public_key_algorithm: "rsaEncryption".to_string(),
public_key_size: Some(2048),
rsa_exponent: None,
san: vec!["example.com".to_string(), "www.example.com".to_string()],
is_ca: false,
key_usage: vec![],
extended_key_usage: vec![],
extended_validation: false,
ev_oids: vec![],
pin_sha256: None,
fingerprint_sha256: None,
debian_weak_key: None,
aia_url: None,
certificate_transparency: None,
der_bytes: vec![],
};
let validator = CertificateValidator::new("example.com".to_string());
let mut issues = Vec::new();
assert!(validator.check_hostname(&cert, &mut issues));
assert!(issues.is_empty());
}
#[test]
fn test_weak_key_detection() {
let cert = CertificateInfo {
subject: "CN=example.com".to_string(),
issuer: "CN=CA".to_string(),
serial_number: "123".to_string(),
not_before: "2024-01-01".to_string(),
not_after: "2025-01-01".to_string(),
expiry_countdown: Some("expires in 1 year".to_string()),
signature_algorithm: "sha256WithRSAEncryption".to_string(),
public_key_algorithm: "rsaEncryption".to_string(),
public_key_size: Some(1024), rsa_exponent: Some("e 65537".to_string()),
san: vec!["example.com".to_string()],
is_ca: false,
key_usage: vec![],
extended_key_usage: vec![],
extended_validation: false,
ev_oids: vec![],
pin_sha256: None,
fingerprint_sha256: None,
debian_weak_key: None,
aia_url: None,
certificate_transparency: None,
der_bytes: vec![],
};
let validator = CertificateValidator::new("example.com".to_string());
let mut issues = Vec::new();
validator.check_key_strength(&cert, &mut issues);
assert!(!issues.is_empty());
assert!(matches!(issues[0].issue_type, IssueType::ShortKeyLength));
}
}