use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevocationSource {
Crl,
Ocsp,
}
impl std::fmt::Display for RevocationSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Crl => write!(f, "CRL"),
Self::Ocsp => write!(f, "OCSP"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevocationReason {
Unspecified,
KeyCompromise,
CaCompromise,
AffiliationChanged,
Superseded,
CessationOfOperation,
CertificateHold,
RemoveFromCrl,
PrivilegeWithdrawn,
AaCompromise,
Unknown(u8),
}
impl RevocationReason {
pub fn from_code(code: u8) -> Self {
match code {
0 => Self::Unspecified,
1 => Self::KeyCompromise,
2 => Self::CaCompromise,
3 => Self::AffiliationChanged,
4 => Self::Superseded,
5 => Self::CessationOfOperation,
6 => Self::CertificateHold,
8 => Self::RemoveFromCrl,
9 => Self::PrivilegeWithdrawn,
10 => Self::AaCompromise,
other => Self::Unknown(other),
}
}
pub fn code(&self) -> u8 {
match self {
Self::Unspecified => 0,
Self::KeyCompromise => 1,
Self::CaCompromise => 2,
Self::AffiliationChanged => 3,
Self::Superseded => 4,
Self::CessationOfOperation => 5,
Self::CertificateHold => 6,
Self::RemoveFromCrl => 8,
Self::PrivilegeWithdrawn => 9,
Self::AaCompromise => 10,
Self::Unknown(v) => *v,
}
}
}
impl std::fmt::Display for RevocationReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unspecified => write!(f, "unspecified"),
Self::KeyCompromise => write!(f, "keyCompromise"),
Self::CaCompromise => write!(f, "cACompromise"),
Self::AffiliationChanged => write!(f, "affiliationChanged"),
Self::Superseded => write!(f, "superseded"),
Self::CessationOfOperation => write!(f, "cessationOfOperation"),
Self::CertificateHold => write!(f, "certificateHold"),
Self::RemoveFromCrl => write!(f, "removeFromCRL"),
Self::PrivilegeWithdrawn => write!(f, "privilegeWithdrawn"),
Self::AaCompromise => write!(f, "aACompromise"),
Self::Unknown(v) => write!(f, "unknown({v})"),
}
}
}
#[derive(Debug, Clone)]
pub enum ValidationStatus {
Valid {
source: RevocationSource,
checked_at: DateTime<Utc>,
},
Revoked {
source: RevocationSource,
reason: RevocationReason,
revocation_time: DateTime<Utc>,
},
Invalid {
reason: String,
},
Unknown {
reason: String,
},
}
impl ValidationStatus {
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid { .. })
}
pub fn is_revoked(&self) -> bool {
matches!(self, Self::Revoked { .. })
}
pub fn is_invalid(&self) -> bool {
matches!(self, Self::Invalid { .. })
}
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
fn priority(&self) -> u8 {
match self {
Self::Revoked { .. } => 3,
Self::Valid { .. } => 2,
Self::Invalid { .. } => 1,
Self::Unknown { .. } => 0,
}
}
}
impl std::fmt::Display for ValidationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Valid { source, checked_at } => {
write!(f, "VALID (via {source}, checked at {checked_at})")
}
Self::Revoked {
source,
reason,
revocation_time,
} => {
write!(
f,
"REVOKED (via {source}, reason={reason}, time={revocation_time})"
)
}
Self::Invalid { reason } => write!(f, "INVALID ({reason})"),
Self::Unknown { reason } => write!(f, "UNKNOWN ({reason})"),
}
}
}
pub fn resolve_priority(a: ValidationStatus, b: ValidationStatus) -> ValidationStatus {
if a.priority() >= b.priority() {
a
} else {
b
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_revocation_reason_roundtrip() {
for code in [0, 1, 2, 3, 4, 5, 6, 8, 9, 10] {
let reason = RevocationReason::from_code(code);
assert_eq!(reason.code(), code);
}
let reason = RevocationReason::from_code(7);
assert_eq!(reason.code(), 7);
assert!(matches!(reason, RevocationReason::Unknown(7)));
}
#[test]
fn test_validation_status_predicates() {
let valid = ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: Utc::now(),
};
assert!(valid.is_valid());
assert!(!valid.is_revoked());
let revoked = ValidationStatus::Revoked {
source: RevocationSource::Crl,
reason: RevocationReason::KeyCompromise,
revocation_time: Utc::now(),
};
assert!(revoked.is_revoked());
assert!(!revoked.is_valid());
let invalid = ValidationStatus::Invalid {
reason: "bad signature".into(),
};
assert!(invalid.is_invalid());
let unknown = ValidationStatus::Unknown {
reason: "no OCSP responder".into(),
};
assert!(unknown.is_unknown());
}
#[test]
fn test_resolve_priority_revoked_wins() {
let valid = ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: Utc::now(),
};
let revoked = ValidationStatus::Revoked {
source: RevocationSource::Crl,
reason: RevocationReason::KeyCompromise,
revocation_time: Utc::now(),
};
let result = resolve_priority(valid, revoked);
assert!(result.is_revoked());
}
#[test]
fn test_resolve_priority_valid_over_unknown() {
let valid = ValidationStatus::Valid {
source: RevocationSource::Crl,
checked_at: Utc::now(),
};
let unknown = ValidationStatus::Unknown {
reason: "timeout".into(),
};
let result = resolve_priority(unknown, valid);
assert!(result.is_valid());
}
#[test]
fn test_resolve_priority_invalid_over_unknown() {
let invalid = ValidationStatus::Invalid {
reason: "bad CRL".into(),
};
let unknown = ValidationStatus::Unknown {
reason: "no OCSP".into(),
};
assert!(resolve_priority(invalid.clone(), unknown.clone()).is_invalid());
assert!(resolve_priority(unknown, invalid).is_invalid());
}
#[test]
fn test_resolve_priority_valid_over_invalid() {
let valid = ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: Utc::now(),
};
let invalid = ValidationStatus::Invalid {
reason: "forged CRL".into(),
};
assert!(resolve_priority(valid.clone(), invalid.clone()).is_valid());
assert!(resolve_priority(invalid, valid).is_valid());
}
#[test]
fn test_display() {
let valid = ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: Utc::now(),
};
let s = format!("{valid}");
assert!(s.starts_with("VALID (via OCSP"));
let reason = RevocationReason::KeyCompromise;
assert_eq!(format!("{reason}"), "keyCompromise");
}
}