use std::collections::HashMap;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use sha1::{Digest as Sha1Digest, Sha1};
use tracing::{debug, warn};
use x509_parser::extensions::GeneralName;
use x509_parser::oid_registry::{
OID_PKIX_ACCESS_DESCRIPTOR_CA_ISSUERS, OID_PKIX_ACCESS_DESCRIPTOR_OCSP,
};
use x509_parser::prelude::*;
use crate::certificates::Certificate;
use crate::error::{CertError, CryptoError, Error, Result, StorageError};
use crate::storage::{self, Storage};
#[derive(Debug, Clone)]
pub struct OcspConfig {
pub disable_stapling: bool,
pub replace_revoked: bool,
pub responder_overrides: HashMap<String, String>,
}
impl Default for OcspConfig {
fn default() -> Self {
Self {
disable_stapling: false,
replace_revoked: true,
responder_overrides: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcspStatus {
Good,
Revoked,
Unknown,
ServerFailed,
}
impl std::fmt::Display for OcspStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Good => write!(f, "Good"),
Self::Revoked => write!(f, "Revoked"),
Self::Unknown => write!(f, "Unknown"),
Self::ServerFailed => write!(f, "ServerFailed"),
}
}
}
#[derive(Debug, Clone)]
pub struct OcspResponse {
pub status: OcspStatus,
pub raw: Vec<u8>,
pub this_update: DateTime<Utc>,
pub next_update: Option<DateTime<Utc>>,
pub produced_at: DateTime<Utc>,
pub revoked_at: Option<DateTime<Utc>>,
}
const MAX_OCSP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
const OCSP_REQUEST_CONTENT_TYPE: &str = "application/ocsp-request";
const OCSP_RESPONSE_CONTENT_TYPE: &str = "application/ocsp-response";
const DEFAULT_OCSP_LIFETIME_HOURS: i64 = 24;
const OID_SHA1: &[u8] = &[0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A];
const OCSP_RESPONSE_STATUS_SUCCESSFUL: u8 = 0;
const CERT_STATUS_GOOD: u8 = 0;
const CERT_STATUS_REVOKED: u8 = 1;
const SHORT_LIVED_CERT_DAYS: i64 = 7;
pub async fn staple_ocsp(
storage: &dyn Storage,
cert: &mut Certificate,
_config: &OcspConfig,
) -> Result<bool> {
let lifetime = cert.not_after - cert.not_before;
if lifetime < ChronoDuration::days(SHORT_LIVED_CERT_DAYS) {
debug!(
lifetime_hours = lifetime.num_hours(),
"certificate lifetime is shorter than {SHORT_LIVED_CERT_DAYS} days; skipping OCSP stapling"
);
return Ok(false);
}
if cert.cert_chain.is_empty() {
debug!("certificate chain is empty; skipping OCSP stapling");
return Ok(false);
}
let first_name = cert.names.first().cloned().unwrap_or_default();
let ocsp_storage_key = storage::ocsp_key(&first_name, &cert.hash);
match storage.load(&ocsp_storage_key).await {
Ok(cached_bytes) => {
match parse_ocsp_response_raw(&cached_bytes) {
Ok(parsed) if is_ocsp_fresh(&parsed) => {
debug!(
name = %first_name,
status = %parsed.status,
"using cached OCSP staple"
);
cert.ocsp_status = Some(parsed.status);
cert.ocsp_response = Some(cached_bytes);
return Ok(parsed.status != OcspStatus::Revoked);
}
Ok(parsed) => {
debug!(
name = %first_name,
status = %parsed.status,
"cached OCSP staple is stale; fetching fresh one"
);
}
Err(e) => {
debug!(
name = %first_name,
error = %e,
"failed to parse cached OCSP response; deleting corrupt entry and fetching fresh one"
);
if let Err(del_err) = storage.delete(&ocsp_storage_key).await {
warn!(
name = %first_name,
error = %del_err,
"failed to delete corrupt cached OCSP response"
);
}
}
}
}
Err(Error::Storage(StorageError::NotFound(_))) => {
debug!(name = %first_name, "no cached OCSP staple found");
}
Err(e) => {
warn!(name = %first_name, error = %e, "error loading cached OCSP staple");
}
}
let (raw_response, parsed) =
match get_ocsp_for_cert_chain(&cert.cert_chain, &_config.responder_overrides).await {
Ok(result) => result,
Err(e) => {
debug!(
name = %first_name,
error = %e,
"failed to get OCSP response; stapling skipped"
);
return Ok(false);
}
};
if let Err(e) = storage.store(&ocsp_storage_key, &raw_response).await {
warn!(
name = %first_name,
error = %e,
"failed to cache OCSP response"
);
}
debug!(
name = %first_name,
status = %parsed.status,
"stapled fresh OCSP response"
);
cert.ocsp_status = Some(parsed.status);
cert.ocsp_response = Some(raw_response);
Ok(parsed.status != OcspStatus::Revoked)
}
pub fn is_ocsp_fresh(response: &OcspResponse) -> bool {
let now = Utc::now();
if let Some(next_update) = response.next_update {
now < next_update
} else {
let expires = response.this_update + ChronoDuration::hours(DEFAULT_OCSP_LIFETIME_HOURS);
now < expires
}
}
pub fn ocsp_needs_update(response: &OcspResponse) -> bool {
if !is_ocsp_fresh(response) {
return true;
}
let now = Utc::now();
if let Some(next_update) = response.next_update {
if next_update - now < ChronoDuration::hours(1) {
return true;
}
let total = next_update - response.this_update;
let elapsed = now - response.this_update;
if elapsed > total / 2 {
return true;
}
} else {
let half_life =
response.this_update + ChronoDuration::hours(DEFAULT_OCSP_LIFETIME_HOURS / 2);
if now > half_life {
return true;
}
}
false
}
pub fn extract_ocsp_urls(cert_der: &[u8]) -> Result<Vec<String>> {
let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| {
CryptoError::InvalidCertificate(format!(
"failed to parse certificate for OCSP URL extraction: {e}"
))
})?;
extract_ocsp_urls_from_parsed(&cert)
}
fn extract_ocsp_urls_from_parsed(cert: &X509Certificate<'_>) -> Result<Vec<String>> {
let mut urls = Vec::new();
for ext in cert.extensions() {
if let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() {
for desc in aia.accessdescs.iter() {
if desc.access_method == OID_PKIX_ACCESS_DESCRIPTOR_OCSP
&& let GeneralName::URI(uri) = &desc.access_location
{
urls.push(uri.to_string());
}
}
}
}
Ok(urls)
}
fn extract_ca_issuer_urls_from_parsed(cert: &X509Certificate<'_>) -> Vec<String> {
let mut urls = Vec::new();
for ext in cert.extensions() {
if let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() {
for desc in aia.accessdescs.iter() {
if desc.access_method == OID_PKIX_ACCESS_DESCRIPTOR_CA_ISSUERS
&& let GeneralName::URI(uri) = &desc.access_location
{
urls.push(uri.to_string());
}
}
}
}
urls
}
async fn fetch_issuer_cert(url: &str) -> Result<Vec<u8>> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| CertError::OcspFailed(format!("failed to build HTTP client: {e}")))?;
let response = client.get(url).send().await.map_err(|e| {
CertError::OcspFailed(format!(
"failed to fetch issuer certificate from {url}: {e}"
))
})?;
if !response.status().is_success() {
return Err(CertError::OcspFailed(format!(
"issuer certificate fetch from {url} returned HTTP {}",
response.status()
))
.into());
}
let body = response.bytes().await.map_err(|e| {
CertError::OcspFailed(format!(
"failed to read issuer certificate response body: {e}"
))
})?;
if body.is_empty() {
return Err(
CertError::OcspFailed("issuer certificate response body is empty".into()).into(),
);
}
if body.starts_with(b"-----BEGIN") {
let pem_str = std::str::from_utf8(&body).map_err(|e| {
CertError::OcspFailed(format!("issuer certificate PEM is not valid UTF-8: {e}"))
})?;
let parsed = ::pem::parse(pem_str).map_err(|e| {
CertError::OcspFailed(format!("failed to parse issuer certificate PEM: {e}"))
})?;
Ok(parsed.into_contents())
} else {
X509Certificate::from_der(&body).map_err(|e| {
CertError::OcspFailed(format!(
"downloaded issuer certificate is not valid DER: {e}"
))
})?;
Ok(body.to_vec())
}
}
async fn get_ocsp_for_cert_chain(
chain: &[rustls::pki_types::CertificateDer<'static>],
responder_overrides: &HashMap<String, String>,
) -> Result<(Vec<u8>, OcspResponse)> {
if chain.is_empty() {
return Err(CertError::OcspFailed("certificate chain is empty".into()).into());
}
let leaf_der = chain[0].as_ref();
let (_, leaf) = X509Certificate::from_der(leaf_der).map_err(|e| {
CryptoError::InvalidCertificate(format!("failed to parse leaf certificate: {e}"))
})?;
let fetched_issuer_der: Option<Vec<u8>>;
let issuer_der_ref: &[u8] = if chain.len() >= 2 {
chain[1].as_ref()
} else {
let issuer_urls = extract_ca_issuer_urls_from_parsed(&leaf);
let mut downloaded = None;
for url in &issuer_urls {
match fetch_issuer_cert(url).await {
Ok(der_bytes) => {
debug!(url = %url, "fetched issuer certificate from AIA extension");
downloaded = Some(der_bytes);
break;
}
Err(e) => {
debug!(url = %url, error = %e, "failed to fetch issuer from AIA URL");
}
}
}
fetched_issuer_der = downloaded;
match fetched_issuer_der.as_deref() {
Some(d) => d,
None => {
return Err(CertError::OcspFailed(
"certificate chain has only 1 entry and issuer could not be fetched from AIA"
.into(),
)
.into());
}
}
};
let (_, issuer) = X509Certificate::from_der(issuer_der_ref).map_err(|e| {
CryptoError::InvalidCertificate(format!("failed to parse issuer certificate: {e}"))
})?;
let override_url = if !responder_overrides.is_empty() {
crate::certificates::extract_names_from_der(leaf_der)
.ok()
.and_then(|names| {
names
.iter()
.find_map(|name| responder_overrides.get(name).cloned())
})
} else {
None
};
let ocsp_url: String = if let Some(url) = override_url {
debug!(url = %url, "using responder override URL");
url
} else {
let ocsp_urls = extract_ocsp_urls_from_parsed(&leaf)?;
ocsp_urls.into_iter().next().ok_or_else(|| {
CertError::OcspFailed("no OCSP responder URL found in certificate AIA extension".into())
})?
};
let ocsp_request_der = build_ocsp_request(&leaf, &issuer)?;
let raw_response = send_ocsp_request(&ocsp_url, &ocsp_request_der).await?;
let parsed = parse_ocsp_response_raw(&raw_response)?;
Ok((raw_response, parsed))
}
pub async fn get_ocsp_for_cert(cert_pem: &[u8]) -> Result<(Vec<u8>, OcspResponse)> {
let pem_str = std::str::from_utf8(cert_pem).map_err(|e| {
CryptoError::InvalidCertificate(format!("certificate PEM is not valid UTF-8: {e}"))
})?;
let pems: Vec<::pem::Pem> = ::pem::parse_many(pem_str)
.map_err(|e| CryptoError::InvalidCertificate(format!("failed to parse PEM bundle: {e}")))?;
let cert_ders: Vec<rustls::pki_types::CertificateDer<'static>> = pems
.into_iter()
.filter(|p| p.tag() == "CERTIFICATE")
.map(|p| rustls::pki_types::CertificateDer::from(p.into_contents()))
.collect();
if cert_ders.len() < 2 {
return Err(CertError::OcspFailed(
"PEM bundle must contain at least 2 certificates (leaf + issuer)".into(),
)
.into());
}
get_ocsp_for_cert_chain(&cert_ders, &HashMap::new()).await
}
fn build_ocsp_request(leaf: &X509Certificate<'_>, issuer: &X509Certificate<'_>) -> Result<Vec<u8>> {
let issuer_name_der = issuer.subject().as_raw();
let issuer_name_hash = sha1_hash(issuer_name_der);
let issuer_key_hash = hash_issuer_public_key(issuer)?;
let serial = leaf.raw_serial();
let cert_id = build_cert_id(&issuer_name_hash, &issuer_key_hash, serial);
let request = der_wrap(0x30, &cert_id);
let request_list = der_wrap(0x30, &request);
let tbs_request = der_wrap(0x30, &request_list);
let ocsp_request = der_wrap(0x30, &tbs_request);
Ok(ocsp_request)
}
fn build_cert_id(issuer_name_hash: &[u8], issuer_key_hash: &[u8], serial_number: &[u8]) -> Vec<u8> {
let sha1_null = der_wrap(0x05, &[]); let mut algo_content = Vec::new();
algo_content.extend_from_slice(OID_SHA1);
algo_content.extend_from_slice(&sha1_null);
let algo_id = der_wrap(0x30, &algo_content);
let name_hash = der_wrap(0x04, issuer_name_hash);
let key_hash = der_wrap(0x04, issuer_key_hash);
let serial_int = if !serial_number.is_empty() && (serial_number[0] & 0x80) != 0 {
let mut padded = vec![0x00];
padded.extend_from_slice(serial_number);
der_wrap(0x02, &padded)
} else {
der_wrap(0x02, serial_number)
};
let mut cert_id_content = Vec::new();
cert_id_content.extend_from_slice(&algo_id);
cert_id_content.extend_from_slice(&name_hash);
cert_id_content.extend_from_slice(&key_hash);
cert_id_content.extend_from_slice(&serial_int);
cert_id_content
}
fn hash_issuer_public_key(issuer: &X509Certificate<'_>) -> Result<Vec<u8>> {
let spki = issuer.public_key();
Ok(sha1_hash(&spki.subject_public_key.data))
}
async fn send_ocsp_request(url: &str, request_der: &[u8]) -> Result<Vec<u8>> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| CertError::OcspFailed(format!("failed to build HTTP client: {e}")))?;
let response = client
.post(url)
.header("Content-Type", OCSP_REQUEST_CONTENT_TYPE)
.header("Accept", OCSP_RESPONSE_CONTENT_TYPE)
.body(request_der.to_vec())
.send()
.await
.map_err(|e| CertError::OcspFailed(format!("OCSP HTTP request to {url} failed: {e}")))?;
if !response.status().is_success() {
return Err(CertError::OcspFailed(format!(
"OCSP responder returned HTTP {}",
response.status()
))
.into());
}
let body = response
.bytes()
.await
.map_err(|e| CertError::OcspFailed(format!("failed to read OCSP response body: {e}")))?;
if body.len() > MAX_OCSP_RESPONSE_SIZE {
return Err(CertError::OcspFailed(format!(
"OCSP response too large: {} bytes (max {})",
body.len(),
MAX_OCSP_RESPONSE_SIZE
))
.into());
}
Ok(body.to_vec())
}
fn parse_ocsp_response_raw(data: &[u8]) -> Result<OcspResponse> {
use x509_parser::der_parser::parse_der;
let (_, outer) = parse_der(data)
.map_err(|e| CertError::OcspFailed(format!("failed to parse OCSP response DER: {e}")))?;
let outer_seq = outer
.as_sequence()
.map_err(|e| CertError::OcspFailed(format!("OCSP response is not a SEQUENCE: {e}")))?;
if outer_seq.is_empty() {
return Err(CertError::OcspFailed("OCSP response SEQUENCE is empty".into()).into());
}
let response_status = outer_seq[0]
.as_u32()
.map_err(|e| CertError::OcspFailed(format!("failed to parse responseStatus: {e}")))?;
if response_status != OCSP_RESPONSE_STATUS_SUCCESSFUL as u32 {
return Ok(OcspResponse {
status: OcspStatus::ServerFailed,
raw: data.to_vec(),
this_update: Utc::now(),
next_update: None,
produced_at: Utc::now(),
revoked_at: None,
});
}
if outer_seq.len() < 2 {
return Err(
CertError::OcspFailed("successful OCSP response missing responseBytes".into()).into(),
);
}
let response_bytes_wrapper = &outer_seq[1];
let response_bytes_content = get_context_content(response_bytes_wrapper).ok_or_else(|| {
CertError::OcspFailed("failed to unwrap responseBytes context tag".into())
})?;
let (_, resp_bytes_der) = parse_der(response_bytes_content)
.map_err(|e| CertError::OcspFailed(format!("failed to parse ResponseBytes: {e}")))?;
let resp_bytes_seq = resp_bytes_der
.as_sequence()
.map_err(|e| CertError::OcspFailed(format!("ResponseBytes is not a SEQUENCE: {e}")))?;
if resp_bytes_seq.len() < 2 {
return Err(CertError::OcspFailed("ResponseBytes SEQUENCE too short".into()).into());
}
let basic_resp_der = resp_bytes_seq[1].as_slice().map_err(|e| {
CertError::OcspFailed(format!(
"failed to extract BasicOCSPResponse OCTET STRING: {e}"
))
})?;
parse_basic_ocsp_response(basic_resp_der, data)
}
fn parse_basic_ocsp_response(data: &[u8], raw: &[u8]) -> Result<OcspResponse> {
use x509_parser::der_parser::parse_der;
let (_, basic) = parse_der(data)
.map_err(|e| CertError::OcspFailed(format!("failed to parse BasicOCSPResponse: {e}")))?;
let basic_seq = basic
.as_sequence()
.map_err(|e| CertError::OcspFailed(format!("BasicOCSPResponse is not a SEQUENCE: {e}")))?;
if basic_seq.is_empty() {
return Err(CertError::OcspFailed("BasicOCSPResponse SEQUENCE is empty".into()).into());
}
let tbs = basic_seq[0]
.as_sequence()
.map_err(|e| CertError::OcspFailed(format!("tbsResponseData is not a SEQUENCE: {e}")))?;
parse_tbs_response_data(tbs, raw)
}
fn parse_tbs_response_data(
tbs: &[x509_parser::der_parser::ber::BerObject<'_>],
raw: &[u8],
) -> Result<OcspResponse> {
let mut idx = 0;
if !tbs.is_empty() && is_context_tagged(&tbs[0], 0) {
idx += 1; }
if idx >= tbs.len() {
return Err(CertError::OcspFailed("ResponseData too short".into()).into());
}
idx += 1;
if idx >= tbs.len() {
return Err(CertError::OcspFailed("ResponseData missing producedAt".into()).into());
}
let produced_at = parse_generalized_time_from_obj(&tbs[idx]).unwrap_or_else(Utc::now);
idx += 1;
if idx >= tbs.len() {
return Err(CertError::OcspFailed("ResponseData missing responses".into()).into());
}
let responses = tbs[idx]
.as_sequence()
.map_err(|e| CertError::OcspFailed(format!("responses is not a SEQUENCE: {e}")))?;
if responses.is_empty() {
return Err(CertError::OcspFailed(
"OCSP response contains no SingleResponse entries".into(),
)
.into());
}
parse_single_response(&responses[0], raw, produced_at)
}
fn parse_single_response(
obj: &x509_parser::der_parser::ber::BerObject<'_>,
raw: &[u8],
produced_at: DateTime<Utc>,
) -> Result<OcspResponse> {
let seq = obj
.as_sequence()
.map_err(|e| CertError::OcspFailed(format!("SingleResponse is not a SEQUENCE: {e}")))?;
if seq.len() < 3 {
return Err(CertError::OcspFailed(
"SingleResponse SEQUENCE too short (need certID, certStatus, thisUpdate)".into(),
)
.into());
}
let (status, revoked_at) = parse_cert_status(&seq[1])?;
let this_update = parse_generalized_time_from_obj(&seq[2])
.ok_or_else(|| CertError::OcspFailed("failed to parse thisUpdate".into()))?;
let next_update = if seq.len() > 3 && is_context_tagged(&seq[3], 0) {
let content = get_context_content(&seq[3]);
content.and_then(parse_generalized_time_from_bytes)
} else {
None
};
Ok(OcspResponse {
status,
raw: raw.to_vec(),
this_update,
next_update,
produced_at,
revoked_at,
})
}
fn parse_cert_status(
obj: &x509_parser::der_parser::ber::BerObject<'_>,
) -> Result<(OcspStatus, Option<DateTime<Utc>>)> {
let tag = obj.header.tag();
if tag.0 == CERT_STATUS_GOOD as u32 {
return Ok((OcspStatus::Good, None));
}
if tag.0 == CERT_STATUS_REVOKED as u32 {
let revoked_at = get_context_content(obj).and_then(|bytes| {
x509_parser::der_parser::parse_der(bytes)
.ok()
.and_then(|(_, inner)| {
inner.as_sequence().ok().and_then(|seq| {
if seq.is_empty() {
None
} else {
parse_generalized_time_from_obj(&seq[0])
}
})
})
});
return Ok((OcspStatus::Revoked, revoked_at));
}
Ok((OcspStatus::Unknown, None))
}
fn der_wrap(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
let len = content.len();
if len < 0x80 {
out.push(len as u8);
} else if len < 0x100 {
out.push(0x81);
out.push(len as u8);
} else if len < 0x10000 {
out.push(0x82);
out.push((len >> 8) as u8);
out.push(len as u8);
} else {
out.push(0x83);
out.push((len >> 16) as u8);
out.push((len >> 8) as u8);
out.push(len as u8);
}
out.extend_from_slice(content);
out
}
fn sha1_hash(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha1::new();
hasher.update(data);
hasher.finalize().to_vec()
}
fn is_context_tagged(obj: &x509_parser::der_parser::ber::BerObject<'_>, tag_num: u32) -> bool {
let tag = obj.header.tag();
let class = obj.header.class();
class == x509_parser::asn1_rs::Class::ContextSpecific && tag.0 == tag_num
}
fn get_context_content<'a>(
obj: &'a x509_parser::der_parser::ber::BerObject<'a>,
) -> Option<&'a [u8]> {
obj.content.as_slice().ok()
}
fn parse_generalized_time_from_obj(
obj: &x509_parser::der_parser::ber::BerObject<'_>,
) -> Option<DateTime<Utc>> {
if let Ok(bytes) = obj.content.as_slice()
&& let Some(dt) = parse_time_string(bytes)
{
return Some(dt);
}
None
}
fn parse_generalized_time_from_bytes(bytes: &[u8]) -> Option<DateTime<Utc>> {
use x509_parser::der_parser::parse_der;
if let Ok((_, obj)) = parse_der(bytes) {
return parse_generalized_time_from_obj(&obj);
}
None
}
fn parse_time_string(bytes: &[u8]) -> Option<DateTime<Utc>> {
let s = std::str::from_utf8(bytes).ok()?;
let s = s.trim_end_matches('Z');
if s.len() >= 14 {
let year: i32 = s[0..4].parse().ok()?;
let month: u32 = s[4..6].parse().ok()?;
let day: u32 = s[6..8].parse().ok()?;
let hour: u32 = s[8..10].parse().ok()?;
let min: u32 = s[10..12].parse().ok()?;
let sec: u32 = s[12..14].parse().ok()?;
use chrono::NaiveDate;
let naive = NaiveDate::from_ymd_opt(year, month, day)?.and_hms_opt(hour, min, sec)?;
Some(DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc))
} else if s.len() >= 12 {
let year: i32 = s[0..2].parse().ok()?;
let year = if year >= 50 { 1900 + year } else { 2000 + year };
let month: u32 = s[2..4].parse().ok()?;
let day: u32 = s[4..6].parse().ok()?;
let hour: u32 = s[6..8].parse().ok()?;
let min: u32 = s[8..10].parse().ok()?;
let sec: u32 = s[10..12].parse().ok()?;
use chrono::NaiveDate;
let naive = NaiveDate::from_ymd_opt(year, month, day)?.and_hms_opt(hour, min, sec)?;
Some(DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ocsp_config_default() {
let config = OcspConfig::default();
assert!(!config.disable_stapling);
assert!(config.replace_revoked);
}
#[test]
fn test_ocsp_status_display() {
assert_eq!(OcspStatus::Good.to_string(), "Good");
assert_eq!(OcspStatus::Revoked.to_string(), "Revoked");
assert_eq!(OcspStatus::Unknown.to_string(), "Unknown");
assert_eq!(OcspStatus::ServerFailed.to_string(), "ServerFailed");
}
#[test]
fn test_ocsp_status_eq() {
assert_eq!(OcspStatus::Good, OcspStatus::Good);
assert_ne!(OcspStatus::Good, OcspStatus::Revoked);
}
#[test]
fn test_is_ocsp_fresh_with_next_update_in_future() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(1),
next_update: Some(Utc::now() + ChronoDuration::hours(23)),
produced_at: Utc::now() - ChronoDuration::hours(1),
revoked_at: None,
};
assert!(is_ocsp_fresh(&response));
}
#[test]
fn test_is_ocsp_fresh_with_next_update_in_past() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(48),
next_update: Some(Utc::now() - ChronoDuration::hours(1)),
produced_at: Utc::now() - ChronoDuration::hours(48),
revoked_at: None,
};
assert!(!is_ocsp_fresh(&response));
}
#[test]
fn test_is_ocsp_fresh_no_next_update_recent() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(1),
next_update: None,
produced_at: Utc::now() - ChronoDuration::hours(1),
revoked_at: None,
};
assert!(is_ocsp_fresh(&response));
}
#[test]
fn test_is_ocsp_fresh_no_next_update_old() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(25),
next_update: None,
produced_at: Utc::now() - ChronoDuration::hours(25),
revoked_at: None,
};
assert!(!is_ocsp_fresh(&response));
}
#[test]
fn test_ocsp_needs_update_stale() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(48),
next_update: Some(Utc::now() - ChronoDuration::hours(1)),
produced_at: Utc::now() - ChronoDuration::hours(48),
revoked_at: None,
};
assert!(ocsp_needs_update(&response));
}
#[test]
fn test_ocsp_needs_update_within_one_hour() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(23),
next_update: Some(Utc::now() + ChronoDuration::minutes(30)),
produced_at: Utc::now() - ChronoDuration::hours(23),
revoked_at: None,
};
assert!(ocsp_needs_update(&response));
}
#[test]
fn test_ocsp_needs_update_past_midpoint() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(18),
next_update: Some(Utc::now() + ChronoDuration::hours(6)),
produced_at: Utc::now() - ChronoDuration::hours(18),
revoked_at: None,
};
assert!(ocsp_needs_update(&response));
}
#[test]
fn test_ocsp_needs_update_fresh_and_before_midpoint() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(2),
next_update: Some(Utc::now() + ChronoDuration::hours(22)),
produced_at: Utc::now() - ChronoDuration::hours(2),
revoked_at: None,
};
assert!(!ocsp_needs_update(&response));
}
#[test]
fn test_ocsp_needs_update_no_next_update_recent() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(2),
next_update: None,
produced_at: Utc::now() - ChronoDuration::hours(2),
revoked_at: None,
};
assert!(!ocsp_needs_update(&response));
}
#[test]
fn test_ocsp_needs_update_no_next_update_past_half() {
let response = OcspResponse {
status: OcspStatus::Good,
raw: vec![],
this_update: Utc::now() - ChronoDuration::hours(13),
next_update: None,
produced_at: Utc::now() - ChronoDuration::hours(13),
revoked_at: None,
};
assert!(ocsp_needs_update(&response));
}
#[test]
fn test_der_wrap_short() {
let wrapped = der_wrap(0x04, &[0x01, 0x02, 0x03]);
assert_eq!(wrapped, vec![0x04, 0x03, 0x01, 0x02, 0x03]);
}
#[test]
fn test_der_wrap_long() {
let content = vec![0xAA; 200];
let wrapped = der_wrap(0x04, &content);
assert_eq!(wrapped[0], 0x04);
assert_eq!(wrapped[1], 0x81); assert_eq!(wrapped[2], 200);
assert_eq!(wrapped.len(), 200 + 3);
}
#[test]
fn test_sha1_hash() {
let hash = sha1_hash(b"");
assert_eq!(hash.len(), 20);
assert_eq!(hash[0], 0xda);
assert_eq!(hash[1], 0x39);
}
#[test]
fn test_build_cert_id() {
let name_hash = vec![0u8; 20];
let key_hash = vec![1u8; 20];
let serial = vec![0x01, 0x02, 0x03];
let cert_id = build_cert_id(&name_hash, &key_hash, &serial);
assert!(!cert_id.is_empty());
}
#[test]
fn test_parse_ocsp_response_raw_invalid() {
let result = parse_ocsp_response_raw(b"not valid der");
assert!(result.is_err());
}
#[test]
fn test_parse_ocsp_response_raw_empty_sequence() {
let result = parse_ocsp_response_raw(&[0x30, 0x00]);
assert!(result.is_err());
}
#[test]
fn test_parse_ocsp_response_unsuccessful_status() {
let data = vec![
0x30, 0x03, 0x0A, 0x01, 0x01, ];
let result = parse_ocsp_response_raw(&data).unwrap();
assert_eq!(result.status, OcspStatus::ServerFailed);
}
}