use std::time::Duration;
use chrono::{DateTime, Utc};
use x509_cert::Certificate;
use crate::ltv::crl::{self, CrlClient};
use crate::ltv::ocsp::{self, OcspClient};
use crate::ltv::status::{resolve_priority, ValidationStatus};
#[derive(Debug, Clone)]
pub struct RevocationConfig {
pub prefer_ocsp: bool,
pub require_revocation_check: bool,
pub ocsp_timeout: Duration,
pub crl_timeout: Duration,
pub use_ocsp_nonce: bool,
pub max_ocsp_recursion: usize,
pub per_cert_timeout: Duration,
}
impl Default for RevocationConfig {
fn default() -> Self {
Self {
prefer_ocsp: true,
require_revocation_check: true,
ocsp_timeout: Duration::from_secs(3),
crl_timeout: Duration::from_secs(7),
use_ocsp_nonce: true,
max_ocsp_recursion: 1,
per_cert_timeout: Duration::from_secs(10),
}
}
}
impl RevocationConfig {
pub fn disabled() -> Self {
Self {
require_revocation_check: false,
..Default::default()
}
}
pub fn strict() -> Self {
Self {
require_revocation_check: true,
ocsp_timeout: Duration::from_secs(2),
crl_timeout: Duration::from_secs(5),
per_cert_timeout: Duration::from_secs(8),
..Default::default()
}
}
}
pub async fn check_certificate_revocation(
cert: &Certificate,
issuer: &Certificate,
config: &RevocationConfig,
crl_client: &CrlClient,
ocsp_client: &OcspClient,
validation_time: Option<DateTime<Utc>>,
) -> ValidationStatus {
let ocsp_fut = run_ocsp_check(cert, issuer, config, ocsp_client, validation_time);
let crl_fut = run_crl_check(cert, issuer, config, crl_client, validation_time);
let result = tokio::time::timeout(config.per_cert_timeout, async {
tokio::join!(ocsp_fut, crl_fut)
})
.await;
let (ocsp_status, crl_status) = match result {
Ok((ocsp, crl)) => (ocsp, crl),
Err(_elapsed) => {
log::warn!("per-certificate revocation check timed out after {:?}", config.per_cert_timeout);
(
ValidationStatus::Unknown {
reason: "OCSP check timed out".into(),
},
ValidationStatus::Unknown {
reason: "CRL check timed out".into(),
},
)
}
};
log::debug!("OCSP result: {ocsp_status}, CRL result: {crl_status}");
resolve_priority(ocsp_status, crl_status)
}
#[cfg(feature = "blocking")]
pub fn check_certificate_revocation_blocking(
cert: &Certificate,
issuer: &Certificate,
config: &RevocationConfig,
crl_client: &CrlClient,
ocsp_client: &OcspClient,
validation_time: Option<DateTime<Utc>>,
) -> ValidationStatus {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to create tokio runtime");
rt.block_on(check_certificate_revocation(
cert,
issuer,
config,
crl_client,
ocsp_client,
validation_time,
))
}
async fn run_ocsp_check(
cert: &Certificate,
issuer: &Certificate,
config: &RevocationConfig,
ocsp_client: &OcspClient,
validation_time: Option<DateTime<Utc>>,
) -> ValidationStatus {
let urls = OcspClient::extract_ocsp_urls(cert);
if urls.is_empty() {
log::debug!("no OCSP responder URL in certificate AIA extension");
return ValidationStatus::Unknown {
reason: "no OCSP responder URL available".into(),
};
}
let result = tokio::time::timeout(config.ocsp_timeout, async {
let fetch_result = if config.use_ocsp_nonce {
match ocsp_client.fetch_ocsp_response_with_nonce(cert, issuer).await {
Ok((response_der, nonce)) => {
ocsp::check_revocation(&response_der, cert, issuer, Some(&nonce), validation_time)
}
Err(e) => Err(e),
}
} else {
match ocsp_client.fetch_ocsp_response(cert, issuer).await {
Ok(response_der) => {
ocsp::check_revocation(&response_der, cert, issuer, None, validation_time)
}
Err(e) => Err(e),
}
};
match fetch_result {
Ok(status) => status,
Err(e) => {
log::warn!("OCSP check failed: {e}");
ValidationStatus::Unknown {
reason: format!("OCSP check failed: {e}"),
}
}
}
})
.await;
match result {
Ok(status) => status,
Err(_elapsed) => {
log::warn!("OCSP check timed out after {:?}", config.ocsp_timeout);
ValidationStatus::Unknown {
reason: format!("OCSP check timed out after {:?}", config.ocsp_timeout),
}
}
}
}
async fn run_crl_check(
cert: &Certificate,
issuer: &Certificate,
config: &RevocationConfig,
crl_client: &CrlClient,
validation_time: Option<DateTime<Utc>>,
) -> ValidationStatus {
let urls = CrlClient::extract_crl_urls(cert);
if urls.is_empty() {
log::debug!("no CRL distribution points in certificate");
return ValidationStatus::Unknown {
reason: "no CRL distribution points available".into(),
};
}
let result = tokio::time::timeout(config.crl_timeout, async {
match crl_client.fetch_crls_for_cert(cert).await {
Ok(crls) => {
if crls.is_empty() {
return ValidationStatus::Unknown {
reason: "no CRLs could be fetched".into(),
};
}
match crl::check_revocation(&crls[0], cert, issuer, validation_time) {
Ok(status) => status,
Err(e) => {
log::warn!("CRL revocation check failed: {e}");
ValidationStatus::Unknown {
reason: format!("CRL check failed: {e}"),
}
}
}
}
Err(e) => {
log::warn!("CRL fetch failed: {e}");
ValidationStatus::Unknown {
reason: format!("CRL fetch failed: {e}"),
}
}
}
})
.await;
match result {
Ok(status) => status,
Err(_elapsed) => {
log::warn!("CRL check timed out after {:?}", config.crl_timeout);
ValidationStatus::Unknown {
reason: format!("CRL check timed out after {:?}", config.crl_timeout),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ltv::status::RevocationSource;
#[test]
fn test_default_config() {
let config = RevocationConfig::default();
assert!(config.prefer_ocsp);
assert!(config.require_revocation_check);
assert!(config.use_ocsp_nonce);
assert_eq!(config.ocsp_timeout, Duration::from_secs(3));
assert_eq!(config.crl_timeout, Duration::from_secs(7));
assert_eq!(config.max_ocsp_recursion, 1);
assert_eq!(config.per_cert_timeout, Duration::from_secs(10));
}
#[test]
fn test_disabled_config() {
let config = RevocationConfig::disabled();
assert!(!config.require_revocation_check);
assert!(config.prefer_ocsp); }
#[test]
fn test_strict_config() {
let config = RevocationConfig::strict();
assert!(config.require_revocation_check);
assert_eq!(config.ocsp_timeout, Duration::from_secs(2));
assert_eq!(config.crl_timeout, Duration::from_secs(5));
assert_eq!(config.per_cert_timeout, Duration::from_secs(8));
}
#[tokio::test]
async fn test_check_no_ocsp_no_crl_returns_unknown() {
let ca_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
let intermediate_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/intermediate_ca_cert.pem"
));
let (_, ca_der) = pem_rfc7468::decode_vec(ca_pem.as_bytes()).unwrap();
let (_, inter_der) = pem_rfc7468::decode_vec(intermediate_pem.as_bytes()).unwrap();
let ca = der::Decode::from_der(&ca_der).unwrap();
let intermediate: Certificate = der::Decode::from_der(&inter_der).unwrap();
let config = RevocationConfig::default();
let crl_client = CrlClient::new();
let ocsp_client = OcspClient::new();
let status = check_certificate_revocation(
&intermediate,
&ca,
&config,
&crl_client,
&ocsp_client,
None,
)
.await;
assert!(
status.is_unknown(),
"expected Unknown for certs without OCSP/CRL endpoints, got: {status}"
);
}
#[tokio::test]
async fn test_resolve_ocsp_valid_crl_unknown() {
let ocsp = ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: Utc::now(),
};
let crl = ValidationStatus::Unknown {
reason: "no CRL endpoints".into(),
};
let result = resolve_priority(ocsp, crl);
assert!(result.is_valid());
}
#[tokio::test]
async fn test_resolve_ocsp_unknown_crl_revoked() {
let ocsp = ValidationStatus::Unknown {
reason: "timeout".into(),
};
let crl = ValidationStatus::Revoked {
source: RevocationSource::Crl,
reason: crate::ltv::status::RevocationReason::KeyCompromise,
revocation_time: Utc::now(),
};
let result = resolve_priority(ocsp, crl);
assert!(result.is_revoked());
}
#[tokio::test]
async fn test_resolve_both_valid_picks_first() {
let ocsp = ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: Utc::now(),
};
let crl = ValidationStatus::Valid {
source: RevocationSource::Crl,
checked_at: Utc::now(),
};
let result = resolve_priority(ocsp, crl);
assert!(result.is_valid());
match result {
ValidationStatus::Valid { source, .. } => {
assert_eq!(source, RevocationSource::Ocsp);
}
_ => panic!("expected Valid"),
}
}
#[tokio::test]
async fn test_resolve_both_unknown() {
let a = ValidationStatus::Unknown {
reason: "no OCSP".into(),
};
let b = ValidationStatus::Unknown {
reason: "no CRL".into(),
};
let result = resolve_priority(a, b);
assert!(result.is_unknown());
}
#[tokio::test]
async fn test_check_with_signer_cert_returns_unknown() {
let signer_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/signer_cert.pem"
));
let intermediate_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/intermediate_ca_cert.pem"
));
let (_, signer_der) = pem_rfc7468::decode_vec(signer_pem.as_bytes()).unwrap();
let (_, inter_der) = pem_rfc7468::decode_vec(intermediate_pem.as_bytes()).unwrap();
let signer: Certificate = der::Decode::from_der(&signer_der).unwrap();
let intermediate: Certificate = der::Decode::from_der(&inter_der).unwrap();
let config = RevocationConfig {
per_cert_timeout: Duration::from_secs(2),
..Default::default()
};
let crl_client = CrlClient::new();
let ocsp_client = OcspClient::new();
let status = check_certificate_revocation(
&signer,
&intermediate,
&config,
&crl_client,
&ocsp_client,
None,
)
.await;
assert!(
status.is_unknown(),
"expected Unknown for signer cert without endpoints, got: {status}"
);
}
#[test]
fn test_config_custom() {
let config = RevocationConfig {
prefer_ocsp: false,
require_revocation_check: false,
ocsp_timeout: Duration::from_millis(500),
crl_timeout: Duration::from_millis(1000),
use_ocsp_nonce: false,
max_ocsp_recursion: 0,
per_cert_timeout: Duration::from_secs(2),
};
assert!(!config.prefer_ocsp);
assert!(!config.require_revocation_check);
assert!(!config.use_ocsp_nonce);
assert_eq!(config.max_ocsp_recursion, 0);
}
}