use super::parser::CertificateInfo;
use super::revocation::{RevocationResult, RevocationStatus};
use super::validator::{IssueType, ValidationResult};
use crate::cli::Args;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CertificateStatus {
pub is_expired: bool,
pub is_self_signed: bool,
pub is_mismatched: bool,
pub is_revoked: bool,
pub is_untrusted: bool,
}
impl CertificateStatus {
pub fn from_validation_result(
validation: &ValidationResult,
hostname: &str,
cert: &CertificateInfo,
revocation: Option<&RevocationResult>,
) -> Self {
let is_expired = Self::detect_expired(validation, cert);
let is_self_signed = Self::detect_self_signed(validation, cert);
let is_mismatched = Self::detect_mismatched(validation, hostname, cert);
let is_revoked = Self::detect_revoked(revocation);
let is_untrusted = Self::detect_untrusted(validation);
Self {
is_expired,
is_self_signed,
is_mismatched,
is_revoked,
is_untrusted,
}
}
pub fn matches_filter(&self, args: &Args) -> bool {
if !args.has_certificate_filters() {
return true;
}
let mut matches = false;
if args.cert_filters.filter_expired && self.is_expired {
matches = true;
}
if args.cert_filters.filter_self_signed && self.is_self_signed {
matches = true;
}
if args.cert_filters.filter_mismatched && self.is_mismatched {
matches = true;
}
if args.cert_filters.filter_revoked && self.is_revoked {
matches = true;
}
if args.cert_filters.filter_untrusted && self.is_untrusted {
matches = true;
}
matches
}
fn detect_expired(validation: &ValidationResult, cert: &CertificateInfo) -> bool {
if validation
.issues
.iter()
.any(|issue| matches!(issue.issue_type, IssueType::Expired))
{
return true;
}
if !validation.not_expired {
return true;
}
if let Ok(not_after) = parse_certificate_date(&cert.not_after)
&& Utc::now() > not_after
{
return true;
}
false
}
fn detect_self_signed(validation: &ValidationResult, cert: &CertificateInfo) -> bool {
if validation
.issues
.iter()
.any(|issue| matches!(issue.issue_type, IssueType::SelfSigned))
{
return true;
}
cert.subject == cert.issuer
}
fn detect_mismatched(
validation: &ValidationResult,
hostname: &str,
cert: &CertificateInfo,
) -> bool {
if validation
.issues
.iter()
.any(|issue| matches!(issue.issue_type, IssueType::HostnameMismatch))
{
return true;
}
if !validation.hostname_match {
return true;
}
let hostname_lower = hostname.to_lowercase();
for san in &cert.san {
if san.to_lowercase() == hostname_lower {
return false; }
if let Some(san_domain) = san.strip_prefix("*.")
&& hostname_lower.ends_with(san_domain)
{
return false; }
}
if cert
.subject
.to_lowercase()
.contains(&format!("cn={}", hostname_lower))
{
return false; }
true
}
fn detect_revoked(revocation: Option<&RevocationResult>) -> bool {
if let Some(rev) = revocation {
matches!(rev.status, RevocationStatus::Revoked)
} else {
false
}
}
fn detect_untrusted(validation: &ValidationResult) -> bool {
if validation
.issues
.iter()
.any(|issue| matches!(issue.issue_type, IssueType::UntrustedCA))
{
return true;
}
if !validation.trust_chain_valid {
return true;
}
if let Some(ref platform_trust) = validation.platform_trust {
if !platform_trust.overall_trusted {
return true;
}
}
false
}
}
fn parse_certificate_date(date_str: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
use chrono::NaiveDateTime;
if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
return Ok(dt.with_timezone(&Utc));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S UTC") {
return Ok(DateTime::from_naive_utc_and_offset(dt, Utc));
}
let cleaned = date_str.replace(" UTC", "").replace(" GMT", "");
if let Ok(dt) = NaiveDateTime::parse_from_str(&cleaned, "%Y-%m-%d %H:%M:%S") {
return Ok(DateTime::from_naive_utc_and_offset(dt, Utc));
}
if date_str.ends_with(" GMT") || date_str.ends_with(" UTC") {
let without_tz = date_str.replace(" GMT", "").replace(" UTC", "");
if let Ok(dt) = NaiveDateTime::parse_from_str(&without_tz, "%b %d %H:%M:%S %Y") {
return Ok(DateTime::from_naive_utc_and_offset(dt, Utc));
}
}
NaiveDateTime::parse_from_str("invalid", "%Y-%m-%d %H:%M:%S")
.map(|dt| DateTime::from_naive_utc_and_offset(dt, Utc))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::certificates::validator::{IssueSeverity, ValidationIssue};
#[test]
fn test_detect_expired() {
let validation = ValidationResult {
valid: false,
issues: vec![ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::Expired,
description: "Certificate expired".to_string(),
}],
trust_chain_valid: true,
hostname_match: true,
not_expired: false,
signature_valid: true,
trusted_ca: None,
platform_trust: None,
};
let cert = CertificateInfo {
subject: "CN=example.com".to_string(),
issuer: "CN=CA".to_string(),
not_after: "2020-01-01 00:00:00 UTC".to_string(),
..Default::default()
};
assert!(
CertificateStatus::detect_expired(&validation, &cert),
"Should detect expired certificate"
);
}
#[test]
fn test_detect_self_signed() {
let validation = ValidationResult {
valid: false,
issues: vec![ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::SelfSigned,
description: "Self-signed certificate".to_string(),
}],
trust_chain_valid: false,
hostname_match: true,
not_expired: true,
signature_valid: true,
trusted_ca: None,
platform_trust: None,
};
let cert = CertificateInfo {
subject: "CN=example.com".to_string(),
issuer: "CN=example.com".to_string(), ..Default::default()
};
assert!(
CertificateStatus::detect_self_signed(&validation, &cert),
"Should detect self-signed certificate"
);
}
#[test]
fn test_detect_mismatched() {
let validation = ValidationResult {
valid: false,
issues: vec![ValidationIssue {
severity: IssueSeverity::Critical,
issue_type: IssueType::HostnameMismatch,
description: "Hostname mismatch".to_string(),
}],
trust_chain_valid: true,
hostname_match: false,
not_expired: true,
signature_valid: true,
trusted_ca: None,
platform_trust: None,
};
let cert = CertificateInfo {
subject: "CN=example.com".to_string(),
san: vec!["example.com".to_string()],
..Default::default()
};
assert!(
CertificateStatus::detect_mismatched(&validation, "different.com", &cert),
"Should detect hostname mismatch"
);
}
#[test]
fn test_matches_filter_no_filters_active() {
let status = CertificateStatus {
is_expired: true,
is_self_signed: true,
is_mismatched: false,
is_revoked: false,
is_untrusted: false,
};
let args = Args::default();
assert!(
status.matches_filter(&args),
"Should match when no filters are active"
);
}
#[test]
fn test_matches_filter_expired_filter() {
let status = CertificateStatus {
is_expired: true,
is_self_signed: false,
is_mismatched: false,
is_revoked: false,
is_untrusted: false,
};
let mut args = Args::default();
args.cert_filters.filter_expired = true;
assert!(
status.matches_filter(&args),
"Expired certificate should match expired filter"
);
let status_not_expired = CertificateStatus {
is_expired: false,
..status
};
assert!(
!status_not_expired.matches_filter(&args),
"Non-expired certificate should not match expired filter"
);
}
#[test]
fn test_matches_filter_multiple_filters() {
let status = CertificateStatus {
is_expired: false,
is_self_signed: true,
is_mismatched: false,
is_revoked: false,
is_untrusted: false,
};
let mut args = Args::default();
args.cert_filters.filter_expired = true;
args.cert_filters.filter_self_signed = true;
assert!(
status.matches_filter(&args),
"Self-signed certificate should match when self-signed filter is active"
);
}
#[test]
fn test_parse_certificate_date() {
assert!(parse_certificate_date("2025-01-01 00:00:00 UTC").is_ok());
assert!(parse_certificate_date("2025-01-01T00:00:00Z").is_ok());
assert!(parse_certificate_date("Jan 01 00:00:00 2025 GMT").is_ok());
}
}