use std::time::Duration;
use der::Encode;
use x509_cert::Certificate;
use crate::der_utils;
use crate::error::LtvError;
use crate::ltv::status::{RevocationReason, RevocationSource, ValidationStatus};
use crate::net::AttestedHttpClient;
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: spki::AlgorithmIdentifierOwned,
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: AttestedHttpClient,
timeout: Duration,
}
impl OcspClient {
pub fn new() -> Result<Self, LtvError> {
Ok(Self {
http_client: crate::net::hardened_http_client()?,
timeout: Duration::from_secs(30),
})
}
pub fn http_client(mut self, client: AttestedHttpClient) -> Self {
self.http_client = client;
self
}
#[cfg(not(feature = "fips"))]
pub fn unverified_http_client(mut self, client: reqwest::Client) -> Result<Self, LtvError> {
self.http_client = crate::net::unverified_http_client(client)?;
Ok(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> {
match tokio::time::timeout(self.timeout, crate::net::validate_fetch_url(url)).await {
Ok(result) => result.map_err(|e| LtvError::Ocsp(format!("URL rejected: {e}")))?,
Err(_) => {
return Err(LtvError::Ocsp(format!(
"URL validation timed out after {:?}",
self.timeout
)))
}
}
log::debug!(
"Sending OCSP request to {url} ({} bytes)",
request_der.len()
);
let response = self
.http_client
.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)
}
}
#[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() -> Result<Vec<u8>, LtvError> {
Ok(kryptering::random_bytes(NONCE_SIZE)?)
}
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 {
return Err(LtvError::OcspResponderStatus(format!(
"{} ({status})",
ocsp_response_status_name(status)
)));
}
Ok(())
}
fn ocsp_response_status_name(status: u8) -> &'static str {
match status {
0 => "successful",
1 => "malformedRequest",
2 => "internalError",
3 => "tryLater",
5 => "sigRequired",
6 => "unauthorized",
_ => "unknown",
}
}
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::OcspResponderStatus(format!(
"{} ({})",
ocsp_response_status_name(status_body[0]),
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_input = rest;
let (sig_alg_tag, _sig_alg_body, rest) = der_utils::parse_tlv_with_rest(sig_alg_input)
.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_der = &sig_alg_input[..sig_alg_input.len() - rest.len()];
let signature_algorithm = {
use der::Decode as _;
spki::AlgorithmIdentifierOwned::from_der(sig_alg_der)
.map_err(|e| LtvError::Ocsp(format!("signatureAlgorithm decode: {e}")))?
};
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,
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 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,
policy: &crate::crypto::verify::SignaturePolicy,
) -> 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_algid_with_policy(
&parsed.tbs_response_data,
&parsed.signature_bytes,
&spki_der,
&parsed.signature_algorithm,
policy,
);
if result.is_ok() {
return Ok(candidate.clone());
}
}
Err(LtvError::Ocsp(
"OCSP response signature could not be verified against any candidate certificate".into(),
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponderRevocationCheck {
NotRequired,
Required,
}
fn validate_responder_trust(
responder_cert: &Certificate,
issuer: &Certificate,
parsed: &ParsedBasicOcspResponse,
policy: &crate::crypto::verify::SignaturePolicy,
now: chrono::DateTime<chrono::Utc>,
) -> Result<ResponderRevocationCheck, LtvError> {
let validity = &responder_cert.tbs_certificate.validity;
let not_before = validity.not_before.to_date_time();
let not_after = validity.not_after.to_date_time();
let nb = chrono::DateTime::parse_from_rfc3339(&format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
not_before.year(),
{ not_before.month() },
{ not_before.day() },
{ not_before.hour() },
{ not_before.minutes() },
{ not_before.seconds() }
))
.map_err(|e| LtvError::Ocsp(format!("invalid responder notBefore: {e}")))?
.with_timezone(&chrono::Utc);
let na = chrono::DateTime::parse_from_rfc3339(&format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
not_after.year(),
{ not_after.month() },
{ not_after.day() },
{ not_after.hour() },
{ not_after.minutes() },
{ not_after.seconds() }
))
.map_err(|e| LtvError::Ocsp(format!("invalid responder notAfter: {e}")))?
.with_timezone(&chrono::Utc);
if now < nb {
return Err(LtvError::Ocsp(format!(
"OCSP responder certificate is not yet valid (notBefore: {nb})"
)));
}
if now > na {
return Err(LtvError::Ocsp(format!(
"OCSP responder certificate is expired (notAfter: {na})"
)));
}
responder_matches_responder_id(responder_cert, &parsed.responder_id)?;
if certs_have_same_subject(responder_cert, issuer) {
return Ok(ResponderRevocationCheck::NotRequired);
}
let issuer_signed = crate::crypto::verify::verify_certificate_signature_with_policy(
responder_cert,
issuer,
policy,
);
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(),
));
}
if has_ocsp_nocheck_extension(responder_cert) {
Ok(ResponderRevocationCheck::NotRequired)
} else {
Ok(ResponderRevocationCheck::Required)
}
}
fn responder_matches_responder_id(
responder_cert: &Certificate,
responder_id: &ResponderId,
) -> Result<(), LtvError> {
match responder_id {
ResponderId::ByName(name_der) => {
let subject_der = responder_cert
.tbs_certificate
.subject
.to_der()
.map_err(|e| LtvError::Ocsp(format!("responder subject encode: {e}")))?;
if &subject_der != name_der {
return Err(LtvError::Ocsp(
"OCSP responder certificate subject does not match responderID (byName)".into(),
));
}
Ok(())
}
ResponderId::ByKeyHash(key_hash) => {
let key_bytes = responder_cert
.tbs_certificate
.subject_public_key_info
.subject_public_key
.raw_bytes();
let computed = sha1_hash(key_bytes)?;
if &computed != key_hash {
return Err(LtvError::Ocsp(
"OCSP responder certificate key hash does not match responderID (byKeyHash)"
.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()))
}
#[derive(Debug, Clone)]
pub struct OcspFreshness {
pub clock_skew: chrono::Duration,
pub max_age_without_next_update: chrono::Duration,
}
impl Default for OcspFreshness {
fn default() -> Self {
Self {
clock_skew: chrono::Duration::minutes(5),
max_age_without_next_update: chrono::Duration::hours(24),
}
}
}
fn validate_response_freshness(
sr: &SingleResponse,
now: chrono::DateTime<chrono::Utc>,
freshness: &OcspFreshness,
) -> Result<(), LtvError> {
let skew = freshness.clock_skew;
match sr.next_update {
Some(next_update) => {
if next_update < sr.this_update {
return Err(LtvError::Ocsp(format!(
"OCSP response has nextUpdate ({next_update}) before thisUpdate ({})",
sr.this_update
)));
}
if now > next_update + skew {
return Err(LtvError::Ocsp(format!(
"OCSP response is stale: nextUpdate ({next_update}) is before validation time ({now})"
)));
}
}
None => {
let max_valid = sr.this_update + freshness.max_age_without_next_update + skew;
if now > max_valid {
return Err(LtvError::Ocsp(format!(
"OCSP response without nextUpdate is too old: thisUpdate ({}), validation time ({now}), max age {}",
sr.this_update, freshness.max_age_without_next_update
)));
}
}
}
Ok(())
}
pub fn check_revocation(
response_der: &[u8],
cert: &Certificate,
issuer: &Certificate,
nonce: Option<&[u8]>,
validation_time: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<ValidationStatus, LtvError> {
check_revocation_with_policy(
response_der,
cert,
issuer,
nonce,
validation_time,
&crate::crypto::verify::SignaturePolicy::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn check_revocation_with_policy(
response_der: &[u8],
cert: &Certificate,
issuer: &Certificate,
nonce: Option<&[u8]>,
validation_time: Option<chrono::DateTime<chrono::Utc>>,
policy: &crate::crypto::verify::SignaturePolicy,
) -> Result<ValidationStatus, LtvError> {
check_revocation_with_options(
response_der,
cert,
issuer,
nonce,
validation_time,
policy,
&OcspFreshness::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn check_revocation_with_options(
response_der: &[u8],
cert: &Certificate,
issuer: &Certificate,
nonce: Option<&[u8]>,
validation_time: Option<chrono::DateTime<chrono::Utc>>,
policy: &crate::crypto::verify::SignaturePolicy,
freshness: &OcspFreshness,
) -> Result<ValidationStatus, LtvError> {
check_revocation_detailed(
response_der,
cert,
issuer,
nonce,
validation_time,
policy,
freshness,
)
.map(|outcome| outcome.status)
}
#[derive(Debug, Clone)]
pub struct OcspCheckOutcome {
pub status: ValidationStatus,
pub delegated_responder: Option<Certificate>,
}
#[allow(clippy::too_many_arguments)]
pub fn check_revocation_detailed(
response_der: &[u8],
cert: &Certificate,
issuer: &Certificate,
nonce: Option<&[u8]>,
validation_time: Option<chrono::DateTime<chrono::Utc>>,
policy: &crate::crypto::verify::SignaturePolicy,
freshness: &OcspFreshness,
) -> Result<OcspCheckOutcome, 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, policy)?;
let responder_check = validate_responder_trust(&responder_cert, issuer, &parsed, policy, now)?;
let delegated_responder = match responder_check {
ResponderRevocationCheck::Required => Some(responder_cert.clone()),
ResponderRevocationCheck::NotRequired => None,
};
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(OcspCheckOutcome {
status: ValidationStatus::Unknown {
reason: "certificate not found in OCSP response".into(),
},
delegated_responder: None,
});
}
};
validate_response_freshness(sr, now, freshness)?;
let status = match &sr.cert_status {
CertStatus::Good => 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
);
ValidationStatus::Valid {
source: RevocationSource::Ocsp,
checked_at: now,
}
} else {
ValidationStatus::Revoked {
source: RevocationSource::Ocsp,
reason: *reason,
revocation_time: *revocation_time,
}
}
}
CertStatus::Unknown => ValidationStatus::Unknown {
reason: "OCSP responder reported certificate status as unknown".into(),
},
};
Ok(OcspCheckOutcome {
status,
delegated_responder,
})
}
fn sha1_hash(data: &[u8]) -> Result<Vec<u8>, LtvError> {
Ok(kryptering::digest::digest(
kryptering::HashAlgorithm::Sha1,
data,
)?)
}
#[cfg(test)]
mod tests {
use super::*;
use der::Decode;
#[test]
fn test_ocsp_client_default() {
let client = OcspClient::new().unwrap();
assert_eq!(client.timeout, Duration::from_secs(30));
}
#[tokio::test]
async fn test_send_ocsp_request_rejects_loopback_url() {
let client = OcspClient::new().unwrap();
let err = client
.send_ocsp_request("http://127.0.0.1/ocsp", &[0x30, 0x00])
.await
.expect_err("loopback OCSP responder URL must be rejected");
let msg = format!("{err}");
assert!(
msg.contains("non-public") || msg.contains("SSRF"),
"expected SSRF rejection, got: {msg}"
);
}
#[test]
fn test_sha1_hash() {
let hash = sha1_hash(b"test").unwrap();
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().unwrap();
assert_eq!(nonce1.len(), NONCE_SIZE);
let nonce2 = generate_nonce().unwrap();
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> {
build_test_ocsp_response_with_times(
issuer_cert,
issuer_key_pem,
cert,
status,
nonce,
"20260601120000Z",
"20260601120000Z",
Some("20260608120000Z"),
)
}
fn build_test_ocsp_response_with_responder_id(
issuer_cert: &Certificate,
issuer_key_pem: &str,
cert: &Certificate,
status: &CertStatus,
responder_id_tlv: &[u8],
) -> Vec<u8> {
build_test_ocsp_response_full(
issuer_cert,
issuer_key_pem,
cert,
status,
None,
"20260601120000Z",
"20260601120000Z",
Some("20260608120000Z"),
responder_id_tlv,
)
}
#[allow(clippy::too_many_arguments)]
fn build_test_ocsp_response_with_times(
issuer_cert: &Certificate,
issuer_key_pem: &str,
cert: &Certificate,
status: &CertStatus,
nonce: Option<&[u8]>,
produced_at_str: &str,
this_update_str: &str,
next_update_str: Option<&str>,
) -> Vec<u8> {
let issuer_subject_der = issuer_cert.tbs_certificate.subject.to_der().unwrap();
let responder_id = der_utils::encode_tlv(0xA1, &issuer_subject_der);
build_test_ocsp_response_full(
issuer_cert,
issuer_key_pem,
cert,
status,
nonce,
produced_at_str,
this_update_str,
next_update_str,
&responder_id,
)
}
#[allow(clippy::too_many_arguments)]
fn build_test_ocsp_response_full(
issuer_cert: &Certificate,
issuer_key_pem: &str,
cert: &Certificate,
status: &CertStatus,
nonce: Option<&[u8]>,
produced_at_str: &str,
this_update_str: &str,
next_update_str: Option<&str>,
responder_id_tlv: &[u8],
) -> Vec<u8> {
use rsa::pkcs1v15::SigningKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::signature::{SignatureEncoding, Signer};
use sha2::Sha256;
let mut tbs_body = Vec::new();
tbs_body.extend_from_slice(responder_id_tlv);
let produced_at = der_utils::encode_tlv(0x18, produced_at_str.as_bytes());
tbs_body.extend_from_slice(&produced_at);
let single_response = build_test_single_response_with_times(
issuer_cert,
cert,
status,
this_update_str,
next_update_str,
);
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_with_times(
issuer_cert: &Certificate,
cert: &Certificate,
status: &CertStatus,
this_update_str: &str,
next_update_str: Option<&str>,
) -> Vec<u8> {
let issuer_name_der = issuer_cert.tbs_certificate.subject.to_der().unwrap();
let issuer_name_hash = sha1_hash(&issuer_name_der).unwrap();
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).unwrap();
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, this_update_str.as_bytes());
let mut parts: Vec<&[u8]> = vec![&cert_id, &cert_status_der, &this_update];
let next_update;
if let Some(nu_str) = next_update_str {
let next_update_gt = der_utils::encode_tlv(0x18, nu_str.as_bytes());
next_update = der_utils::encode_tlv(0xA0, &next_update_gt);
parts.push(&next_update);
}
der_utils::encode_sequence_from_parts(&parts)
}
#[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_rejects_stale_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-09-01T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let err = check_revocation(&response_der, &cert, &issuer, None, Some(validation_time))
.expect_err("stale OCSP response must be rejected");
assert!(
matches!(err, LtvError::Ocsp(ref m) if m.contains("stale")),
"expected stale error, got: {err}"
);
}
#[test]
fn test_check_revocation_accepts_later_produced_evidence() {
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_with_times(
&issuer,
&key_pem,
&cert,
&CertStatus::Good,
None,
"20260601140000Z", "20260601120000Z", Some("20260608120000Z"), );
let validation_time = chrono::DateTime::parse_from_rfc3339("2026-06-01T13:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let status =
check_revocation(&response_der, &cert, &issuer, None, Some(validation_time)).unwrap();
assert!(
status.is_valid(),
"later-produced OCSP evidence must remain acceptable: {status}"
);
}
#[test]
fn test_check_revocation_accepts_stale_within_skew() {
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-08T12:03: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 within skew: {status}");
}
#[test]
fn test_validate_response_freshness_no_next_update_max_age() {
let this_update = chrono::DateTime::parse_from_rfc3339("2026-06-01T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let sr = SingleResponse {
hash_algorithm_oid: vec![],
issuer_name_hash: vec![],
issuer_key_hash: vec![],
serial_number: vec![],
cert_status: CertStatus::Good,
this_update,
next_update: None,
};
let freshness = OcspFreshness::default();
let within = this_update + chrono::Duration::hours(12);
assert!(validate_response_freshness(&sr, within, &freshness).is_ok());
let before = this_update - chrono::Duration::hours(6);
assert!(validate_response_freshness(&sr, before, &freshness).is_ok());
let beyond = this_update + chrono::Duration::hours(25);
let err = validate_response_freshness(&sr, beyond, &freshness)
.expect_err("response older than max age must be rejected");
assert!(
matches!(err, LtvError::Ocsp(ref m) if m.contains("too old")),
"got: {err}"
);
}
#[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(_)));
}
#[test]
fn test_responder_matches_responder_id_byname() {
let issuer = intermediate_ca_cert();
let issuer_subject = issuer.tbs_certificate.subject.to_der().unwrap();
assert!(responder_matches_responder_id(
&issuer,
&ResponderId::ByName(issuer_subject.clone())
)
.is_ok());
let other = signer_cert().tbs_certificate.subject.to_der().unwrap();
let err = responder_matches_responder_id(&issuer, &ResponderId::ByName(other))
.expect_err("mismatched responderID byName must be rejected");
assert!(
matches!(err, LtvError::Ocsp(ref m) if m.contains("byName")),
"got {err}"
);
}
#[test]
fn test_responder_matches_responder_id_bykeyhash() {
let issuer = intermediate_ca_cert();
let key_bytes = issuer
.tbs_certificate
.subject_public_key_info
.subject_public_key
.raw_bytes()
.to_vec();
let correct = sha1_hash(&key_bytes).unwrap();
assert!(responder_matches_responder_id(&issuer, &ResponderId::ByKeyHash(correct)).is_ok());
let wrong = sha1_hash(b"not the responder key").unwrap();
let err = responder_matches_responder_id(&issuer, &ResponderId::ByKeyHash(wrong))
.expect_err("mismatched responderID byKeyHash must be rejected");
assert!(
matches!(err, LtvError::Ocsp(ref m) if m.contains("byKeyHash")),
"got {err}"
);
}
#[test]
fn test_check_revocation_rejects_wrong_responder_id() {
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 wrong_name = cert.tbs_certificate.subject.to_der().unwrap();
let response_der = build_test_ocsp_response_with_responder_id(
&issuer,
&key_pem,
&cert,
&CertStatus::Good,
&der_utils::encode_tlv(0xA1, &wrong_name),
);
let validation_time = chrono::DateTime::parse_from_rfc3339("2026-06-01T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let err = check_revocation(&response_der, &cert, &issuer, None, Some(validation_time))
.expect_err("a response with a mismatched responderID must be rejected");
assert!(
matches!(err, LtvError::Ocsp(ref m) if m.contains("responderID")),
"expected responderID mismatch rejection, got: {err}"
);
}
#[test]
fn test_issuer_responder_needs_no_revocation_check() {
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 outcome = check_revocation_detailed(
&response_der,
&cert,
&issuer,
None,
Some(validation_time),
&crate::crypto::verify::SignaturePolicy::default(),
&OcspFreshness::default(),
)
.unwrap();
assert!(outcome.status.is_valid());
assert!(
outcome.delegated_responder.is_none(),
"a CA-signed (non-delegated) response needs no responder revocation check"
);
}
}