use std::time::Duration;
use der::Encode;
use reqwest::Client;
use x509_cert::Certificate;
use crate::der_utils;
use crate::error::LtvError;
use crate::ltv::status::{RevocationReason, RevocationSource, ValidationStatus};
const OCSP_REQUEST_CONTENT_TYPE: &str = "application/ocsp-request";
const OCSP_RESPONSE_SUCCESSFUL: u8 = 0;
const OCSP_BASIC_RESPONSE_OID: &[u8] = &[0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x01];
const OCSP_NONCE_OID_BYTES: &[u8] = &[0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x02];
const OCSP_SIGNING_EKU_OID_BYTES: &[u8] = &[0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x09];
const NONCE_SIZE: usize = 30;
#[derive(Debug, Clone)]
pub enum CertStatus {
Good,
Revoked {
revocation_time: chrono::DateTime<chrono::Utc>,
reason: RevocationReason,
},
Unknown,
}
#[derive(Debug, Clone)]
pub struct SingleResponse {
pub hash_algorithm_oid: Vec<u8>,
pub issuer_name_hash: Vec<u8>,
pub issuer_key_hash: Vec<u8>,
pub serial_number: Vec<u8>,
pub cert_status: CertStatus,
pub this_update: chrono::DateTime<chrono::Utc>,
pub next_update: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug)]
pub struct ParsedBasicOcspResponse {
pub tbs_response_data: Vec<u8>,
pub signature_algorithm_oid: const_oid::ObjectIdentifier,
pub signature_bytes: Vec<u8>,
pub responder_id: ResponderId,
pub produced_at: chrono::DateTime<chrono::Utc>,
pub responses: Vec<SingleResponse>,
pub nonce: Option<Vec<u8>>,
pub embedded_certs_der: Vec<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub enum ResponderId {
ByName(Vec<u8>),
ByKeyHash(Vec<u8>),
}
#[derive(Debug, Clone)]
pub struct OcspClient {
http_client: Client,
timeout: Duration,
}
impl OcspClient {
pub fn new() -> Self {
Self {
http_client: Client::new(),
timeout: Duration::from_secs(30),
}
}
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 extract_ocsp_urls(cert: &Certificate) -> Vec<String> {
extract_aia_urls(cert, AiaAccessMethod::Ocsp)
}
pub async fn fetch_ocsp_response(
&self,
cert: &Certificate,
issuer: &Certificate,
) -> Result<Vec<u8>, LtvError> {
let urls = Self::extract_ocsp_urls(cert);
if urls.is_empty() {
return Err(LtvError::Ocsp(
"no OCSP responder URL in certificate AIA extension".into(),
));
}
let ocsp_request = build_ocsp_request(cert, issuer)?;
let mut last_error = None;
for url in &urls {
match self.send_ocsp_request(url, &ocsp_request).await {
Ok(response) => return Ok(response),
Err(e) => {
log::warn!("OCSP request to {url} failed: {e}");
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or_else(|| {
LtvError::Ocsp("all OCSP responder URLs failed".into())
}))
}
pub async fn fetch_ocsp_response_with_nonce(
&self,
cert: &Certificate,
issuer: &Certificate,
) -> Result<(Vec<u8>, Vec<u8>), LtvError> {
let urls = Self::extract_ocsp_urls(cert);
if urls.is_empty() {
return Err(LtvError::Ocsp(
"no OCSP responder URL in certificate AIA extension".into(),
));
}
let (ocsp_request, nonce) = build_ocsp_request_with_nonce(cert, issuer)?;
let mut last_error = None;
for url in &urls {
match self.send_ocsp_request(url, &ocsp_request).await {
Ok(response) => return Ok((response, nonce)),
Err(e) => {
log::warn!("OCSP request to {url} failed: {e}");
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or_else(|| {
LtvError::Ocsp("all OCSP responder URLs failed".into())
}))
}
async fn send_ocsp_request(
&self,
url: &str,
request_der: &[u8],
) -> Result<Vec<u8>, LtvError> {
log::debug!("Sending OCSP request to {url} ({} bytes)", request_der.len());
let response = self
.http_client
.post(url)
.header("Content-Type", OCSP_REQUEST_CONTENT_TYPE)
.timeout(self.timeout)
.body(request_der.to_vec())
.send()
.await
.map_err(|e| LtvError::Ocsp(format!("OCSP request to {url} failed: {e}")))?;
let status = response.status();
if !status.is_success() {
return Err(LtvError::Ocsp(format!(
"OCSP responder {url} returned HTTP {status}"
)));
}
let resp_bytes = response
.bytes()
.await
.map_err(|e| LtvError::Ocsp(format!("failed to read OCSP response body: {e}")))?
.to_vec();
if resp_bytes.is_empty() {
return Err(LtvError::Ocsp(format!(
"OCSP responder {url} returned empty response"
)));
}
if resp_bytes[0] != 0x30 {
return Err(LtvError::Ocsp(format!(
"OCSP response from {url} does not appear to be DER-encoded"
)));
}
validate_ocsp_response_status(&resp_bytes)?;
log::debug!("OCSP response from {url}: {} bytes", resp_bytes.len());
Ok(resp_bytes)
}
}
impl Default for OcspClient {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AiaAccessMethod {
Ocsp,
CaIssuers,
}
pub fn extract_aia_urls(cert: &Certificate, method: AiaAccessMethod) -> Vec<String> {
let mut urls = Vec::new();
let aia_oid = const_oid::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.1.1");
let method_oid = match method {
AiaAccessMethod::Ocsp => {
const_oid::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.48.1")
}
AiaAccessMethod::CaIssuers => {
const_oid::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.48.2")
}
};
if let Some(extensions) = &cert.tbs_certificate.extensions {
for ext in extensions.iter() {
if ext.extn_id == aia_oid {
if let Ok(parsed) = parse_aia_extension(ext.extn_value.as_bytes(), &method_oid) {
urls.extend(parsed);
}
}
}
}
urls
}
fn parse_aia_extension(
der_bytes: &[u8],
target_method_oid: &const_oid::ObjectIdentifier,
) -> Result<Vec<String>, LtvError> {
let mut urls = Vec::new();
let (tag, body) = der_utils::parse_tlv(der_bytes)
.map_err(|e| LtvError::Ocsp(format!("AIA parse error: {e}")))?;
if tag != 0x30 {
return Err(LtvError::Ocsp(format!("AIA: expected SEQUENCE, got 0x{tag:02x}")));
}
let target_oid_der = target_method_oid
.to_der()
.map_err(|e| LtvError::Ocsp(format!("failed to encode target OID: {e}")))?;
let mut pos = &body[..];
while !pos.is_empty() {
let (ad_tag, ad_body, rest) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| LtvError::Ocsp(format!("AIA parse error: {e}")))?;
if ad_tag == 0x30 {
let (oid_tag, oid_body, ad_rest) = der_utils::parse_tlv_with_rest(&ad_body)
.map_err(|e| LtvError::Ocsp(format!("AIA parse error: {e}")))?;
if oid_tag == 0x06 {
let oid_tlv = der_utils::encode_tlv(0x06, &oid_body);
if oid_tlv == target_oid_der {
if !ad_rest.is_empty() {
let (gn_tag, gn_body, _) = der_utils::parse_tlv_with_rest(ad_rest)
.map_err(|e| LtvError::Ocsp(format!("AIA parse error: {e}")))?;
if gn_tag == 0x86 {
if let Ok(uri) = std::str::from_utf8(&gn_body) {
urls.push(uri.to_string());
}
}
}
}
}
}
pos = rest;
}
Ok(urls)
}
fn build_ocsp_request(
cert: &Certificate,
issuer: &Certificate,
) -> Result<Vec<u8>, LtvError> {
let cert_id = build_cert_id(cert, issuer)?;
let request = der_utils::encode_sequence_from_parts(&[&cert_id]);
let request_list = der_utils::encode_sequence_from_parts(&[&request]);
let tbs_request = der_utils::encode_sequence_from_parts(&[&request_list]);
let ocsp_request = der_utils::encode_sequence_from_parts(&[&tbs_request]);
Ok(ocsp_request)
}
pub fn build_ocsp_request_with_nonce(
cert: &Certificate,
issuer: &Certificate,
) -> Result<(Vec<u8>, Vec<u8>), LtvError> {
let cert_id = build_cert_id(cert, issuer)?;
let request = der_utils::encode_sequence_from_parts(&[&cert_id]);
let request_list = der_utils::encode_sequence_from_parts(&[&request]);
let nonce = generate_nonce();
let nonce_oid_tlv = der_utils::encode_tlv(0x06, OCSP_NONCE_OID_BYTES);
let nonce_inner = der_utils::encode_tlv(0x04, &nonce); let nonce_outer = der_utils::encode_tlv(0x04, &nonce_inner); let nonce_ext = der_utils::encode_sequence_from_parts(&[&nonce_oid_tlv, &nonce_outer]);
let extensions_seq = der_utils::encode_sequence_from_parts(&[&nonce_ext]);
let request_extensions = der_utils::encode_tlv(0xA2, &extensions_seq);
let tbs_request =
der_utils::encode_sequence_from_parts(&[&request_list, &request_extensions]);
let ocsp_request = der_utils::encode_sequence_from_parts(&[&tbs_request]);
Ok((ocsp_request, nonce))
}
fn build_cert_id(
cert: &Certificate,
issuer: &Certificate,
) -> Result<Vec<u8>, LtvError> {
let issuer_name_der = issuer
.tbs_certificate
.subject
.to_der()
.map_err(|e| LtvError::Ocsp(format!("failed to encode issuer name: {e}")))?;
let issuer_name_hash = sha1_hash(&issuer_name_der);
let issuer_key_der = issuer
.tbs_certificate
.subject_public_key_info
.subject_public_key
.raw_bytes()
.to_vec();
let issuer_key_hash = sha1_hash(&issuer_key_der);
let serial_der = cert
.tbs_certificate
.serial_number
.to_der()
.map_err(|e| LtvError::Ocsp(format!("failed to encode serial number: {e}")))?;
let sha1_alg_id = build_sha1_algorithm_identifier()?;
let issuer_name_hash_oct = der_utils::encode_tlv(0x04, &issuer_name_hash);
let issuer_key_hash_oct = der_utils::encode_tlv(0x04, &issuer_key_hash);
Ok(der_utils::encode_sequence_from_parts(&[
&sha1_alg_id,
&issuer_name_hash_oct,
&issuer_key_hash_oct,
&serial_der,
]))
}
fn generate_nonce() -> Vec<u8> {
use std::time::SystemTime;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let mut nonce = Vec::with_capacity(NONCE_SIZE);
let seed = now.as_nanos();
for i in 0..NONCE_SIZE {
let byte = ((seed >> ((i * 7) % 64)) ^ (seed >> ((i * 3) % 64))) as u8;
nonce.push(byte.wrapping_add(i as u8));
}
nonce
}
fn build_sha1_algorithm_identifier() -> Result<Vec<u8>, LtvError> {
let sha1_oid = const_oid::ObjectIdentifier::new_unwrap("1.3.14.3.2.26");
let oid_der = sha1_oid
.to_der()
.map_err(|e| LtvError::Ocsp(format!("failed to encode SHA-1 OID: {e}")))?;
let null_der = vec![0x05, 0x00]; Ok(der_utils::encode_sequence_from_parts(&[&oid_der, &null_der]))
}
fn validate_ocsp_response_status(der_bytes: &[u8]) -> Result<(), LtvError> {
let (tag, body) = der_utils::parse_tlv(der_bytes)
.map_err(|e| LtvError::Ocsp(format!("OCSP response parse error: {e}")))?;
if tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"OCSP response: expected SEQUENCE, got 0x{tag:02x}"
)));
}
let (status_tag, status_body, _) = der_utils::parse_tlv_with_rest(&body)
.map_err(|e| LtvError::Ocsp(format!("OCSP response parse error: {e}")))?;
if status_tag != 0x0A {
return Err(LtvError::Ocsp(format!(
"OCSP response: expected ENUMERATED status, got 0x{status_tag:02x}"
)));
}
if status_body.is_empty() {
return Err(LtvError::Ocsp("OCSP response: empty status".into()));
}
let status = status_body[0];
if status != OCSP_RESPONSE_SUCCESSFUL {
let status_name = match status {
1 => "malformedRequest",
2 => "internalError",
3 => "tryLater",
5 => "sigRequired",
6 => "unauthorized",
_ => "unknown",
};
return Err(LtvError::Ocsp(format!(
"OCSP response status: {status_name} ({status})"
)));
}
Ok(())
}
pub fn parse_ocsp_response(response_der: &[u8]) -> Result<ParsedBasicOcspResponse, LtvError> {
let (outer_tag, outer_body) = der_utils::parse_tlv(response_der)
.map_err(|e| LtvError::Ocsp(format!("OCSPResponse outer: {e}")))?;
if outer_tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"expected OCSPResponse SEQUENCE, got 0x{outer_tag:02x}"
)));
}
let (status_tag, status_body, rest) = der_utils::parse_tlv_with_rest(&outer_body)
.map_err(|e| LtvError::Ocsp(format!("responseStatus: {e}")))?;
if status_tag != 0x0A || status_body.is_empty() {
return Err(LtvError::Ocsp("invalid responseStatus".into()));
}
if status_body[0] != OCSP_RESPONSE_SUCCESSFUL {
return Err(LtvError::Ocsp(format!(
"OCSP response not successful: {}",
status_body[0]
)));
}
if rest.is_empty() {
return Err(LtvError::Ocsp(
"OCSP response successful but no responseBytes".into(),
));
}
let (rb_tag, rb_body, _) = der_utils::parse_tlv_with_rest(rest)
.map_err(|e| LtvError::Ocsp(format!("responseBytes: {e}")))?;
if rb_tag != 0xA0 {
return Err(LtvError::Ocsp(format!(
"expected responseBytes [0], got 0x{rb_tag:02x}"
)));
}
let (rb_seq_tag, rb_seq_body) = der_utils::parse_tlv(&rb_body)
.map_err(|e| LtvError::Ocsp(format!("ResponseBytes SEQUENCE: {e}")))?;
if rb_seq_tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"expected ResponseBytes SEQUENCE, got 0x{rb_seq_tag:02x}"
)));
}
let (oid_tag, oid_body, rb_rest) = der_utils::parse_tlv_with_rest(&rb_seq_body)
.map_err(|e| LtvError::Ocsp(format!("responseType OID: {e}")))?;
if oid_tag != 0x06 {
return Err(LtvError::Ocsp("expected responseType OID".into()));
}
if oid_body != OCSP_BASIC_RESPONSE_OID {
return Err(LtvError::Ocsp(
"responseType is not id-pkix-ocsp-basic".into(),
));
}
let (oct_tag, oct_body, _) = der_utils::parse_tlv_with_rest(rb_rest)
.map_err(|e| LtvError::Ocsp(format!("response OCTET STRING: {e}")))?;
if oct_tag != 0x04 {
return Err(LtvError::Ocsp("expected response OCTET STRING".into()));
}
parse_basic_ocsp_response(&oct_body)
}
fn parse_basic_ocsp_response(der: &[u8]) -> Result<ParsedBasicOcspResponse, LtvError> {
let (tag, body) = der_utils::parse_tlv(der)
.map_err(|e| LtvError::Ocsp(format!("BasicOCSPResponse: {e}")))?;
if tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"expected BasicOCSPResponse SEQUENCE, got 0x{tag:02x}"
)));
}
let (tbs_tag, tbs_value, rest) = der_utils::parse_tlv_with_rest(&body)
.map_err(|e| LtvError::Ocsp(format!("tbsResponseData: {e}")))?;
if tbs_tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"expected tbsResponseData SEQUENCE, got 0x{tbs_tag:02x}"
)));
}
let tbs_consumed = body.len() - rest.len();
let tbs_response_data = body[..tbs_consumed].to_vec();
let (sig_alg_tag, sig_alg_body, rest) = der_utils::parse_tlv_with_rest(rest)
.map_err(|e| LtvError::Ocsp(format!("signatureAlgorithm: {e}")))?;
if sig_alg_tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"expected signatureAlgorithm SEQUENCE, got 0x{sig_alg_tag:02x}"
)));
}
let sig_alg_oid = parse_oid_from_algorithm_identifier(&sig_alg_body)?;
let (sig_tag, sig_body, rest) = der_utils::parse_tlv_with_rest(rest)
.map_err(|e| LtvError::Ocsp(format!("signature BIT STRING: {e}")))?;
if sig_tag != 0x03 {
return Err(LtvError::Ocsp(format!(
"expected signature BIT STRING, got 0x{sig_tag:02x}"
)));
}
if sig_body.is_empty() {
return Err(LtvError::Ocsp("empty signature BIT STRING".into()));
}
let signature_bytes = sig_body[1..].to_vec();
let mut embedded_certs_der = Vec::new();
if !rest.is_empty() {
let (certs_tag, certs_body, _) = der_utils::parse_tlv_with_rest(rest)
.map_err(|e| LtvError::Ocsp(format!("certs [0]: {e}")))?;
if certs_tag == 0xA0 {
let (seq_tag, seq_body) = der_utils::parse_tlv(&certs_body)
.map_err(|e| LtvError::Ocsp(format!("certs SEQUENCE: {e}")))?;
if seq_tag == 0x30 {
let mut cert_pos = &seq_body[..];
while !cert_pos.is_empty() {
let (cert_tag, _cert_value, cert_rest) =
der_utils::parse_tlv_with_rest(cert_pos)
.map_err(|e| LtvError::Ocsp(format!("embedded cert: {e}")))?;
if cert_tag == 0x30 {
let cert_len = cert_pos.len() - cert_rest.len();
embedded_certs_der.push(cert_pos[..cert_len].to_vec());
}
cert_pos = cert_rest;
}
}
}
}
let mut tbs_pos = &tbs_value[..];
if !tbs_pos.is_empty() && tbs_pos[0] == 0xA0 {
let (_, _, r) = der_utils::parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Ocsp(format!("version: {e}")))?;
tbs_pos = r;
}
let responder_id = if !tbs_pos.is_empty() && tbs_pos[0] == 0xA1 {
let (_, name_body, r) = der_utils::parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Ocsp(format!("responderID byName: {e}")))?;
tbs_pos = r;
ResponderId::ByName(name_body.to_vec())
} else if !tbs_pos.is_empty() && tbs_pos[0] == 0xA2 {
let (_, hash_wrapper, r) = der_utils::parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Ocsp(format!("responderID byKeyHash: {e}")))?;
tbs_pos = r;
let (oct_tag, oct_body, _) = der_utils::parse_tlv_with_rest(hash_wrapper)
.map_err(|e| LtvError::Ocsp(format!("responderID keyHash OCTET STRING: {e}")))?;
if oct_tag != 0x04 {
return Err(LtvError::Ocsp(format!(
"expected OCTET STRING in byKeyHash, got 0x{oct_tag:02x}"
)));
}
ResponderId::ByKeyHash(oct_body.to_vec())
} else {
return Err(LtvError::Ocsp("missing or unknown responderID".into()));
};
let (pa_tag, pa_body, rest_after_pa) = der_utils::parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Ocsp(format!("producedAt: {e}")))?;
if pa_tag != 0x18 {
return Err(LtvError::Ocsp(format!(
"expected producedAt GeneralizedTime (0x18), got 0x{pa_tag:02x}"
)));
}
let produced_at = der_utils::parse_generalized_time(pa_body)
.map_err(|e| LtvError::Ocsp(format!("producedAt parse: {e}")))?;
tbs_pos = rest_after_pa;
let (resp_seq_tag, resp_seq_body, rest_after_responses) =
der_utils::parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Ocsp(format!("responses SEQUENCE: {e}")))?;
if resp_seq_tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"expected responses SEQUENCE, got 0x{resp_seq_tag:02x}"
)));
}
let mut responses = Vec::new();
let mut sr_pos = &resp_seq_body[..];
while !sr_pos.is_empty() {
let (sr_tag, sr_body, sr_rest) = der_utils::parse_tlv_with_rest(sr_pos)
.map_err(|e| LtvError::Ocsp(format!("SingleResponse: {e}")))?;
if sr_tag == 0x30 {
responses.push(parse_single_response(sr_body)?);
}
sr_pos = sr_rest;
}
tbs_pos = rest_after_responses;
let mut nonce = None;
if !tbs_pos.is_empty() && tbs_pos[0] == 0xA1 {
let (_, ext_wrapper, _) = der_utils::parse_tlv_with_rest(tbs_pos)
.map_err(|e| LtvError::Ocsp(format!("responseExtensions: {e}")))?;
nonce = extract_nonce_from_extensions(&ext_wrapper);
}
Ok(ParsedBasicOcspResponse {
tbs_response_data,
signature_algorithm_oid: sig_alg_oid,
signature_bytes,
responder_id,
produced_at,
responses,
nonce,
embedded_certs_der,
})
}
fn parse_single_response(body: &[u8]) -> Result<SingleResponse, LtvError> {
let mut pos = body;
let (cid_tag, cid_body, rest) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| LtvError::Ocsp(format!("CertID: {e}")))?;
if cid_tag != 0x30 {
return Err(LtvError::Ocsp(format!(
"expected CertID SEQUENCE, got 0x{cid_tag:02x}"
)));
}
pos = rest;
let (alg_tag, alg_body, cid_rest) = der_utils::parse_tlv_with_rest(cid_body)
.map_err(|e| LtvError::Ocsp(format!("CertID hashAlgorithm: {e}")))?;
if alg_tag != 0x30 {
return Err(LtvError::Ocsp("expected hashAlgorithm SEQUENCE".into()));
}
let (oid_tag, oid_body, _) = der_utils::parse_tlv_with_rest(&alg_body)
.map_err(|e| LtvError::Ocsp(format!("hashAlgorithm OID: {e}")))?;
if oid_tag != 0x06 {
return Err(LtvError::Ocsp("expected OID in hashAlgorithm".into()));
}
let hash_algorithm_oid = oid_body.to_vec();
let (inh_tag, inh_body, cid_rest) = der_utils::parse_tlv_with_rest(cid_rest)
.map_err(|e| LtvError::Ocsp(format!("issuerNameHash: {e}")))?;
if inh_tag != 0x04 {
return Err(LtvError::Ocsp("expected issuerNameHash OCTET STRING".into()));
}
let issuer_name_hash = inh_body.to_vec();
let (ikh_tag, ikh_body, cid_rest) = der_utils::parse_tlv_with_rest(cid_rest)
.map_err(|e| LtvError::Ocsp(format!("issuerKeyHash: {e}")))?;
if ikh_tag != 0x04 {
return Err(LtvError::Ocsp("expected issuerKeyHash OCTET STRING".into()));
}
let issuer_key_hash = ikh_body.to_vec();
let (sn_tag, sn_body, _) = der_utils::parse_tlv_with_rest(cid_rest)
.map_err(|e| LtvError::Ocsp(format!("serialNumber: {e}")))?;
if sn_tag != 0x02 {
return Err(LtvError::Ocsp("expected serialNumber INTEGER".into()));
}
let serial_number = der_utils::parse_integer_body(sn_body);
let (cs_tag, cs_body, rest_after_status) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| LtvError::Ocsp(format!("certStatus: {e}")))?;
pos = rest_after_status;
let cert_status = match cs_tag {
0x80 => {
CertStatus::Good
}
0xA1 => {
parse_revoked_info(cs_body)?
}
0x82 => {
CertStatus::Unknown
}
_ => {
return Err(LtvError::Ocsp(format!(
"unknown certStatus tag: 0x{cs_tag:02x}"
)));
}
};
let (tu_tag, tu_body, rest_after_tu) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| LtvError::Ocsp(format!("thisUpdate: {e}")))?;
if tu_tag != 0x18 {
return Err(LtvError::Ocsp(format!(
"expected thisUpdate GeneralizedTime, got 0x{tu_tag:02x}"
)));
}
let this_update = der_utils::parse_generalized_time(tu_body)
.map_err(|e| LtvError::Ocsp(format!("thisUpdate parse: {e}")))?;
pos = rest_after_tu;
let mut next_update = None;
if !pos.is_empty() && pos[0] == 0xA0 {
let (_, nu_inner, rest_after_nu) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| LtvError::Ocsp(format!("nextUpdate [0]: {e}")))?;
let (nu_tag, nu_body, _) = der_utils::parse_tlv_with_rest(nu_inner)
.map_err(|e| LtvError::Ocsp(format!("nextUpdate GeneralizedTime: {e}")))?;
if nu_tag == 0x18 {
next_update = Some(
der_utils::parse_generalized_time(nu_body)
.map_err(|e| LtvError::Ocsp(format!("nextUpdate parse: {e}")))?,
);
}
pos = rest_after_nu;
}
let _ = pos;
Ok(SingleResponse {
hash_algorithm_oid,
issuer_name_hash,
issuer_key_hash,
serial_number,
cert_status,
this_update,
next_update,
})
}
fn parse_revoked_info(body: &[u8]) -> Result<CertStatus, LtvError> {
let (rt_tag, rt_body, rest) = der_utils::parse_tlv_with_rest(body)
.map_err(|e| LtvError::Ocsp(format!("revocationTime: {e}")))?;
if rt_tag != 0x18 {
return Err(LtvError::Ocsp(format!(
"expected revocationTime GeneralizedTime, got 0x{rt_tag:02x}"
)));
}
let revocation_time = der_utils::parse_generalized_time(rt_body)
.map_err(|e| LtvError::Ocsp(format!("revocationTime parse: {e}")))?;
let mut reason = RevocationReason::Unspecified;
if !rest.is_empty() && rest[0] == 0xA0 {
let (_, reason_inner, _) = der_utils::parse_tlv_with_rest(rest)
.map_err(|e| LtvError::Ocsp(format!("revocationReason: {e}")))?;
if let Some(enum_body) = der_utils::find_tagged_value(reason_inner, 0x0A) {
if !enum_body.is_empty() {
reason = RevocationReason::from_code(enum_body[0]);
}
}
}
Ok(CertStatus::Revoked {
revocation_time,
reason,
})
}
fn parse_oid_from_algorithm_identifier(
body: &[u8],
) -> Result<const_oid::ObjectIdentifier, LtvError> {
let (tag, oid_bytes, _) = der_utils::parse_tlv_with_rest(body)
.map_err(|e| LtvError::Ocsp(format!("AlgId OID: {e}")))?;
if tag != 0x06 {
return Err(LtvError::Ocsp(format!(
"expected OID (0x06) in AlgorithmIdentifier, got 0x{tag:02x}"
)));
}
const_oid::ObjectIdentifier::from_bytes(oid_bytes)
.map_err(|e| LtvError::Ocsp(format!("invalid OID: {e}")))
}
fn extract_nonce_from_extensions(ext_area: &[u8]) -> Option<Vec<u8>> {
let (tag, ext_body) = der_utils::parse_tlv(ext_area).ok()?;
if tag != 0x30 {
return None;
}
let mut pos = &ext_body[..];
while !pos.is_empty() {
let (ext_tag, ext_value, rest) = der_utils::parse_tlv_with_rest(pos).ok()?;
if ext_tag == 0x30 {
if let Some(oid_body) = der_utils::find_tagged_value(ext_value, 0x06) {
if oid_body == OCSP_NONCE_OID_BYTES {
if let Some(octet_body) = der_utils::find_tagged_value(ext_value, 0x04) {
if !octet_body.is_empty() && octet_body[0] == 0x04 {
if let Ok((inner_tag, inner_body)) = der_utils::parse_tlv(octet_body) {
if inner_tag == 0x04 {
return Some(inner_body);
}
}
}
return Some(octet_body.to_vec());
}
}
}
}
pos = rest;
}
None
}
fn verify_ocsp_response_signature(
parsed: &ParsedBasicOcspResponse,
issuer: &Certificate,
) -> Result<Certificate, LtvError> {
use der::Decode;
let mut candidates: Vec<Certificate> = Vec::new();
for cert_der in &parsed.embedded_certs_der {
if let Ok(cert) = Certificate::from_der(cert_der) {
candidates.push(cert);
}
}
candidates.push(issuer.clone());
for candidate in &candidates {
let spki_der = match candidate.tbs_certificate.subject_public_key_info.to_der() {
Ok(d) => d,
Err(_) => continue,
};
let result = crate::crypto::verify::verify_signature_by_oid(
&parsed.tbs_response_data,
&parsed.signature_bytes,
&spki_der,
&parsed.signature_algorithm_oid,
);
if result.is_ok() {
return Ok(candidate.clone());
}
}
Err(LtvError::Ocsp(
"OCSP response signature could not be verified against any candidate certificate".into(),
))
}
fn validate_responder_trust(
responder_cert: &Certificate,
issuer: &Certificate,
) -> Result<(), LtvError> {
if certs_have_same_subject(responder_cert, issuer) {
return Ok(());
}
let issuer_signed = crate::crypto::verify::verify_certificate_signature(responder_cert, issuer);
if issuer_signed.is_err() {
return Err(LtvError::Ocsp(
"OCSP responder certificate is not issued by the expected CA".into(),
));
}
if !has_ocsp_signing_eku(responder_cert) {
return Err(LtvError::Ocsp(
"OCSP responder certificate lacks id-kp-OCSPSigning EKU".into(),
));
}
Ok(())
}
fn certs_have_same_subject(a: &Certificate, b: &Certificate) -> bool {
let a_der = a.tbs_certificate.subject.to_der();
let b_der = b.tbs_certificate.subject.to_der();
match (a_der, b_der) {
(Ok(ad), Ok(bd)) => ad == bd,
_ => false,
}
}
fn has_ocsp_signing_eku(cert: &Certificate) -> bool {
let eku_oid = const_oid::ObjectIdentifier::new_unwrap("2.5.29.37");
if let Some(extensions) = &cert.tbs_certificate.extensions {
for ext in extensions.iter() {
if ext.extn_id == eku_oid {
return eku_contains_oid(ext.extn_value.as_bytes(), OCSP_SIGNING_EKU_OID_BYTES);
}
}
}
false
}
fn eku_contains_oid(eku_der: &[u8], target_oid_bytes: &[u8]) -> bool {
let Ok((tag, body)) = der_utils::parse_tlv(eku_der) else {
return false;
};
if tag != 0x30 {
return false;
}
let mut pos = &body[..];
while !pos.is_empty() {
let Ok((oid_tag, oid_body, rest)) = der_utils::parse_tlv_with_rest(pos) else {
break;
};
if oid_tag == 0x06 && oid_body == target_oid_bytes {
return true;
}
pos = rest;
}
false
}
pub fn has_ocsp_nocheck_extension(cert: &Certificate) -> bool {
let nocheck_oid = const_oid::ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.48.1.5");
if let Some(extensions) = &cert.tbs_certificate.extensions {
for ext in extensions.iter() {
if ext.extn_id == nocheck_oid {
return true;
}
}
}
false
}
fn validate_nonce(
request_nonce: &[u8],
response_nonce: &[u8],
) -> Result<(), LtvError> {
if response_nonce == request_nonce {
return Ok(());
}
let wrapped_nonce = der_utils::encode_tlv(0x04, request_nonce);
if response_nonce == wrapped_nonce.as_slice() {
return Ok(());
}
if !response_nonce.is_empty() && response_nonce[0] == 0x04 {
if let Ok((_, inner)) = der_utils::parse_tlv(response_nonce) {
if inner == request_nonce {
return Ok(());
}
}
}
Err(LtvError::Ocsp("OCSP response nonce mismatch".into()))
}
pub fn check_revocation(
response_der: &[u8],
cert: &Certificate,
issuer: &Certificate,
nonce: Option<&[u8]>,
validation_time: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<ValidationStatus, LtvError> {
let now = validation_time.unwrap_or_else(chrono::Utc::now);
let parsed = parse_ocsp_response(response_der)?;
let responder_cert = verify_ocsp_response_signature(&parsed, issuer)?;
validate_responder_trust(&responder_cert, issuer)?;
if let Some(request_nonce) = nonce {
match &parsed.nonce {
Some(response_nonce) => {
validate_nonce(request_nonce, response_nonce)?;
}
None => {
log::warn!("OCSP response does not contain a nonce (nonce was requested)");
}
}
}
let issuer_name_der = issuer
.tbs_certificate
.subject
.to_der()
.map_err(|e| LtvError::Ocsp(format!("issuer name encode: {e}")))?;
let expected_name_hash = sha1_hash(&issuer_name_der);
let issuer_key_bytes = issuer
.tbs_certificate
.subject_public_key_info
.subject_public_key
.raw_bytes()
.to_vec();
let expected_key_hash = sha1_hash(&issuer_key_bytes);
let cert_serial = der_utils::parse_integer_body(cert.tbs_certificate.serial_number.as_bytes());
let matching_response = parsed.responses.iter().find(|sr| {
sr.issuer_name_hash == expected_name_hash
&& sr.issuer_key_hash == expected_key_hash
&& der_utils::integer_bodies_equal(&sr.serial_number, &cert_serial)
});
let sr = match matching_response {
Some(sr) => sr,
None => {
return Ok(ValidationStatus::Unknown {
reason: "certificate not found in OCSP response".into(),
});
}
};
match &sr.cert_status {
CertStatus::Good => Ok(ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: now,
}),
CertStatus::Revoked {
revocation_time,
reason,
} => {
if *revocation_time > now {
log::debug!(
"cert found revoked in OCSP but revocation_time ({}) is after validation_time ({})",
revocation_time, now
);
Ok(ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: now,
})
} else {
Ok(ValidationStatus::Revoked {
source: RevocationSource::Ocsp,
reason: *reason,
revocation_time: *revocation_time,
})
}
}
CertStatus::Unknown => Ok(ValidationStatus::Unknown {
reason: "OCSP responder reported certificate status as unknown".into(),
}),
}
}
fn sha1_hash(data: &[u8]) -> Vec<u8> {
use sha1::Digest;
sha1::Sha1::digest(data).to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
use der::Decode;
#[test]
fn test_ocsp_client_default() {
let client = OcspClient::new();
assert_eq!(client.timeout, Duration::from_secs(30));
}
#[test]
fn test_sha1_hash() {
let hash = sha1_hash(b"test");
assert_eq!(hash.len(), 20); }
#[test]
fn test_build_sha1_algorithm_identifier() {
let alg_id = build_sha1_algorithm_identifier().unwrap();
assert_eq!(alg_id[0], 0x30); }
#[test]
fn test_extract_ocsp_urls_from_fixture_cert() {
let cert_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/signer_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 = OcspClient::extract_ocsp_urls(&cert);
let _ = urls;
}
}
}
#[test]
fn test_generate_nonce() {
let nonce1 = generate_nonce();
assert_eq!(nonce1.len(), NONCE_SIZE);
let nonce2 = generate_nonce();
assert_eq!(nonce2.len(), NONCE_SIZE);
}
#[test]
fn test_build_ocsp_request_with_nonce() {
let cert_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/signer_cert.pem"
));
let issuer_pem = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/intermediate_ca_cert.pem"
));
let (_, cert_der) = pem_rfc7468::decode_vec(cert_pem.as_bytes()).unwrap();
let (_, issuer_der) = pem_rfc7468::decode_vec(issuer_pem.as_bytes()).unwrap();
let cert = Certificate::from_der(&cert_der).unwrap();
let issuer = Certificate::from_der(&issuer_der).unwrap();
let (request, nonce) = build_ocsp_request_with_nonce(&cert, &issuer).unwrap();
assert!(!request.is_empty());
assert_eq!(request[0], 0x30); assert_eq!(nonce.len(), NONCE_SIZE);
}
#[test]
fn test_validate_nonce_direct_match() {
let nonce = b"test-nonce-12345678901234567890";
assert!(validate_nonce(nonce, nonce).is_ok());
}
#[test]
fn test_validate_nonce_der_wrapped() {
let nonce = b"test-nonce-data-here";
let wrapped = der_utils::encode_tlv(0x04, nonce);
assert!(validate_nonce(nonce, &wrapped).is_ok());
}
#[test]
fn test_validate_nonce_mismatch() {
let nonce1 = b"nonce-one-aaaaaaaaaaaaaaaaaaa";
let nonce2 = b"nonce-two-bbbbbbbbbbbbbbbbbbb";
assert!(validate_nonce(nonce1, nonce2).is_err());
}
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_path() -> &'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)
}
fn build_test_ocsp_response(
issuer_cert: &Certificate,
issuer_key_pem: &str,
cert: &Certificate,
status: &CertStatus,
nonce: Option<&[u8]>,
) -> Vec<u8> {
use rsa::pkcs1v15::SigningKey;
use rsa::signature::{Signer, SignatureEncoding};
use rsa::pkcs8::DecodePrivateKey;
use sha2::Sha256;
let mut tbs_body = Vec::new();
let issuer_subject_der = issuer_cert
.tbs_certificate
.subject
.to_der()
.unwrap();
let responder_id = der_utils::encode_tlv(0xA1, &issuer_subject_der);
tbs_body.extend_from_slice(&responder_id);
let produced_at = der_utils::encode_tlv(0x18, b"20260601120000Z");
tbs_body.extend_from_slice(&produced_at);
let single_response = build_test_single_response(issuer_cert, cert, status);
let responses_seq = der_utils::encode_sequence_from_parts(&[&single_response]);
tbs_body.extend_from_slice(&responses_seq);
if let Some(nonce_bytes) = nonce {
let nonce_oid_tlv = der_utils::encode_tlv(0x06, OCSP_NONCE_OID_BYTES);
let nonce_inner = der_utils::encode_tlv(0x04, nonce_bytes);
let nonce_outer = der_utils::encode_tlv(0x04, &nonce_inner);
let nonce_ext = der_utils::encode_sequence_from_parts(&[&nonce_oid_tlv, &nonce_outer]);
let extensions_seq = der_utils::encode_sequence_from_parts(&[&nonce_ext]);
let response_extensions = der_utils::encode_tlv(0xA1, &extensions_seq);
tbs_body.extend_from_slice(&response_extensions);
}
let tbs_der = der_utils::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 sha256_rsa_oid: &[u8] = &[
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B,
];
let alg_id = der_utils::encode_sequence_from_parts(&[sha256_rsa_oid, &[0x05, 0x00]]);
let mut bit_string_value = vec![0x00];
bit_string_value.extend_from_slice(&sig_bytes);
let sig_bit_string = der_utils::encode_tlv(0x03, &bit_string_value);
let basic_response =
der_utils::encode_sequence_from_parts(&[&tbs_der, &alg_id, &sig_bit_string]);
let basic_oid_tlv = der_utils::encode_tlv(0x06, OCSP_BASIC_RESPONSE_OID);
let basic_octet = der_utils::encode_tlv(0x04, &basic_response);
let response_bytes_seq =
der_utils::encode_sequence_from_parts(&[&basic_oid_tlv, &basic_octet]);
let response_bytes_tagged = der_utils::encode_tlv(0xA0, &response_bytes_seq);
let response_status = der_utils::encode_tlv(0x0A, &[0x00]);
der_utils::encode_sequence_from_parts(&[&response_status, &response_bytes_tagged])
}
fn build_test_single_response(
issuer_cert: &Certificate,
cert: &Certificate,
status: &CertStatus,
) -> Vec<u8> {
let issuer_name_der = issuer_cert
.tbs_certificate
.subject
.to_der()
.unwrap();
let issuer_name_hash = sha1_hash(&issuer_name_der);
let issuer_key_bytes = issuer_cert
.tbs_certificate
.subject_public_key_info
.subject_public_key
.raw_bytes()
.to_vec();
let issuer_key_hash = sha1_hash(&issuer_key_bytes);
let serial = cert.tbs_certificate.serial_number.to_der().unwrap();
let sha1_alg_id = build_sha1_algorithm_identifier().unwrap();
let name_hash_oct = der_utils::encode_tlv(0x04, &issuer_name_hash);
let key_hash_oct = der_utils::encode_tlv(0x04, &issuer_key_hash);
let cert_id = der_utils::encode_sequence_from_parts(&[
&sha1_alg_id,
&name_hash_oct,
&key_hash_oct,
&serial,
]);
let cert_status_der = match status {
CertStatus::Good => {
der_utils::encode_tlv(0x80, &[])
}
CertStatus::Revoked {
revocation_time,
reason,
} => {
let rt_str = revocation_time.format("%Y%m%d%H%M%SZ").to_string();
let mut revoked_body = der_utils::encode_tlv(0x18, rt_str.as_bytes());
if *reason != RevocationReason::Unspecified {
let reason_enum = der_utils::encode_tlv(0x0A, &[reason.code()]);
let reason_explicit = der_utils::encode_tlv(0xA0, &reason_enum);
revoked_body.extend_from_slice(&reason_explicit);
}
der_utils::encode_tlv(0xA1, &revoked_body)
}
CertStatus::Unknown => {
der_utils::encode_tlv(0x82, &[])
}
};
let this_update = der_utils::encode_tlv(0x18, b"20260601120000Z");
let next_update_gt = der_utils::encode_tlv(0x18, b"20260608120000Z");
let next_update = der_utils::encode_tlv(0xA0, &next_update_gt);
der_utils::encode_sequence_from_parts(&[
&cert_id,
&cert_status_der,
&this_update,
&next_update,
])
}
#[test]
fn test_parse_ocsp_response_good() {
let key_path = intermediate_ca_key_pem_path();
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 response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &CertStatus::Good, None);
let parsed = parse_ocsp_response(&response_der).unwrap();
assert_eq!(parsed.responses.len(), 1);
assert!(matches!(parsed.responses[0].cert_status, CertStatus::Good));
assert!(parsed.nonce.is_none());
assert!(parsed.embedded_certs_der.is_empty());
}
#[test]
fn test_parse_ocsp_response_revoked() {
let key_path = intermediate_ca_key_pem_path();
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 revocation_time =
chrono::DateTime::parse_from_rfc3339("2026-03-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status = CertStatus::Revoked {
revocation_time,
reason: RevocationReason::KeyCompromise,
};
let response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &status, None);
let parsed = parse_ocsp_response(&response_der).unwrap();
assert_eq!(parsed.responses.len(), 1);
match &parsed.responses[0].cert_status {
CertStatus::Revoked { reason, .. } => {
assert_eq!(*reason, RevocationReason::KeyCompromise);
}
other => panic!("expected Revoked, got: {other:?}"),
}
}
#[test]
fn test_parse_ocsp_response_with_nonce() {
let key_path = intermediate_ca_key_pem_path();
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 nonce = b"test-nonce-1234567890abcdef1234";
let response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &CertStatus::Good, Some(nonce));
let parsed = parse_ocsp_response(&response_der).unwrap();
assert!(parsed.nonce.is_some());
assert_eq!(parsed.nonce.as_deref(), Some(nonce.as_slice()));
}
#[test]
fn test_check_revocation_good() {
let key_path = intermediate_ca_key_pem_path();
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 response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &CertStatus::Good, None);
let validation_time =
chrono::DateTime::parse_from_rfc3339("2026-06-01T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status =
check_revocation(&response_der, &cert, &issuer, None, Some(validation_time)).unwrap();
assert!(status.is_valid(), "should be valid: {status}");
}
#[test]
fn test_check_revocation_revoked() {
let key_path = intermediate_ca_key_pem_path();
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 revocation_time =
chrono::DateTime::parse_from_rfc3339("2026-03-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let cert_status = CertStatus::Revoked {
revocation_time,
reason: RevocationReason::Superseded,
};
let response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &cert_status, None);
let validation_time =
chrono::DateTime::parse_from_rfc3339("2026-06-01T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status =
check_revocation(&response_der, &cert, &issuer, None, 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_path();
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 revocation_time =
chrono::DateTime::parse_from_rfc3339("2027-01-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let cert_status = CertStatus::Revoked {
revocation_time,
reason: RevocationReason::Unspecified,
};
let response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &cert_status, None);
let validation_time =
chrono::DateTime::parse_from_rfc3339("2026-06-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status =
check_revocation(&response_der, &cert, &issuer, None, Some(validation_time)).unwrap();
assert!(
status.is_valid(),
"should be valid (revocation in future): {status}"
);
}
#[test]
fn test_check_revocation_unknown_status() {
let key_path = intermediate_ca_key_pem_path();
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 response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &CertStatus::Unknown, None);
let validation_time =
chrono::DateTime::parse_from_rfc3339("2026-06-01T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status =
check_revocation(&response_der, &cert, &issuer, None, Some(validation_time)).unwrap();
assert!(status.is_unknown(), "should be unknown: {status}");
}
#[test]
fn test_check_revocation_with_valid_nonce() {
let key_path = intermediate_ca_key_pem_path();
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 nonce = b"test-nonce-1234567890abcdef1234";
let response_der = build_test_ocsp_response(
&issuer,
&key_pem,
&cert,
&CertStatus::Good,
Some(nonce),
);
let validation_time =
chrono::DateTime::parse_from_rfc3339("2026-06-01T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status = check_revocation(
&response_der,
&cert,
&issuer,
Some(nonce),
Some(validation_time),
)
.unwrap();
assert!(status.is_valid(), "should be valid with nonce: {status}");
}
#[test]
fn test_parse_ocsp_response_invalid_data() {
let result = parse_ocsp_response(&[0x04, 0x00]); assert!(result.is_err());
}
#[test]
fn test_has_ocsp_nocheck_extension() {
let cert = signer_cert();
assert!(!has_ocsp_nocheck_extension(&cert));
}
#[test]
fn test_responder_id_variants() {
let key_path = intermediate_ca_key_pem_path();
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 response_der =
build_test_ocsp_response(&issuer, &key_pem, &cert, &CertStatus::Good, None);
let parsed = parse_ocsp_response(&response_der).unwrap();
assert!(matches!(parsed.responder_id, ResponderId::ByName(_)));
}
}