use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use reqwest::Client;
use x509_cert::Certificate;
use crate::der_utils::{
find_tagged_value, integer_bodies_equal, parse_integer_body, parse_tlv,
parse_tlv_with_rest, parse_x509_time,
};
use crate::error::LtvError;
use crate::ltv::status::{RevocationReason, RevocationSource, ValidationStatus};
#[derive(Debug, Clone)]
struct CrlCacheEntry {
der: Vec<u8>,
fetched_at: Instant,
}
#[derive(Debug, Clone)]
pub struct CrlClient {
http_client: Client,
timeout: Duration,
cache: Arc<Mutex<HashMap<String, CrlCacheEntry>>>,
grace_period: Duration,
}
impl CrlClient {
pub fn new() -> Self {
Self {
http_client: Client::new(),
timeout: Duration::from_secs(30),
cache: Arc::new(Mutex::new(HashMap::new())),
grace_period: Duration::from_secs(3600),
}
}
pub fn http_client(mut self, client: Client) -> Self {
self.http_client = client;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn grace_period(mut self, grace: Duration) -> Self {
self.grace_period = grace;
self
}
pub fn extract_crl_urls(cert: &Certificate) -> Vec<String> {
let mut urls = Vec::new();
let crl_dp_oid = const_oid::ObjectIdentifier::new_unwrap("2.5.29.31");
if let Some(extensions) = &cert.tbs_certificate.extensions {
for ext in extensions.iter() {
if ext.extn_id == crl_dp_oid {
if let Ok(urls_from_ext) = parse_crl_dp_extension(ext.extn_value.as_bytes()) {
urls.extend(urls_from_ext);
}
}
}
}
urls
}
pub async fn fetch_crl(&self, url: &str) -> Result<Vec<u8>, LtvError> {
{
let cache = self.cache.lock().map_err(|e| {
LtvError::Crl(format!("cache lock poisoned: {e}"))
})?;
if let Some(entry) = cache.get(url) {
if entry.fetched_at.elapsed() < self.grace_period {
log::debug!("CRL cache hit for {url}");
return Ok(entry.der.clone());
}
}
}
log::debug!("Fetching CRL from {url}");
let response = self
.http_client
.get(url)
.timeout(self.timeout)
.send()
.await
.map_err(|e| LtvError::Crl(format!("CRL fetch from {url} failed: {e}")))?;
if !response.status().is_success() {
return Err(LtvError::Crl(format!(
"CRL fetch from {url} returned HTTP {}",
response.status()
)));
}
let crl_bytes = response
.bytes()
.await
.map_err(|e| LtvError::Crl(format!("failed to read CRL response body: {e}")))?
.to_vec();
if crl_bytes.is_empty() || crl_bytes[0] != 0x30 {
return Err(LtvError::Crl(format!(
"CRL from {url} does not appear to be DER-encoded"
)));
}
log::debug!("CRL from {url}: {} bytes", crl_bytes.len());
{
let mut cache = self.cache.lock().map_err(|e| {
LtvError::Crl(format!("cache lock poisoned: {e}"))
})?;
cache.insert(
url.to_string(),
CrlCacheEntry {
der: crl_bytes.clone(),
fetched_at: Instant::now(),
},
);
}
Ok(crl_bytes)
}
pub async fn fetch_crls_for_cert(
&self,
cert: &Certificate,
) -> Result<Vec<Vec<u8>>, LtvError> {
let urls = Self::extract_crl_urls(cert);
let mut crls = Vec::new();
for url in &urls {
match self.fetch_crl(url).await {
Ok(crl) => {
crls.push(crl);
break;
}
Err(e) => {
log::warn!("Failed to fetch CRL from {url}: {e}");
}
}
}
Ok(crls)
}
pub fn clear_cache(&self) {
if let Ok(mut cache) = self.cache.lock() {
cache.clear();
}
}
}
impl Default for CrlClient {
fn default() -> Self {
Self::new()
}
}
fn parse_crl_dp_extension(der_bytes: &[u8]) -> Result<Vec<String>, String> {
let mut urls = Vec::new();
let (tag, body) = parse_tlv(der_bytes)?;
if tag != 0x30 {
return Err(format!("expected SEQUENCE, got 0x{tag:02x}"));
}
let mut pos = &body[..];
while !pos.is_empty() {
let (dp_tag, dp_body, rest) = parse_tlv_with_rest(pos)?;
if dp_tag == 0x30 {
if !dp_body.is_empty() {
if let Ok((inner_tag, inner_body, _)) = parse_tlv_with_rest(&dp_body) {
if inner_tag == 0xA0 {
if let Ok((fn_tag, fn_body, _)) = parse_tlv_with_rest(&inner_body) {
if fn_tag == 0xA0 {
let mut gn_pos = &fn_body[..];
while !gn_pos.is_empty() {
if let Ok((gn_tag, gn_body, gn_rest)) =
parse_tlv_with_rest(gn_pos)
{
if gn_tag == 0x86 {
if let Ok(uri) = std::str::from_utf8(&gn_body) {
urls.push(uri.to_string());
}
}
gn_pos = gn_rest;
} else {
break;
}
}
}
}
}
}
}
}
pos = rest;
}
Ok(urls)
}
#[derive(Debug, Clone)]
pub struct RevokedEntry {
pub serial_number: Vec<u8>,
pub revocation_time: chrono::DateTime<chrono::Utc>,
pub reason: RevocationReason,
}
#[derive(Debug)]
pub struct ParsedCrl {
pub tbs_bytes: Vec<u8>,
pub signature_algorithm_oid: const_oid::ObjectIdentifier,
pub signature_bytes: Vec<u8>,
pub issuer_der: Vec<u8>,
pub this_update: chrono::DateTime<chrono::Utc>,
pub next_update: Option<chrono::DateTime<chrono::Utc>>,
pub revoked_entries: Vec<RevokedEntry>,
}
pub fn parse_crl(crl_der: &[u8]) -> Result<ParsedCrl, LtvError> {
let (outer_tag, outer_body) =
parse_tlv(crl_der).map_err(|e| LtvError::Crl(format!("CRL outer SEQUENCE: {e}")))?;
if outer_tag != 0x30 {
return Err(LtvError::Crl(format!(
"expected CRL SEQUENCE (0x30), got 0x{outer_tag:02x}"
)));
}
let (tbs_tag, tbs_value, rest) = parse_tlv_with_rest(&outer_body)
.map_err(|e| LtvError::Crl(format!("CRL tbsCertList: {e}")))?;
if tbs_tag != 0x30 {
return Err(LtvError::Crl(format!(
"expected tbsCertList SEQUENCE, got 0x{tbs_tag:02x}"
)));
}
let tbs_start = crl_der.len() - outer_body.len();
let tbs_end = crl_der.len() - outer_body.len() + (outer_body.len() - rest.len());
let tbs_bytes = crl_der[tbs_start..tbs_end].to_vec();
let (sig_alg_tag, sig_alg_body, rest) =
parse_tlv_with_rest(rest).map_err(|e| LtvError::Crl(format!("CRL sigAlg: {e}")))?;
if sig_alg_tag != 0x30 {
return Err(LtvError::Crl(format!(
"expected signatureAlgorithm SEQUENCE, got 0x{sig_alg_tag:02x}"
)));
}
let sig_oid = parse_oid_from_algorithm_identifier(&sig_alg_body)?;
let (sig_val_tag, sig_val_body, _) =
parse_tlv_with_rest(rest).map_err(|e| LtvError::Crl(format!("CRL sigValue: {e}")))?;
if sig_val_tag != 0x03 {
return Err(LtvError::Crl(format!(
"expected signatureValue BIT STRING (0x03), got 0x{sig_val_tag:02x}"
)));
}
if sig_val_body.is_empty() {
return Err(LtvError::Crl("empty signature BIT STRING".into()));
}
let signature_bytes = sig_val_body[1..].to_vec();
let mut tbs_pos = &tbs_value[..];
if !tbs_pos.is_empty() && tbs_pos[0] == 0x02 {
let (_, _, r) = parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Crl(format!("CRL version: {e}")))?;
tbs_pos = r;
}
if !tbs_pos.is_empty() {
let (tag, _, r) = parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Crl(format!("CRL inner sigAlg: {e}")))?;
if tag == 0x30 {
tbs_pos = r;
}
}
let (issuer_tag, _issuer_body, rest_after_issuer) = parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Crl(format!("CRL issuer: {e}")))?;
if issuer_tag != 0x30 {
return Err(LtvError::Crl(format!(
"expected issuer SEQUENCE, got 0x{issuer_tag:02x}"
)));
}
let issuer_len = tbs_pos.len() - rest_after_issuer.len();
let issuer_der = tbs_pos[..issuer_len].to_vec();
tbs_pos = rest_after_issuer;
let (time_tag, time_body, rest_after_this) = parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Crl(format!("CRL thisUpdate: {e}")))?;
let this_update = parse_x509_time(time_tag, time_body)
.map_err(|e| LtvError::Crl(format!("CRL thisUpdate parse: {e}")))?;
tbs_pos = rest_after_this;
let mut next_update = None;
if !tbs_pos.is_empty() && (tbs_pos[0] == 0x17 || tbs_pos[0] == 0x18) {
let (nt_tag, nt_body, r) = parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Crl(format!("CRL nextUpdate: {e}")))?;
next_update = Some(
parse_x509_time(nt_tag, nt_body)
.map_err(|e| LtvError::Crl(format!("CRL nextUpdate parse: {e}")))?,
);
tbs_pos = r;
}
let mut revoked_entries = Vec::new();
if !tbs_pos.is_empty() && tbs_pos[0] == 0x30 {
let (rc_tag, rc_body, r) = parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Crl(format!("CRL revokedCertificates: {e}")))?;
if rc_tag == 0x30 {
parse_revoked_certificates(rc_body, &mut revoked_entries)?;
}
tbs_pos = r;
}
let _ = tbs_pos;
Ok(ParsedCrl {
tbs_bytes,
signature_algorithm_oid: sig_oid,
signature_bytes,
issuer_der,
this_update,
next_update,
revoked_entries,
})
}
fn parse_oid_from_algorithm_identifier(body: &[u8]) -> Result<const_oid::ObjectIdentifier, LtvError>
{
let (tag, oid_bytes, _) =
parse_tlv_with_rest(body).map_err(|e| LtvError::Crl(format!("AlgId OID: {e}")))?;
if tag != 0x06 {
return Err(LtvError::Crl(format!(
"expected OID (0x06) in AlgorithmIdentifier, got 0x{tag:02x}"
)));
}
const_oid::ObjectIdentifier::from_bytes(oid_bytes)
.map_err(|e| LtvError::Crl(format!("invalid OID: {e}")))
}
fn parse_revoked_certificates(
body: &[u8],
entries: &mut Vec<RevokedEntry>,
) -> Result<(), LtvError> {
let mut pos = body;
while !pos.is_empty() {
let (entry_tag, entry_body, rest) = parse_tlv_with_rest(pos)
.map_err(|e| LtvError::Crl(format!("revoked entry: {e}")))?;
if entry_tag != 0x30 {
return Err(LtvError::Crl(format!(
"expected revoked entry SEQUENCE, got 0x{entry_tag:02x}"
)));
}
let (serial_tag, serial_body, entry_rest) = parse_tlv_with_rest(entry_body)
.map_err(|e| LtvError::Crl(format!("revoked serial: {e}")))?;
if serial_tag != 0x02 {
return Err(LtvError::Crl(format!(
"expected serial INTEGER (0x02), got 0x{serial_tag:02x}"
)));
}
let serial_number = parse_integer_body(serial_body);
let (time_tag, time_body, entry_rest2) = parse_tlv_with_rest(entry_rest)
.map_err(|e| LtvError::Crl(format!("revocation date: {e}")))?;
let revocation_time = parse_x509_time(time_tag, time_body)
.map_err(|e| LtvError::Crl(format!("revocation date parse: {e}")))?;
let reason = parse_revocation_reason(entry_rest2);
entries.push(RevokedEntry {
serial_number,
revocation_time,
reason,
});
pos = rest;
}
Ok(())
}
fn parse_revocation_reason(extensions_area: &[u8]) -> RevocationReason {
if extensions_area.is_empty() {
return RevocationReason::Unspecified;
}
let Ok((tag, ext_body, _)) = parse_tlv_with_rest(extensions_area) else {
return RevocationReason::Unspecified;
};
if tag != 0x30 {
return RevocationReason::Unspecified;
}
let reason_oid_bytes: &[u8] = &[0x55, 0x1D, 0x15];
let mut pos = &ext_body[..];
while !pos.is_empty() {
let Ok((ext_tag, ext_value, rest)) = parse_tlv_with_rest(pos) else {
break;
};
if ext_tag == 0x30 {
if let Some(oid_body) = find_tagged_value(ext_value, 0x06) {
if oid_body == reason_oid_bytes {
if let Some(octet_body) = find_tagged_value(ext_value, 0x04) {
if let Some(enum_body) = find_tagged_value(octet_body, 0x0A) {
if !enum_body.is_empty() {
return RevocationReason::from_code(enum_body[0]);
}
}
}
}
}
}
pos = rest;
}
RevocationReason::Unspecified
}
pub fn verify_crl_signature(
parsed_crl: &ParsedCrl,
issuer: &Certificate,
) -> Result<(), LtvError> {
use der::Encode;
let spki_der = issuer
.tbs_certificate
.subject_public_key_info
.to_der()
.map_err(|e| LtvError::Crl(format!("issuer SPKI encode failed: {e}")))?;
crate::crypto::verify::verify_signature_by_oid(
&parsed_crl.tbs_bytes,
&parsed_crl.signature_bytes,
&spki_der,
&parsed_crl.signature_algorithm_oid,
)
.map_err(|e| LtvError::Crl(format!("CRL signature verification failed: {e}")))
}
pub fn check_revocation(
crl_der: &[u8],
cert: &Certificate,
issuer: &Certificate,
validation_time: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<ValidationStatus, LtvError> {
let now = validation_time.unwrap_or_else(chrono::Utc::now);
let parsed = parse_crl(crl_der)?;
verify_crl_signature(&parsed, issuer)?;
if let Some(next_update) = parsed.next_update {
if now > next_update {
log::warn!("CRL is stale: nextUpdate={next_update}, validation_time={now}");
}
}
let cert_issuer_der = get_cert_issuer_der(cert)?;
if parsed.issuer_der != cert_issuer_der {
return Err(LtvError::Crl(
"CRL issuer does not match certificate issuer".into(),
));
}
let cert_serial = get_cert_serial_body(cert);
for entry in &parsed.revoked_entries {
if integer_bodies_equal(&entry.serial_number, &cert_serial) {
if entry.revocation_time > now {
log::debug!(
"cert serial found in CRL but revocation_time ({}) is in the future relative to validation_time ({})",
entry.revocation_time, now
);
return Ok(ValidationStatus::Valid {
source: RevocationSource::Crl,
checked_at: now,
});
}
return Ok(ValidationStatus::Revoked {
source: RevocationSource::Crl,
reason: entry.reason,
revocation_time: entry.revocation_time,
});
}
}
Ok(ValidationStatus::Valid {
source: RevocationSource::Crl,
checked_at: now,
})
}
fn get_cert_issuer_der(cert: &Certificate) -> Result<Vec<u8>, LtvError> {
use der::Encode;
cert.tbs_certificate
.issuer
.to_der()
.map_err(|e| LtvError::Crl(format!("cert issuer DER encode failed: {e}")))
}
fn get_cert_serial_body(cert: &Certificate) -> Vec<u8> {
let serial = &cert.tbs_certificate.serial_number;
let bytes = serial.as_bytes();
parse_integer_body(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::der_utils::{encode_integer_u64, encode_sequence_from_parts, encode_sequence_raw, encode_tlv};
use der::{Decode, Encode};
#[test]
fn test_crl_client_default() {
let client = CrlClient::new();
assert_eq!(client.grace_period, Duration::from_secs(3600));
}
#[test]
fn test_crl_client_builder() {
let client = CrlClient::new()
.timeout(Duration::from_secs(10))
.grace_period(Duration::from_secs(7200));
assert_eq!(client.grace_period, Duration::from_secs(7200));
}
#[test]
fn test_extract_crl_urls_no_extensions() {
let cert_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
let pem_data = pem_rfc7468::decode_vec(cert_pem.as_bytes());
if let Ok((_label, der)) = pem_data {
if let Ok(cert) = Certificate::from_der(&der) {
let urls = CrlClient::extract_crl_urls(&cert);
let _ = urls;
}
}
}
fn build_test_crl(
issuer_cert: &Certificate,
issuer_key_pem: &str,
revoked_serials: &[(Vec<u8>, &str)], ) -> Vec<u8> {
use rsa::pkcs1v15::SigningKey;
use rsa::signature::Signer;
use rsa::signature::SignatureEncoding;
use rsa::pkcs8::DecodePrivateKey;
use sha2::Sha256;
let mut tbs_body = Vec::new();
tbs_body.extend_from_slice(&encode_integer_u64(1));
let sha256_rsa_oid: &[u8] = &[
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B,
];
let alg_id = encode_sequence_from_parts(&[sha256_rsa_oid, &[0x05, 0x00]]);
tbs_body.extend_from_slice(&alg_id);
let issuer_name_der = issuer_cert
.tbs_certificate
.subject
.to_der()
.unwrap();
tbs_body.extend_from_slice(&issuer_name_der);
let this_update_utc = encode_tlv(0x17, b"260101000000Z");
tbs_body.extend_from_slice(&this_update_utc);
let next_update_utc = encode_tlv(0x17, b"270101000000Z");
tbs_body.extend_from_slice(&next_update_utc);
if !revoked_serials.is_empty() {
let mut revoked_body = Vec::new();
for (serial, time_str) in revoked_serials {
let serial_tlv = encode_tlv(0x02, serial);
let time_tlv = encode_tlv(0x17, time_str.as_bytes());
let entry = encode_sequence_from_parts(&[&serial_tlv, &time_tlv]);
revoked_body.extend_from_slice(&entry);
}
let revoked_seq = encode_sequence_raw(&revoked_body);
tbs_body.extend_from_slice(&revoked_seq);
}
let tbs_der = encode_sequence_raw(&tbs_body);
let key_der = pem_rfc7468::decode_vec(issuer_key_pem.as_bytes())
.unwrap()
.1;
let private_key =
rsa::RsaPrivateKey::from_pkcs8_der(&key_der).unwrap();
let signing_key = SigningKey::<Sha256>::new(private_key);
let signature: rsa::pkcs1v15::Signature = signing_key.sign(&tbs_der);
let sig_bytes = signature.to_vec();
let outer_alg_id = alg_id.clone();
let mut bit_string_value = vec![0x00];
bit_string_value.extend_from_slice(&sig_bytes);
let sig_bit_string = encode_tlv(0x03, &bit_string_value);
let cert_list =
encode_sequence_from_parts(&[&tbs_der, &outer_alg_id, &sig_bit_string]);
cert_list
}
fn load_test_cert(pem_str: &str) -> Certificate {
let (_, der) = pem_rfc7468::decode_vec(pem_str.as_bytes()).unwrap();
Certificate::from_der(&der).unwrap()
}
fn intermediate_ca_cert() -> Certificate {
let pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/intermediate_ca_cert.pem"
));
load_test_cert(pem)
}
fn intermediate_ca_key_pem() -> &'static str {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/intermediate_ca_key.pem"
)
}
fn signer_cert() -> Certificate {
let pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/signer_cert.pem"
));
load_test_cert(pem)
}
#[test]
fn test_parse_crl_empty_revoked_list() {
let key_path = intermediate_ca_key_pem();
let Ok(key_pem) = std::fs::read_to_string(key_path) else {
eprintln!("skipping test: intermediate_ca_key.pem not found (run gen-test-fixtures.sh)");
return;
};
let issuer = intermediate_ca_cert();
let crl_der = build_test_crl(&issuer, &key_pem, &[]);
let parsed = parse_crl(&crl_der).unwrap();
assert!(parsed.revoked_entries.is_empty());
assert!(parsed.next_update.is_some());
assert_eq!(
parsed.this_update.to_rfc3339(),
"2026-01-01T00:00:00+00:00"
);
}
#[test]
fn test_parse_crl_with_revoked_entries() {
let key_path = intermediate_ca_key_pem();
let Ok(key_pem) = std::fs::read_to_string(key_path) else {
eprintln!("skipping test: intermediate_ca_key.pem not found");
return;
};
let issuer = intermediate_ca_cert();
let revoked = vec![
(vec![0x01], "250601120000Z"),
(vec![0x00, 0xFF], "250701120000Z"),
];
let crl_der = build_test_crl(&issuer, &key_pem, &revoked);
let parsed = parse_crl(&crl_der).unwrap();
assert_eq!(parsed.revoked_entries.len(), 2);
assert_eq!(parsed.revoked_entries[0].serial_number, vec![0x01]);
assert_eq!(parsed.revoked_entries[1].serial_number, vec![0xFF]);
}
#[test]
fn test_verify_crl_signature() {
let key_path = intermediate_ca_key_pem();
let Ok(key_pem) = std::fs::read_to_string(key_path) else {
eprintln!("skipping test: intermediate_ca_key.pem not found");
return;
};
let issuer = intermediate_ca_cert();
let crl_der = build_test_crl(&issuer, &key_pem, &[]);
let parsed = parse_crl(&crl_der).unwrap();
let result = verify_crl_signature(&parsed, &issuer);
assert!(result.is_ok(), "CRL signature should verify: {result:?}");
}
#[test]
fn test_verify_crl_signature_wrong_issuer() {
let key_path = intermediate_ca_key_pem();
let Ok(key_pem) = std::fs::read_to_string(key_path) else {
eprintln!("skipping test: intermediate_ca_key.pem not found");
return;
};
let issuer = intermediate_ca_cert();
let crl_der = build_test_crl(&issuer, &key_pem, &[]);
let parsed = parse_crl(&crl_der).unwrap();
let wrong_issuer = signer_cert();
let result = verify_crl_signature(&parsed, &wrong_issuer);
assert!(result.is_err(), "wrong issuer should fail verification");
}
#[test]
fn test_check_revocation_not_revoked() {
let key_path = intermediate_ca_key_pem();
let Ok(key_pem) = std::fs::read_to_string(key_path) else {
eprintln!("skipping test: intermediate_ca_key.pem not found");
return;
};
let issuer = intermediate_ca_cert();
let cert = signer_cert();
let crl_der = build_test_crl(&issuer, &key_pem, &[]);
let validation_time = chrono::DateTime::parse_from_rfc3339("2026-06-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status = check_revocation(&crl_der, &cert, &issuer, Some(validation_time)).unwrap();
assert!(status.is_valid(), "should be valid: {status}");
}
#[test]
fn test_check_revocation_cert_is_revoked() {
let key_path = intermediate_ca_key_pem();
let Ok(key_pem) = std::fs::read_to_string(key_path) else {
eprintln!("skipping test: intermediate_ca_key.pem not found");
return;
};
let issuer = intermediate_ca_cert();
let cert = signer_cert();
let serial = get_cert_serial_body(&cert);
let revoked = vec![(serial, "250601120000Z")];
let crl_der = build_test_crl(&issuer, &key_pem, &revoked);
let validation_time = chrono::DateTime::parse_from_rfc3339("2026-06-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status = check_revocation(&crl_der, &cert, &issuer, Some(validation_time)).unwrap();
assert!(status.is_revoked(), "should be revoked: {status}");
}
#[test]
fn test_check_revocation_time_aware_future_revocation() {
let key_path = intermediate_ca_key_pem();
let Ok(key_pem) = std::fs::read_to_string(key_path) else {
eprintln!("skipping test: intermediate_ca_key.pem not found");
return;
};
let issuer = intermediate_ca_cert();
let cert = signer_cert();
let serial = get_cert_serial_body(&cert);
let revoked = vec![(serial, "270101120000Z")];
let crl_der = build_test_crl(&issuer, &key_pem, &revoked);
let validation_time = chrono::DateTime::parse_from_rfc3339("2026-06-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status = check_revocation(&crl_der, &cert, &issuer, Some(validation_time)).unwrap();
assert!(
status.is_valid(),
"should be valid (revocation in future): {status}"
);
}
#[test]
fn test_parse_crl_invalid_data() {
let result = parse_crl(&[0x04, 0x00]); assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("SEQUENCE"));
}
#[test]
fn test_parse_revocation_reason_unspecified() {
let reason = parse_revocation_reason(&[]);
assert_eq!(reason, RevocationReason::Unspecified);
}
}