use c2pa_status_tracker::{log_item, validation_codes, StatusTracker};
use chrono::{DateTime, NaiveDateTime, Utc};
use rasn_ocsp::{BasicOcspResponse, CertStatus, OcspResponseStatus};
use rasn_pkix::CrlReason;
use thiserror::Error;
use crate::internal::time;
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>>>,
}
impl Default for OcspResponse {
fn default() -> Self {
Self {
ocsp_der: Vec::new(),
next_update: time::utc_now(),
revoked_at: None,
ocsp_certs: None,
}
}
}
impl OcspResponse {
pub(crate) fn from_der_checked(
der: &[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);
}
if output.ocsp_certs.is_none() {
output.ocsp_certs = Some(cert_der_vec);
}
}
for single_response in &response_data.responses {
let cert_status = &single_response.cert_status;
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 {
this_update
};
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 && now <= next_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 {
return Ok(output);
}
}
CertStatus::Revoked(revoked_info) => {
if let Some(reason) = revoked_info.revocation_reason {
if reason == CrlReason::RemoveFromCRL {
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 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 revoked_at_native = NaiveDateTime::parse_from_str(
&revocation_time.to_string(),
DATE_FMT,
)
.map_err(|_e| OcspError::InvalidCertificate)?;
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 {
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,
);
}
}
CertStatus::Unknown(_) => return Err(OcspError::CertificateStatusUnknown),
}
}
validation_log.append(&internal_validation_log);
Ok(output)
}
}
#[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";
#[cfg(not(target_arch = "wasm32"))]
mod fetch;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use fetch::fetch_ocsp_response;