use std::str::FromStr;
use chrono::{DateTime, NaiveDateTime, Utc};
use rasn_ocsp::{BasicOcspResponse, CertId, CertStatus, OcspResponseStatus};
use rasn_pkix::{Certificate, CrlReason};
use thiserror::Error;
use crate::{
crypto::{internal::time, raw_signature::RawSignatureValidationError},
log_item,
status_tracker::StatusTracker,
validation_results::validation_codes,
};
pub struct OcspResponse {
pub ocsp_der: Vec<u8>,
pub next_update: DateTime<Utc>,
pub revoked_at: Option<DateTime<Utc>>,
pub ocsp_certs: Option<Vec<Vec<u8>>>,
pub certificate_serial_num: String,
}
impl Default for OcspResponse {
fn default() -> Self {
Self {
ocsp_der: Vec::new(),
next_update: time::utc_now(),
revoked_at: None,
ocsp_certs: None,
certificate_serial_num: String::new(),
}
}
}
impl OcspResponse {
pub(crate) fn from_der_checked(
der: &[u8],
signing_cert_chain: &[Vec<u8>],
signing_time: Option<DateTime<Utc>>,
validation_log: &mut StatusTracker,
) -> Result<Self, OcspError> {
let mut output = OcspResponse {
ocsp_der: der.to_vec(),
..Default::default()
};
let Ok(ocsp_response) = rasn::der::decode::<rasn_ocsp::OcspResponse>(der) else {
return Ok(output);
};
if ocsp_response.status != OcspResponseStatus::Successful {
return Ok(output);
}
let Some(response_bytes) = ocsp_response.bytes else {
return Ok(output);
};
let Ok(basic_response) = rasn::der::decode::<BasicOcspResponse>(&response_bytes.response)
else {
return Ok(output);
};
let mut internal_validation_log = StatusTracker::default();
let response_data = &basic_response.tbs_response_data;
if let Some(ocsp_certs) = &basic_response.certs {
let mut cert_der_vec = Vec::new();
for ocsp_cert in ocsp_certs {
let cert_der =
rasn::der::encode(ocsp_cert).map_err(|_e| OcspError::InvalidCertificate)?;
cert_der_vec.push(cert_der);
}
let Ok(sig_alg) =
bcder::Oid::from_str(&basic_response.signature_algorithm.algorithm.to_string())
else {
return Ok(output);
};
let Some(hash_alg) =
hash_alg_for_sig_alg(&basic_response.signature_algorithm.algorithm)
else {
return Ok(output);
};
let sig_val = bcder::OctetString::new(bytes::Bytes::copy_from_slice(
basic_response.signature.as_raw_slice(),
));
let Ok(tbs) = rasn::der::encode(&basic_response.tbs_response_data) else {
return Ok(output);
};
let Some(first_cert) = ocsp_certs.first() else {
return Ok(output);
};
let Ok(signing_key_der) =
rasn::der::encode(&first_cert.tbs_certificate.subject_public_key_info)
else {
return Ok(output);
};
if validate_ocsp_sig(&sig_alg, &hash_alg, &sig_val, &tbs, &signing_key_der).is_ok() {
output.ocsp_certs = Some(cert_der_vec);
} else {
return Ok(OcspResponse::default());
}
} else {
return Ok(OcspResponse::default());
}
for single_response in &response_data.responses {
if !cert_id_matches_signer(&single_response.cert_id, signing_cert_chain) {
continue;
}
let cert_status = &single_response.cert_status;
if output.certificate_serial_num.is_empty() {
output.certificate_serial_num = single_response.cert_id.serial_number.to_string();
}
match cert_status {
CertStatus::Good => {
let this_update = NaiveDateTime::parse_from_str(
&single_response.this_update.to_string(),
DATE_FMT,
)
.map_err(|_e| OcspError::InvalidCertificate)?
.and_utc()
.timestamp();
let next_update = if let Some(nu) = &single_response.next_update {
NaiveDateTime::parse_from_str(&nu.to_string(), DATE_FMT)
.map_err(|_e| OcspError::InvalidCertificate)?
.and_utc()
.timestamp()
} else {
response_data.produced_at.to_utc().timestamp() + (24 * 60 * 60)
};
let in_range = if let Some(st) = signing_time {
st.timestamp() < this_update
|| (st.timestamp() >= this_update && st.timestamp() <= next_update)
} else {
let now = time::utc_now().timestamp();
now >= this_update
};
if let Some(nu) = &single_response.next_update {
let nu_utc = nu.naive_utc();
output.next_update = DateTime::from_naive_utc_and_offset(nu_utc, Utc);
}
if !in_range {
log_item!(
"OCSP_RESPONSE",
"certificate revoked",
"check_ocsp_response"
)
.validation_status(validation_codes::SIGNING_CREDENTIAL_REVOKED)
.failure_no_throw(
&mut internal_validation_log,
OcspError::CertificateRevoked,
);
} else {
log_item!(
"OCSP_RESPONSE",
"certificate not revoked",
"check_ocsp_response"
)
.validation_status(validation_codes::SIGNING_CREDENTIAL_NOT_REVOKED)
.success(validation_log);
return Ok(output);
}
}
CertStatus::Revoked(revoked_info) => {
let revocation_time = &revoked_info.revocation_time;
let revoked_at =
NaiveDateTime::parse_from_str(&revocation_time.to_string(), DATE_FMT)
.map_err(|_e| OcspError::InvalidCertificate)?
.and_utc()
.timestamp();
let revoked_at_native =
NaiveDateTime::parse_from_str(&revocation_time.to_string(), DATE_FMT)
.map_err(|_e| OcspError::InvalidCertificate)?;
if let Some(reason) = revoked_info.revocation_reason {
if reason == CrlReason::RemoveFromCRL {
let in_range = if let Some(st) = signing_time {
revoked_at > st.timestamp()
} else {
let now = time::utc_now().timestamp();
revoked_at > now
};
if !in_range {
let utc_with_offset: DateTime<Utc> =
DateTime::from_naive_utc_and_offset(revoked_at_native, Utc);
let msg = format!("certificate revoked at: {utc_with_offset}");
log_item!("OCSP_RESPONSE", msg, "check_ocsp_response")
.validation_status(validation_codes::SIGNING_CREDENTIAL_REVOKED)
.failure_no_throw(
&mut internal_validation_log,
OcspError::CertificateRevoked,
);
output.revoked_at = Some(DateTime::from_naive_utc_and_offset(
revoked_at_native,
Utc,
));
}
} else {
let Ok(revoked_at_native) = NaiveDateTime::parse_from_str(
&revoked_info.revocation_time.to_string(),
DATE_FMT,
) else {
return Err(OcspError::InvalidCertificate);
};
let utc_with_offset: DateTime<Utc> =
DateTime::from_naive_utc_and_offset(revoked_at_native, Utc);
let in_range = if let Some(st) = signing_time {
st.timestamp() < utc_with_offset.timestamp()
} else {
false
};
if !in_range {
log_item!(
"OCSP_RESPONSE",
format!("certificate revoked at: {}", utc_with_offset),
"check_ocsp_response"
)
.validation_status(validation_codes::SIGNING_CREDENTIAL_REVOKED)
.failure_no_throw(
&mut internal_validation_log,
OcspError::CertificateRevoked,
);
output.revoked_at = Some(DateTime::from_naive_utc_and_offset(
revoked_at_native,
Utc,
));
} else {
output.revoked_at = None;
return Ok(output);
}
}
} else {
log_item!(
"OCSP_RESPONSE",
"certificate revoked",
"check_ocsp_response"
)
.validation_status(validation_codes::SIGNING_CREDENTIAL_REVOKED)
.failure_no_throw(
&mut internal_validation_log,
OcspError::CertificateRevoked,
);
output.revoked_at =
Some(DateTime::from_naive_utc_and_offset(revoked_at_native, Utc));
}
}
CertStatus::Unknown(_) => {
log_item!("OCSP_RESPONSE", "unknown certStatus", "check_ocsp_response")
.validation_status(validation_codes::SIGNING_CREDENTIAL_OCSP_UNKNOWN)
.failure_no_throw(
&mut internal_validation_log,
OcspError::CertificateStatusUnknown,
);
}
}
}
validation_log.append(&internal_validation_log);
Ok(output)
}
}
fn validate_ocsp_sig(
sig_alg: &bcder::Oid,
hash_alg: &bcder::Oid,
sig_val: &bcder::OctetString,
tbs: &[u8],
signing_key_der: &[u8],
) -> Result<(), RawSignatureValidationError> {
if let Some(validator) =
crate::crypto::raw_signature::validator_for_sig_and_hash_algs(sig_alg, hash_alg)
{
validator
.validate(&sig_val.to_bytes(), tbs, signing_key_der)
.map_err(|e| RawSignatureValidationError::CryptoLibraryError(e.to_string()))
} else {
Err(RawSignatureValidationError::UnsupportedAlgorithm)
}
}
fn cert_id_matches_signer(cert_id: &CertId, signing_cert_chain: &[Vec<u8>]) -> bool {
let (Some(subject_der), Some(issuer_der)) =
(signing_cert_chain.first(), signing_cert_chain.get(1))
else {
return false;
};
let Ok(subject) = rasn::der::decode::<Certificate>(subject_der) else {
return false;
};
let Ok(issuer) = rasn::der::decode::<Certificate>(issuer_der) else {
return false;
};
if cert_id.serial_number != subject.tbs_certificate.serial_number {
return false;
}
let Ok(issuer_name_raw) = rasn::der::encode(&issuer.tbs_certificate.subject) else {
return false;
};
let issuer_key_raw = issuer
.tbs_certificate
.subject_public_key_info
.subject_public_key
.as_raw_slice();
let Some(expected_name_hash) = hash_by_oid(&cert_id.hash_algorithm.algorithm, &issuer_name_raw)
else {
return false;
};
let Some(expected_key_hash) = hash_by_oid(&cert_id.hash_algorithm.algorithm, issuer_key_raw)
else {
return false;
};
cert_id.issuer_name_hash.as_ref() == expected_name_hash.as_slice()
&& cert_id.issuer_key_hash.as_ref() == expected_key_hash.as_slice()
}
fn hash_by_oid(alg: &rasn::types::ObjectIdentifier, data: &[u8]) -> Option<Vec<u8>> {
match alg.to_string().as_str() {
"1.3.14.3.2.26" => Some(crate::crypto::hash::sha1(data)),
"2.16.840.1.101.3.4.2.1" => Some(crate::crypto::hash::sha256(data)),
_ => None,
}
}
fn hash_alg_for_sig_alg(sig_alg: &rasn::types::ObjectIdentifier) -> Option<bcder::Oid> {
match sig_alg.to_string().as_ref() {
"1.2.840.10045.4.3.2" => Some(bcder::Oid::from_str("2.16.840.1.101.3.4.2.1").ok()?),
"1.2.840.10045.4.3.3" => Some(bcder::Oid::from_str("2.16.840.1.101.3.4.2.2").ok()?),
"1.2.840.10045.4.3.4" => Some(bcder::Oid::from_str("2.16.840.1.101.3.4.2.3").ok()?),
"1.2.840.113549.1.1.11" => Some(bcder::Oid::from_str("2.16.840.1.101.3.4.2.1").ok()?),
"1.2.840.113549.1.1.12" => Some(bcder::Oid::from_str("2.16.840.1.101.3.4.2.2").ok()?),
"1.2.840.113549.1.1.13" => Some(bcder::Oid::from_str("2.16.840.1.101.3.4.2.3").ok()?),
"1.3.101.112" => Some(bcder::Oid::from_str("2.16.840.1.101.3.4.2.3").ok()?),
_ => None,
}
}
#[derive(Debug, Eq, Error, PartialEq)]
#[allow(unused)] pub(crate) enum OcspError {
#[error("Invalid certificate detected")]
InvalidCertificate,
#[error("Invalid system time")]
InvalidSystemTime,
#[error("Certificate revoked")]
CertificateRevoked,
#[error("Unknown certificate status")]
CertificateStatusUnknown,
}
const DATE_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";
mod fetch;
pub(crate) use fetch::{fetch_ocsp_response, fetch_ocsp_response_async};
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use chrono::{TimeZone, Utc};
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
use wasm_bindgen_test::wasm_bindgen_test;
use crate::{
crypto::ocsp::OcspResponse,
status_tracker::StatusTracker,
validation_status::{
SIGNING_CREDENTIAL_NOT_REVOKED, SIGNING_CREDENTIAL_OCSP_UNKNOWN,
SIGNING_CREDENTIAL_REVOKED,
},
};
fn signing_cert_chain() -> Vec<Vec<u8>> {
let pem = include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem");
pem::parse_many(pem)
.unwrap()
.into_iter()
.map(|p| p.into_contents())
.collect()
}
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn good() {
let rsp_data = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der");
let mut validation_log = StatusTracker::default();
let test_time = Utc.with_ymd_and_hms(2023, 2, 1, 8, 0, 0).unwrap();
let ocsp_data = OcspResponse::from_der_checked(
rsp_data,
&signing_cert_chain(),
Some(test_time),
&mut validation_log,
)
.unwrap();
assert_eq!(ocsp_data.revoked_at, None);
assert!(ocsp_data.ocsp_certs.is_some());
assert!(validation_log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
}
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn revoked() {
let rsp_data = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der");
let mut validation_log = StatusTracker::default();
let test_time = Utc.with_ymd_and_hms(2024, 2, 1, 8, 0, 0).unwrap();
let ocsp_data = OcspResponse::from_der_checked(
rsp_data,
&signing_cert_chain(),
Some(test_time),
&mut validation_log,
)
.unwrap();
assert!(ocsp_data.revoked_at.is_some());
assert!(validation_log.has_status(SIGNING_CREDENTIAL_REVOKED));
}
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn unknown() {
let rsp_data = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_unknown.der");
let mut validation_log = StatusTracker::default();
let test_time = Utc.with_ymd_and_hms(2024, 2, 1, 8, 0, 0).unwrap();
let ocsp_data = OcspResponse::from_der_checked(
rsp_data,
&signing_cert_chain(),
Some(test_time),
&mut validation_log,
)
.unwrap();
assert!(ocsp_data.revoked_at.is_none());
assert!(validation_log.has_any_error());
assert!(validation_log.has_status(SIGNING_CREDENTIAL_OCSP_UNKNOWN));
}
const OCSP_EMPTY_CERTS_DER: &[u8] = &[
0x30, 0x5d, 0x0a, 0x01, 0x00, 0xa0, 0x58, 0x30, 0x56, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01,
0x01, 0x04, 0x49, 0x30, 0x47, 0x30, 0x2b, 0xa2, 0x16, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18,
0x0f, 0x32, 0x30, 0x32, 0x33, 0x30, 0x31, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x05, 0x00, 0xde, 0xad, 0xbe, 0xef, 0xa0, 0x02, 0x30, 0x00, ];
#[test]
fn from_der_checked_empty_certs_returns_default_not_panic() {
let mut log = StatusTracker::default();
let result = OcspResponse::from_der_checked(OCSP_EMPTY_CERTS_DER, &[], None, &mut log);
assert!(result.is_ok());
assert!(result.unwrap().ocsp_certs.is_none());
}
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn validity() {
let rsp_data = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der");
let mut validation_log = StatusTracker::default();
let test_time = Utc.with_ymd_and_hms(2026, 2, 1, 8, 0, 0).unwrap();
let ocsp_data = OcspResponse::from_der_checked(
rsp_data,
&signing_cert_chain(),
Some(test_time),
&mut validation_log,
)
.unwrap();
assert!(ocsp_data.revoked_at.is_none());
assert!(validation_log.has_any_error());
assert!(validation_log.has_status(SIGNING_CREDENTIAL_REVOKED));
}
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn good_response_for_wrong_cert_is_ignored() {
let rsp_data = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der");
let mut validation_log = StatusTracker::default();
let test_time = Utc.with_ymd_and_hms(2023, 2, 1, 8, 0, 0).unwrap();
let mut wrong_chain = signing_cert_chain();
wrong_chain.reverse();
let ocsp_data = OcspResponse::from_der_checked(
rsp_data,
&wrong_chain,
Some(test_time),
&mut validation_log,
)
.unwrap();
assert!(ocsp_data.certificate_serial_num.is_empty());
assert!(!validation_log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
}
}