use asn1_rs::FromDer;
use async_generic::async_generic;
use chrono::{DateTime, Utc};
use coset::{cbor::value::Value, CoseSign1, Label};
use x509_parser::prelude::X509Certificate;
use crate::{
context::Context,
crypto::{
asn1::rfc3161::TstInfo,
cose::{
cert_chain_from_sign1, check_end_entity_certificate_profile, validate_cose_tst_info,
validate_cose_tst_info_async, CertificateTrustError, CertificateTrustPolicy, CoseError,
},
ocsp::OcspResponse,
},
log_item,
settings::Settings,
status_tracker::StatusTracker,
validation_status::{
self, SIGNING_CREDENTIAL_NOT_REVOKED, SIGNING_CREDENTIAL_OCSP_INACCESSIBLE,
SIGNING_CREDENTIAL_OCSP_SKIPPED, SIGNING_CREDENTIAL_REVOKED, SIGNING_CREDENTIAL_UNTRUSTED,
},
};
const OCSP_OID_STR: &str = "1.3.6.1.5.5.7.3.9";
#[async_generic(async_signature(
sign1: &CoseSign1,
data: &[u8],
fetch_policy: OcspFetchPolicy,
ctp: &CertificateTrustPolicy,
ocsp_responses: Option<&Vec<Vec<u8>>>,
tst_info: Option<&TstInfo>,
validation_log: &mut StatusTracker,
context: &Context,
))]
#[allow(clippy::too_many_arguments)]
pub fn check_ocsp_status(
sign1: &CoseSign1,
data: &[u8],
fetch_policy: OcspFetchPolicy,
ctp: &CertificateTrustPolicy,
ocsp_responses: Option<&Vec<Vec<u8>>>,
tst_info: Option<&TstInfo>,
validation_log: &mut StatusTracker,
context: &Context,
) -> Result<OcspResponse, CoseError> {
let live_override_fallback = if fetch_policy == OcspFetchPolicy::FetchAllowed
&& context.settings().verify.ocsp_fetch_should_override
{
let mut live_log = StatusTracker::default();
let live_result = if _sync {
fetch_and_check_ocsp_response(sign1, data, ctp, tst_info, &mut live_log, context)
} else {
fetch_and_check_ocsp_response_async(sign1, data, ctp, tst_info, &mut live_log, context)
.await
};
match live_result {
Err(err) => {
validation_log.append(&live_log);
return Err(err);
}
Ok(_) if live_log.has_status(validation_status::SIGNING_CREDENTIAL_REVOKED) => {
validation_log.append(&live_log);
return Err(CoseError::CertificateTrustError(
CertificateTrustError::CertificateNotTrusted,
));
}
Ok(ocsp_response)
if live_log.has_status(validation_status::SIGNING_CREDENTIAL_NOT_REVOKED) =>
{
validation_log.append(&live_log);
return Ok(ocsp_response);
}
Ok(ocsp_response) => Some((ocsp_response, live_log)),
}
} else {
None
};
if context
.settings()
.builder
.certificate_status_should_override
.unwrap_or(false)
{
if let Some(ocsp_response_ders) = ocsp_responses {
if !ocsp_response_ders.is_empty() {
let mut cert_status_log = StatusTracker::default();
let result = if _sync {
process_ocsp_responses(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
&mut cert_status_log,
context.settings(),
)
} else {
process_ocsp_responses_async(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
&mut cert_status_log,
context.settings(),
)
.await
};
if live_override_fallback.is_none()
|| result.is_err()
|| cert_status_log.has_status(validation_status::SIGNING_CREDENTIAL_NOT_REVOKED)
{
validation_log.append(&cert_status_log);
return result;
}
}
}
}
if let Some(ocsp_response_der) = get_ocsp_der(sign1) {
let mut ocsp_log = StatusTracker::default();
let result = if _sync {
check_stapled_ocsp_response(
sign1,
&ocsp_response_der,
data,
ctp,
tst_info,
&mut ocsp_log,
context.settings(),
)
} else {
check_stapled_ocsp_response_async(
sign1,
&ocsp_response_der,
data,
ctp,
tst_info,
&mut ocsp_log,
context.settings(),
)
.await
};
if let Ok(ocsp_response) = result {
if ocsp_log.has_status(validation_status::SIGNING_CREDENTIAL_REVOKED) {
return Err(log_item!(
"",
format!(
"signing cert revoked: {}",
ocsp_response.certificate_serial_num
),
"check_ocsp_status"
)
.validation_status(SIGNING_CREDENTIAL_REVOKED)
.failure_as_err(
validation_log,
CoseError::CertificateTrustError(CertificateTrustError::CertificateNotTrusted),
));
}
if ocsp_log.has_status(validation_status::SIGNING_CREDENTIAL_NOT_REVOKED) {
log_item!(
"",
format!(
"signing cert not revoked: {}",
ocsp_response.certificate_serial_num
),
"check_ocsp_status"
)
.validation_status(SIGNING_CREDENTIAL_NOT_REVOKED)
.success(validation_log);
return Ok(ocsp_response);
}
}
if fetch_policy == OcspFetchPolicy::DoNotFetch {
if let Some(ocsp_response_ders) = ocsp_responses.filter(|ders| !ders.is_empty()) {
let mut cert_status_log = StatusTracker::default();
let result = if _sync {
process_ocsp_responses(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
&mut cert_status_log,
context.settings(),
)
} else {
process_ocsp_responses_async(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
&mut cert_status_log,
context.settings(),
)
.await
};
if result.is_err()
|| cert_status_log.has_status(validation_status::SIGNING_CREDENTIAL_NOT_REVOKED)
{
validation_log.append(&cert_status_log);
return result;
}
}
log_item!("", "OCSP fetching skipped", "check_ocsp_status")
.validation_status(SIGNING_CREDENTIAL_OCSP_SKIPPED)
.informational(validation_log);
return Ok(OcspResponse::default());
}
}
if let Some((live_response, live_log)) = live_override_fallback {
if let Some(ocsp_response_ders) = ocsp_responses {
if !ocsp_response_ders.is_empty() {
let mut cert_status_log = StatusTracker::default();
let result = if _sync {
process_ocsp_responses(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
&mut cert_status_log,
context.settings(),
)
} else {
process_ocsp_responses_async(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
&mut cert_status_log,
context.settings(),
)
.await
};
if result.is_err()
|| cert_status_log.has_status(validation_status::SIGNING_CREDENTIAL_NOT_REVOKED)
{
validation_log.append(&cert_status_log);
return result;
}
}
}
validation_log.append(&live_log);
return Ok(live_response);
}
match fetch_policy {
OcspFetchPolicy::FetchAllowed => {
if _sync {
fetch_and_check_ocsp_response(sign1, data, ctp, tst_info, validation_log, context)
} else {
fetch_and_check_ocsp_response_async(
sign1,
data,
ctp,
tst_info,
validation_log,
context,
)
.await
}
}
OcspFetchPolicy::DoNotFetch => {
if let Some(ocsp_response_ders) = ocsp_responses {
if !ocsp_response_ders.is_empty() {
if _sync {
process_ocsp_responses(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
validation_log,
context.settings(),
)
} else {
process_ocsp_responses_async(
sign1,
data,
ctp,
ocsp_response_ders,
tst_info,
validation_log,
context.settings(),
)
.await
}
} else {
log_item!("", "OCSP fetching skipped", "check_ocsp_status")
.validation_status(SIGNING_CREDENTIAL_OCSP_SKIPPED)
.informational(validation_log);
Ok(OcspResponse::default())
}
} else {
log_item!("", "OCSP fetching skipped", "check_ocsp_status")
.validation_status(SIGNING_CREDENTIAL_OCSP_SKIPPED)
.informational(validation_log);
Ok(OcspResponse::default())
}
}
}
}
#[async_generic]
fn process_ocsp_responses(
sign1: &CoseSign1,
data: &[u8],
ctp: &CertificateTrustPolicy,
ocsp_response_ders: &[Vec<u8>],
tst_info: Option<&TstInfo>,
validation_log: &mut StatusTracker,
settings: &Settings,
) -> Result<OcspResponse, CoseError> {
let mut not_revoked_response = None;
for ocsp_response_der in ocsp_response_ders {
let mut current_validation_log = StatusTracker::default();
let ocsp_response = if _sync {
check_stapled_ocsp_response(
sign1,
ocsp_response_der,
data,
ctp,
tst_info,
&mut current_validation_log,
settings,
)
} else {
check_stapled_ocsp_response_async(
sign1,
ocsp_response_der,
data,
ctp,
tst_info,
&mut current_validation_log,
settings,
)
.await
};
if let Ok(ocsp_response) = ocsp_response {
if current_validation_log.has_status(validation_status::SIGNING_CREDENTIAL_REVOKED) {
return Err(log_item!(
"",
format!(
"signing cert revoked: {}",
ocsp_response.certificate_serial_num
),
"check_ocsp_status"
)
.validation_status(SIGNING_CREDENTIAL_REVOKED)
.failure_as_err(
validation_log,
CoseError::CertificateTrustError(CertificateTrustError::CertificateNotTrusted),
));
}
if current_validation_log.has_status(validation_status::SIGNING_CREDENTIAL_NOT_REVOKED)
&& not_revoked_response.is_none()
{
not_revoked_response = Some(ocsp_response);
}
}
}
if let Some(ocsp_response) = not_revoked_response {
log_item!(
"",
format!(
"signing cert not revoked: {}",
ocsp_response.certificate_serial_num
),
"check_ocsp_status"
)
.validation_status(SIGNING_CREDENTIAL_NOT_REVOKED)
.success(validation_log);
Ok(ocsp_response)
} else {
Ok(OcspResponse::default())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OcspFetchPolicy {
FetchAllowed,
DoNotFetch,
}
#[async_generic]
fn check_stapled_ocsp_response(
sign1: &CoseSign1,
ocsp_response_der: &[u8],
data: &[u8],
ctp: &CertificateTrustPolicy,
tst_info: Option<&TstInfo>,
validation_log: &mut StatusTracker,
settings: &Settings,
) -> Result<OcspResponse, CoseError> {
let mut local_log_sync = StatusTracker::default();
let time_stamp_info = match tst_info {
Some(tst_info) => Ok(tst_info.clone()),
None => {
if _sync {
validate_cose_tst_info(
sign1,
data,
ctp,
&mut local_log_sync,
settings.verify.verify_timestamp_trust,
)
} else {
validate_cose_tst_info_async(
sign1,
data,
ctp,
&mut local_log_sync,
settings.verify.verify_timestamp_trust,
)
.await
}
}
};
let (tst_info, signing_time) = match time_stamp_info {
Ok(tstinfo) => {
let signing_time = tstinfo.gen_time.clone().into();
(Some(tstinfo), Some(signing_time))
}
Err(_) => (None, None),
};
let signing_cert_chain = cert_chain_from_sign1(sign1)?;
let signing_cert_chain = ocsp_signing_chain(&signing_cert_chain, ctp);
let mut current_validation_log = StatusTracker::default();
let Ok(ocsp_data) = OcspResponse::from_der_checked(
ocsp_response_der,
&signing_cert_chain,
signing_time,
&mut current_validation_log,
) else {
return Ok(OcspResponse::default());
};
if let Some(ocsp_certs) = &ocsp_data.ocsp_certs {
let Some(first_cert) = ocsp_certs.first() else {
return Ok(OcspResponse::default());
};
let mut new_ctp = CertificateTrustPolicy::default();
new_ctp.clear_ekus();
new_ctp.add_mandatory_ekus(OCSP_OID_STR.as_bytes()); if check_end_entity_certificate_profile(
first_cert,
&new_ctp,
validation_log,
tst_info.as_ref(),
)
.is_err()
{
return Ok(OcspResponse::default());
}
let ocsp_cert_chain = extend_ocsp_cert_chain(ocsp_certs, &signing_cert_chain);
if ctp
.check_certificate_trust(
&ocsp_cert_chain,
first_cert,
signing_time.map(|t| t.timestamp()),
)
.is_err()
{
return Ok(OcspResponse::default());
}
} else {
return Ok(OcspResponse::default());
}
validation_log.append(¤t_validation_log);
Ok(ocsp_data)
}
fn extend_ocsp_cert_chain(ocsp_certs: &[Vec<u8>], signing_cert_chain: &[Vec<u8>]) -> Vec<Vec<u8>> {
if let ([responder_der], [_, issuer_der, ..]) = (ocsp_certs, signing_cert_chain) {
if let (Ok((_, responder)), Ok((_, issuer))) = (
X509Certificate::from_der(responder_der),
X509Certificate::from_der(issuer_der),
) {
if responder.issuer().as_raw() == issuer.subject().as_raw() {
return [ocsp_certs, &signing_cert_chain[1..]].concat();
}
}
}
ocsp_certs.to_vec()
}
fn ocsp_signing_chain(certs: &[Vec<u8>], ctp: &CertificateTrustPolicy) -> Vec<Vec<u8>> {
let Some(leaf_der) = certs.first() else {
return certs.to_vec();
};
let Ok((_, leaf)) = X509Certificate::from_der(leaf_der) else {
return certs.to_vec();
};
let leaf_issuer_raw = leaf.issuer().as_raw();
if let Some(next_der) = certs.get(1) {
if let Ok((_, next)) = X509Certificate::from_der(next_der) {
if next.subject().as_raw() == leaf_issuer_raw {
return certs.to_vec();
}
}
}
for anchor in ctp.signing_trust_anchors() {
for anchor_der in &anchor.trust_anchor_ders {
if let Ok((_, anchor_cert)) = X509Certificate::from_der(anchor_der) {
if anchor_cert.subject().as_raw() == leaf_issuer_raw {
let mut chain = Vec::with_capacity(certs.len() + 1);
chain.push(leaf_der.clone());
chain.push(anchor_der.clone());
chain.extend_from_slice(&certs[1..]);
return chain;
}
}
}
}
certs.to_vec()
}
#[async_generic(async_signature(
sign1: &CoseSign1,
data: &[u8],
ctp: &CertificateTrustPolicy,
tst_info: Option<&TstInfo>,
validation_log: &mut StatusTracker,
context: &crate::context::Context,
))]
pub(crate) fn fetch_and_check_ocsp_response(
sign1: &CoseSign1,
data: &[u8],
ctp: &CertificateTrustPolicy,
tst_info: Option<&TstInfo>,
validation_log: &mut StatusTracker,
context: &crate::context::Context,
) -> Result<OcspResponse, CoseError> {
let certs = cert_chain_from_sign1(sign1)?;
let certs = ocsp_signing_chain(&certs, ctp);
let signing_time: Option<DateTime<Utc>> = match tst_info {
Some(tst_info) => Some(tst_info.gen_time.clone().into()),
None => validate_cose_tst_info(
sign1,
data,
ctp,
validation_log,
context.settings().verify.verify_timestamp_trust,
)
.ok()
.map(|tst_info| tst_info.gen_time.clone().into()),
};
for i in 1..certs.len().saturating_sub(1) {
let ca_der = if _sync {
crate::crypto::ocsp::fetch_ocsp_response(&certs, i, context)
} else {
crate::crypto::ocsp::fetch_ocsp_response_async(&certs, i, context).await
};
let Some(ca_der) = ca_der else {
continue;
};
let mut ca_log = StatusTracker::default();
let ca_response =
validate_fetched_ocsp(&ca_der, &certs[i..], ctp, signing_time, &mut ca_log);
if ca_response.revoked_at.is_some() {
log_item!("", "issuing CA revoked", "fetch_and_check_ocsp_response")
.validation_status(SIGNING_CREDENTIAL_UNTRUSTED)
.failure_no_throw(validation_log, CertificateTrustError::CertificateNotTrusted);
return Err(CoseError::CertificateTrustError(
CertificateTrustError::CertificateNotTrusted,
));
}
}
let ocsp_der = if _sync {
crate::crypto::ocsp::fetch_ocsp_response(&certs, 0, context)
} else {
crate::crypto::ocsp::fetch_ocsp_response_async(&certs, 0, context).await
};
let Some(ocsp_response_der) = ocsp_der else {
log_item!(
"",
"signing cert not fetched".to_string(),
"fetch_and_check_ocsp_response"
)
.validation_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE)
.informational(validation_log);
return Ok(OcspResponse::default());
};
let response = validate_fetched_ocsp(
&ocsp_response_der,
&certs,
ctp,
signing_time,
validation_log,
);
if response.revoked_at.is_some() {
return Err(CoseError::CertificateTrustError(
CertificateTrustError::CertificateNotTrusted,
));
}
Ok(response)
}
fn validate_fetched_ocsp(
ocsp_response_der: &[u8],
subject_chain: &[Vec<u8>],
ctp: &CertificateTrustPolicy,
signing_time: Option<DateTime<Utc>>,
validation_log: &mut StatusTracker,
) -> OcspResponse {
let mut current_validation_log = StatusTracker::default();
let ocsp_data = match OcspResponse::from_der_checked(
ocsp_response_der,
subject_chain,
signing_time,
&mut current_validation_log,
) {
Ok(data) => data,
Err(_) => return OcspResponse::default(),
};
if let Some(ocsp_certs) = &ocsp_data.ocsp_certs {
let Some(first_cert) = ocsp_certs.first() else {
return OcspResponse::default();
};
let mut new_ctp = CertificateTrustPolicy::default();
new_ctp.clear_ekus();
new_ctp.add_mandatory_ekus(OCSP_OID_STR.as_bytes()); if check_end_entity_certificate_profile(
first_cert,
&new_ctp,
&mut current_validation_log,
None,
)
.is_err()
{
return OcspResponse::default();
}
let ocsp_cert_chain = extend_ocsp_cert_chain(ocsp_certs, subject_chain);
if ctp
.check_certificate_trust(&ocsp_cert_chain, first_cert, None)
.is_err()
{
return OcspResponse::default();
}
} else {
return OcspResponse::default();
}
validation_log.append(¤t_validation_log);
ocsp_data
}
pub fn get_ocsp_der(sign1: &coset::CoseSign1) -> Option<Vec<u8>> {
let der = sign1
.unprotected
.rest
.iter()
.find_map(|x: &(Label, Value)| {
if x.0 == Label::Text("rVals".to_string()) {
Some(x.1.clone())
} else {
None
}
})?;
let Value::Map(rvals_map) = der else {
return None;
};
rvals_map.iter().find_map(|x: &(Value, Value)| {
if x.0 == Value::Text("ocspVals".to_string()) {
x.1.as_array()
.and_then(|ocsp_rsp_val| ocsp_rsp_val.first())
.and_then(Value::as_bytes)
.cloned()
} else {
None
}
})
}
#[cfg(test)]
mod tests {
#![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 super::*;
use crate::crypto::cert_chain_pem_to_der;
fn leaf_and_intermediate() -> Vec<Vec<u8>> {
let pem = include_bytes!("../../../tests/fixtures/certs/es256.pub");
cert_chain_pem_to_der(pem).unwrap()
}
fn ocsp_signing_chain() -> Vec<Vec<u8>> {
let pem = include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem");
cert_chain_pem_to_der(pem).unwrap()
}
#[test]
fn extends_chain_only_when_responder_issued_by_signing_ca() {
let chain = leaf_and_intermediate();
assert_eq!(extend_ocsp_cert_chain(&chain[..1], &chain), chain);
assert_eq!(extend_ocsp_cert_chain(&chain[1..], &chain), &chain[1..]);
}
#[test]
fn ocsp_signing_chain_resolves_issuer_from_anchor() {
use super::ocsp_signing_chain;
use crate::crypto::cose::{CertificateTrustPolicy, TrustAnchorType};
let chain = leaf_and_intermediate(); let leaf = vec![chain[0].clone()];
let mut ctp = CertificateTrustPolicy::default();
ctp.add_trust_anchors(
include_bytes!("../../../tests/fixtures/certs/es256.pub"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
assert_eq!(ocsp_signing_chain(&leaf, &ctp), chain);
assert_eq!(ocsp_signing_chain(&chain, &ctp), chain);
assert_eq!(
ocsp_signing_chain(&leaf, &CertificateTrustPolicy::new()),
leaf
);
assert!(ocsp_signing_chain(&[], &ctp).is_empty());
let junk_leaf = vec![vec![0xde, 0xad, 0xbe, 0xef]];
assert_eq!(ocsp_signing_chain(&junk_leaf, &ctp), junk_leaf);
let leaf_plus_junk = vec![chain[0].clone(), vec![0u8; 4]];
assert_eq!(
ocsp_signing_chain(&leaf_plus_junk, &ctp),
vec![chain[0].clone(), chain[1].clone(), vec![0u8; 4]]
);
let mut junk_ctp = CertificateTrustPolicy::new();
junk_ctp
.add_trust_anchors(
b"-----BEGIN CERTIFICATE-----\nAAAAAAAA\n-----END CERTIFICATE-----\n",
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
assert_eq!(ocsp_signing_chain(&leaf, &junk_ctp), leaf);
}
#[test]
fn revoked_lone_leaf_detected_after_issuer_resolved() {
use chrono::{TimeZone, Utc};
use super::ocsp_signing_chain;
use crate::{
crypto::{
cose::{CertificateTrustPolicy, TrustAnchorType},
ocsp::OcspResponse,
},
status_tracker::StatusTracker,
validation_status::SIGNING_CREDENTIAL_REVOKED,
};
let full_chain = cert_chain_pem_to_der(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"
))
.unwrap();
let leaf = vec![full_chain[0].clone()];
let revoked = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der");
let test_time = Utc.with_ymd_and_hms(2024, 2, 1, 8, 0, 0).unwrap();
let mut log = StatusTracker::default();
let _ = OcspResponse::from_der_checked(revoked, &leaf, Some(test_time), &mut log);
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
let mut ctp = CertificateTrustPolicy::new();
ctp.add_trust_anchors(
include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
let resolved = ocsp_signing_chain(&leaf, &ctp);
assert_eq!(resolved, full_chain);
let mut log2 = StatusTracker::default();
let ocsp_data =
OcspResponse::from_der_checked(revoked, &resolved, Some(test_time), &mut log2).unwrap();
assert!(ocsp_data.revoked_at.is_some());
assert!(log2.has_status(SIGNING_CREDENTIAL_REVOKED));
}
#[test]
fn extend_ocsp_cert_chain_builds_from_subject_issuer() {
let certs = ca_chain();
let responder = vec![certs[2].clone()]; assert_eq!(extend_ocsp_cert_chain(&responder, &certs[1..]).len(), 2);
assert_eq!(extend_ocsp_cert_chain(&responder, &certs).len(), 1);
}
#[test]
fn validate_fetched_ocsp_rejects_unauthorized_responder() {
let rsp = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der");
let chain = ocsp_signing_chain();
let test_time = Utc.with_ymd_and_hms(2024, 2, 1, 8, 0, 0).unwrap();
let mut log = StatusTracker::default();
let resp = validate_fetched_ocsp(
rsp,
&chain,
&CertificateTrustPolicy::default(),
Some(test_time),
&mut log,
);
assert!(resp.revoked_at.is_none());
}
#[test]
fn validate_fetched_ocsp_accepts_current_responder_for_historical_signing_time() {
let rsp = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der");
let chain = ocsp_signing_chain();
let test_time = Utc.with_ymd_and_hms(2024, 2, 1, 8, 0, 0).unwrap();
let mut ctp = CertificateTrustPolicy::new();
ctp.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem"
))
.unwrap();
let mut log = StatusTracker::default();
let resp = validate_fetched_ocsp(rsp, &chain, &ctp, Some(test_time), &mut log);
assert!(resp.revoked_at.is_some());
assert!(log.has_status(SIGNING_CREDENTIAL_REVOKED));
}
#[test]
fn validate_fetched_ocsp_ignores_unusable_response() {
let chain = ocsp_signing_chain();
let mut log = StatusTracker::default();
let resp = validate_fetched_ocsp(
&[0xde, 0xad, 0xbe, 0xef],
&chain,
&CertificateTrustPolicy::default(),
None,
&mut log,
);
assert!(resp.revoked_at.is_none());
assert!(resp.ocsp_certs.is_none());
}
fn ca_chain() -> Vec<Vec<u8>> {
let pem = include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_ca_chain.pem");
cert_chain_pem_to_der(pem).unwrap()
}
fn sign1_with_x5chain(certs: &[Vec<u8>]) -> CoseSign1 {
let x5 = Value::Array(certs.iter().map(|c| Value::Bytes(c.clone())).collect());
let mut unprotected = coset::Header::default();
unprotected
.rest
.push((Label::Text("x5chain".to_string()), x5));
CoseSign1 {
protected: coset::ProtectedHeader::default(),
unprotected,
payload: None,
signature: vec![0u8; 8],
}
}
#[test]
fn process_ocsp_responses_skips_expired_good_response() {
use crate::crypto::asn1::{rfc3161::MessageImprint, AlgorithmIdentifier, GeneralizedTime};
let chain = ocsp_signing_chain();
let sign1 = sign1_with_x5chain(&chain);
let good = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der").to_vec();
let revoked =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der").to_vec();
let tst_info = TstInfo {
version: 1.into(),
policy: "1.2.3".parse().unwrap(),
message_imprint: MessageImprint {
hash_algorithm: AlgorithmIdentifier {
algorithm: "2.16.840.1.101.3.4.2.1".parse().unwrap(),
},
hashed_message: bcder::OctetString::new(bytes::Bytes::from_static(&[0; 32])),
},
serial_number: 1.into(),
gen_time: GeneralizedTime::from_der_bytes(b"\x18\x0f20260201080000Z").unwrap(),
accuracy: None,
ordering: None,
nonce: None,
tsa: None,
extensions: None,
};
let mut ctp = CertificateTrustPolicy::new();
ctp.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem"
))
.unwrap();
let settings = Settings::default();
let mut log = StatusTracker::default();
let response = process_ocsp_responses(
&sign1,
b"data",
&ctp,
std::slice::from_ref(&good),
Some(&tst_info),
&mut log,
&settings,
)
.unwrap();
assert!(response.ocsp_certs.is_none());
assert!(response.revoked_at.is_none());
assert!(!log.has_any_error());
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
let mut log = StatusTracker::default();
let result = process_ocsp_responses(
&sign1,
b"data",
&ctp,
&[good, revoked],
Some(&tst_info),
&mut log,
&settings,
);
assert!(matches!(
result,
Err(CoseError::CertificateTrustError(
CertificateTrustError::CertificateNotTrusted
))
));
assert!(log.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
}
#[c2pa_macros::c2pa_test_async]
async fn fetched_leaf_revocation_returns_trust_error() {
use std::{
io::{Cursor, Read},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use http::{Request, Response};
use rasn_pkix::Certificate;
use crate::http::{AsyncHttpResolver, HttpResolverError, SyncHttpResolver};
struct RevokedResolver(Arc<AtomicUsize>);
impl SyncHttpResolver for RevokedResolver {
fn http_resolve(
&self,
_: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
self.0.fetch_add(1, Ordering::SeqCst);
let der = include_bytes!(
"../../../tests/fixtures/crypto/ocsp/es256_revoked_response.der"
)
.to_vec();
Ok(Response::builder()
.status(200)
.body(Box::new(Cursor::new(der)) as Box<dyn Read>)
.unwrap())
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
impl AsyncHttpResolver for RevokedResolver {
async fn http_resolve_async(
&self,
request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
self.http_resolve(request)
}
}
let mut chain = leaf_and_intermediate();
let mut leaf: Certificate = rasn::der::decode(&chain[0]).unwrap();
let online_leaf: Certificate = rasn::der::decode(&ca_chain()[0]).unwrap();
let aia = online_leaf
.tbs_certificate
.extensions
.as_ref()
.unwrap()
.iter()
.find(|extension| extension.extn_id.to_string() == "1.3.6.1.5.5.7.1.1")
.cloned()
.unwrap();
leaf.tbs_certificate.extensions.as_mut().unwrap().push(aia);
chain[0] = rasn::der::encode(&leaf).unwrap();
let sign1 = sign1_with_x5chain(&chain);
let mut ctp = CertificateTrustPolicy::default();
ctp.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/es256_revoked_responder.pem"
))
.unwrap();
for use_async in [false, true] {
let calls = Arc::new(AtomicUsize::new(0));
let context = Context::new()
.with_resolver(RevokedResolver(calls.clone()))
.with_resolver_async(RevokedResolver(calls.clone()))
.with_settings(serde_json::json!({
"verify": {"ocsp_fetch": true, "ocsp_fetch_should_override": false}
}))
.unwrap();
let mut log = StatusTracker::default();
let result = if use_async {
check_ocsp_status_async(
&sign1,
b"payload",
OcspFetchPolicy::FetchAllowed,
&ctp,
None,
None,
&mut log,
&context,
)
.await
} else {
check_ocsp_status(
&sign1,
b"payload",
OcspFetchPolicy::FetchAllowed,
&ctp,
None,
None,
&mut log,
&context,
)
};
assert!(calls.load(Ordering::SeqCst) > 0);
assert!(log.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(matches!(
result,
Err(CoseError::CertificateTrustError(
CertificateTrustError::CertificateNotTrusted
))
));
}
}
#[test]
fn fetch_and_check_queries_leaf_and_ca() {
use std::io::{Cursor, Read};
use http::{Request, Response};
use crate::{
context::Context,
http::{HttpResolverError, SyncHttpResolver},
};
struct OcspResolver;
impl SyncHttpResolver for OcspResolver {
fn http_resolve(
&self,
_request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
let der =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der")
.to_vec();
Ok(Response::builder()
.status(200)
.body(Box::new(Cursor::new(der)) as Box<dyn Read>)
.unwrap())
}
}
let sign1 = sign1_with_x5chain(&ca_chain());
let context = Context::new().with_resolver(OcspResolver);
let ctp = CertificateTrustPolicy::default();
let mut log = StatusTracker::default();
let result =
fetch_and_check_ocsp_response(&sign1, b"payload", &ctp, None, &mut log, &context);
assert!(result.is_ok());
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn fetch_and_check_queries_leaf_and_ca_async() {
use std::io::{Cursor, Read};
use async_trait::async_trait;
use http::{Request, Response};
use crate::{
context::Context,
http::{AsyncHttpResolver, HttpResolverError},
};
struct OcspResolver;
#[async_trait]
impl AsyncHttpResolver for OcspResolver {
async fn http_resolve_async(
&self,
_request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
let der =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der")
.to_vec();
Ok(Response::builder()
.status(200)
.body(Box::new(Cursor::new(der)) as Box<dyn Read>)
.unwrap())
}
}
let sign1 = sign1_with_x5chain(&ca_chain());
let context = Context::new().with_resolver_async(OcspResolver);
let ctp = CertificateTrustPolicy::default();
let mut log = StatusTracker::default();
let result =
fetch_and_check_ocsp_response_async(&sign1, b"payload", &ctp, None, &mut log, &context)
.await;
assert!(result.is_ok());
}
#[test]
fn fetch_and_check_handles_unreachable_responders() {
use std::io::Read;
use http::{Request, Response};
use crate::{
context::Context,
http::{HttpResolverError, SyncHttpResolver},
};
struct FailingResolver;
impl SyncHttpResolver for FailingResolver {
fn http_resolve(
&self,
_request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
Ok(Response::builder()
.status(404)
.body(Box::new(std::io::empty()) as Box<dyn Read>)
.unwrap())
}
}
let sign1 = sign1_with_x5chain(&ca_chain());
let context = Context::new().with_resolver(FailingResolver);
let ctp = CertificateTrustPolicy::default();
let mut log = StatusTracker::default();
let result =
fetch_and_check_ocsp_response(&sign1, b"payload", &ctp, None, &mut log, &context);
assert!(result.is_ok());
assert!(log.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
}
#[test]
fn check_ocsp_status_stapled_revoked_lone_leaf() {
use coset::{cbor::value::Value, CoseSign1, Header, Label, ProtectedHeader};
use super::{check_ocsp_status, OcspFetchPolicy};
use crate::{
context::Context,
crypto::cose::{CertificateTrustPolicy, TrustAnchorType},
status_tracker::StatusTracker,
};
let full_chain = cert_chain_pem_to_der(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"
))
.unwrap();
let revoked =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der").to_vec();
let mut unprotected = Header::default();
unprotected.rest.push((
Label::Text("x5chain".to_string()),
Value::Array(vec![Value::Bytes(full_chain[0].clone())]),
));
unprotected.rest.push((
Label::Text("rVals".to_string()),
Value::Map(vec![(
Value::Text("ocspVals".to_string()),
Value::Array(vec![Value::Bytes(revoked)]),
)]),
));
let sign1 = CoseSign1 {
protected: ProtectedHeader::default(),
unprotected,
payload: None,
signature: vec![0u8; 8],
};
let responder = include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem");
let mut anchored = CertificateTrustPolicy::new();
anchored
.add_trust_anchors(
include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
anchored.add_end_entity_credentials(responder).unwrap();
let context = Context::new();
let mut log = StatusTracker::default();
let with_anchor = check_ocsp_status(
&sign1,
b"data",
OcspFetchPolicy::DoNotFetch,
&anchored,
None,
None,
&mut log,
&context,
);
assert!(with_anchor.is_err());
let mut no_anchor = CertificateTrustPolicy::new();
no_anchor.add_end_entity_credentials(responder).unwrap();
let mut log2 = StatusTracker::default();
let without_anchor = check_ocsp_status(
&sign1,
b"data",
OcspFetchPolicy::DoNotFetch,
&no_anchor,
None,
None,
&mut log2,
&context,
);
assert!(without_anchor.is_ok());
assert!(log2.has_status(SIGNING_CREDENTIAL_OCSP_SKIPPED));
assert!(!log.has_status(SIGNING_CREDENTIAL_OCSP_SKIPPED));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn check_ocsp_status_stapled_do_not_fetch_sync_and_async() {
use crate::{crypto::cose::TrustAnchorType, status_tracker::LogKind};
let chain = ocsp_signing_chain();
let mut ctp = CertificateTrustPolicy::new();
ctp.add_trust_anchors(
include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
ctp.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem"
))
.unwrap();
let good = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der").to_vec();
let revoked =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der").to_vec();
let supplied = vec![good.clone()];
let context = Context::new();
for (staple, responses, expected) in [
(good, None, SIGNING_CREDENTIAL_OCSP_SKIPPED),
(revoked, None, SIGNING_CREDENTIAL_REVOKED),
(vec![0xff], None, SIGNING_CREDENTIAL_OCSP_SKIPPED),
(vec![0xff], Some(&supplied), SIGNING_CREDENTIAL_OCSP_SKIPPED),
] {
let mut sign1 = sign1_with_x5chain(&chain);
sign1.unprotected.rest.push((
Label::Text("rVals".to_string()),
Value::Map(vec![(
Value::Text("ocspVals".to_string()),
Value::Array(vec![Value::Bytes(staple)]),
)]),
));
let mut sync_log = StatusTracker::default();
let sync_result = check_ocsp_status(
&sign1,
b"data",
OcspFetchPolicy::DoNotFetch,
&ctp,
responses,
None,
&mut sync_log,
&context,
);
let mut async_log = StatusTracker::default();
let async_result = check_ocsp_status_async(
&sign1,
b"data",
OcspFetchPolicy::DoNotFetch,
&ctp,
responses,
None,
&mut async_log,
&context,
)
.await;
for (result, log) in [(sync_result, sync_log), (async_result, async_log)] {
assert_eq!(result.is_err(), expected == SIGNING_CREDENTIAL_REVOKED);
assert!(log.has_status(expected));
assert_eq!(log.logged_items().len(), 1);
if expected == SIGNING_CREDENTIAL_OCSP_SKIPPED {
assert_eq!(log.logged_items()[0].kind, LogKind::Informational);
} else {
assert!(!log.has_status(SIGNING_CREDENTIAL_OCSP_SKIPPED));
}
}
}
}
#[test]
fn check_ocsp_status_fetch_resolves_issuer_for_lone_leaf() {
use std::io::{Cursor, Read};
use coset::{cbor::value::Value, CoseSign1, Header, Label, ProtectedHeader};
use http::{Request, Response};
use super::{check_ocsp_status, OcspFetchPolicy};
use crate::{
context::Context,
crypto::cose::{CertificateTrustPolicy, TrustAnchorType},
http::{HttpResolverError, SyncHttpResolver},
status_tracker::StatusTracker,
validation_status::SIGNING_CREDENTIAL_OCSP_INACCESSIBLE,
};
let full_chain = cert_chain_pem_to_der(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"
))
.unwrap();
let mut unprotected = Header::default();
unprotected.rest.push((
Label::Text("x5chain".to_string()),
Value::Array(vec![Value::Bytes(full_chain[0].clone())]),
));
let sign1 = CoseSign1 {
protected: ProtectedHeader::default(),
unprotected,
payload: None,
signature: vec![0u8; 8],
};
struct RevokedResolver;
impl SyncHttpResolver for RevokedResolver {
fn http_resolve(
&self,
_request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
let der =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der")
.to_vec();
Ok(Response::builder()
.status(200)
.body(Box::new(Cursor::new(der)) as Box<dyn Read>)
.unwrap())
}
}
let context = Context::new().with_resolver(RevokedResolver);
let mut anchored = CertificateTrustPolicy::new();
anchored
.add_trust_anchors(
include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
let mut log = StatusTracker::default();
let _ = check_ocsp_status(
&sign1,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log,
&context,
);
assert!(!log.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
let mut log2 = StatusTracker::default();
let _ = check_ocsp_status(
&sign1,
b"data",
OcspFetchPolicy::FetchAllowed,
&CertificateTrustPolicy::new(),
None,
None,
&mut log2,
&context,
);
assert!(log2.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
}
#[test]
fn check_ocsp_status_fetch_override_bypasses_staple_and_falls_back_on_failure() {
use std::{
io::{Cursor, Read},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use coset::{cbor::value::Value, CoseSign1, Header, Label, ProtectedHeader};
use http::{Request, Response};
use super::{check_ocsp_status, OcspFetchPolicy};
use crate::{
context::Context,
crypto::cose::{CertificateTrustPolicy, TrustAnchorType},
http::{HttpResolverError, SyncHttpResolver},
settings::Settings,
status_tracker::StatusTracker,
validation_status::{
SIGNING_CREDENTIAL_NOT_REVOKED, SIGNING_CREDENTIAL_OCSP_INACCESSIBLE,
SIGNING_CREDENTIAL_REVOKED,
},
};
fn sign1_with_staple(ocsp: Option<Vec<u8>>) -> CoseSign1 {
let full_chain = cert_chain_pem_to_der(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"
))
.unwrap();
let mut unprotected = Header::default();
unprotected.rest.push((
Label::Text("x5chain".to_string()),
Value::Array(full_chain.into_iter().map(Value::Bytes).collect()),
));
if let Some(ocsp_bytes) = ocsp {
unprotected.rest.push((
Label::Text("rVals".to_string()),
Value::Map(vec![(
Value::Text("ocspVals".to_string()),
Value::Array(vec![Value::Bytes(ocsp_bytes)]),
)]),
));
}
CoseSign1 {
protected: ProtectedHeader::default(),
unprotected,
payload: None,
signature: vec![0u8; 8],
}
}
let mut anchored = CertificateTrustPolicy::new();
anchored
.add_trust_anchors(
include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
anchored
.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem"
))
.unwrap();
#[derive(Clone)]
struct CountingSyncResolver {
counter: Arc<AtomicUsize>,
status: u16,
body: Vec<u8>,
}
impl SyncHttpResolver for CountingSyncResolver {
fn http_resolve(
&self,
_request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
self.counter.fetch_add(1, Ordering::SeqCst);
if self.status == 200 {
Ok(Response::builder()
.status(200)
.body(Box::new(Cursor::new(self.body.clone())) as Box<dyn Read>)
.unwrap())
} else {
Ok(Response::builder()
.status(self.status)
.body(Box::new(std::io::empty()) as Box<dyn Read>)
.unwrap())
}
}
}
let good = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der").to_vec();
let revoked =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der").to_vec();
let sign1_revoked = sign1_with_staple(Some(revoked.clone()));
let counter1 = Arc::new(AtomicUsize::new(0));
let resolver1 = CountingSyncResolver {
counter: counter1.clone(),
status: 200,
body: good.clone(),
};
let context1 = Context::new().with_resolver(resolver1);
let mut log1 = StatusTracker::default();
let result1 = check_ocsp_status(
&sign1_revoked,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log1,
&context1,
);
assert!(result1.is_err());
assert!(log1.has_status(SIGNING_CREDENTIAL_REVOKED));
assert_eq!(counter1.load(Ordering::SeqCst), 0);
#[cfg(feature = "openssl")]
{
let mut anchored = anchored.clone();
let live_good = trusted_fresh_good_response(&mut anchored);
let counter2 = Arc::new(AtomicUsize::new(0));
let resolver2 = CountingSyncResolver {
counter: counter2.clone(),
status: 200,
body: live_good,
};
let settings2 = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch_should_override": true}}"#)
.unwrap();
let context2 = Context::new()
.with_settings(settings2)
.unwrap()
.with_resolver(resolver2);
let mut log2 = StatusTracker::default();
let result2 = check_ocsp_status(
&sign1_revoked,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log2,
&context2,
);
assert!(result2.is_ok());
assert!(counter2.load(Ordering::SeqCst) > 0);
assert!(log2.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(!log2.has_status(SIGNING_CREDENTIAL_REVOKED));
}
let assertion_responses = vec![good.clone(), revoked.clone()];
let sign1_without_staple = sign1_with_staple(None);
let counter2b = Arc::new(AtomicUsize::new(0));
let resolver2b = CountingSyncResolver {
counter: counter2b.clone(),
status: 404,
body: vec![],
};
let settings2b = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch_should_override": true}}"#)
.unwrap();
let context2b = Context::new()
.with_settings(settings2b)
.unwrap()
.with_resolver(resolver2b);
let mut log2b = StatusTracker::default();
let result2b = check_ocsp_status(
&sign1_without_staple,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
Some(&assertion_responses),
None,
&mut log2b,
&context2b,
);
assert!(result2b.is_err());
assert!(counter2b.load(Ordering::SeqCst) > 0);
assert!(log2b.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log2b.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
let sign1_good = sign1_with_staple(Some(good.clone()));
let counter3 = Arc::new(AtomicUsize::new(0));
let resolver3 = CountingSyncResolver {
counter: counter3.clone(),
status: 200,
body: revoked.clone(),
};
let settings3 = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch_should_override": true}}"#)
.unwrap();
let context3 = Context::new()
.with_settings(settings3)
.unwrap()
.with_resolver(resolver3);
let mut log3 = StatusTracker::default();
let result3 = check_ocsp_status(
&sign1_good,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log3,
&context3,
);
assert!(result3.is_err());
assert!(counter3.load(Ordering::SeqCst) > 0);
assert!(log3.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log3.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
let counter4 = Arc::new(AtomicUsize::new(0));
let resolver4 = CountingSyncResolver {
counter: counter4.clone(),
status: 404,
body: vec![],
};
let settings4 = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch_should_override": true}}"#)
.unwrap();
let context4 = Context::new()
.with_settings(settings4)
.unwrap()
.with_resolver(resolver4);
let mut log4 = StatusTracker::default();
let result4 = check_ocsp_status(
&sign1_revoked,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log4,
&context4,
);
assert!(result4.is_err());
assert!(counter4.load(Ordering::SeqCst) > 0);
assert!(log4.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log4.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
let sign1_no_staple = sign1_with_staple(None);
let counter5 = Arc::new(AtomicUsize::new(0));
let resolver5 = CountingSyncResolver {
counter: counter5.clone(),
status: 404,
body: vec![],
};
let settings5 = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch_should_override": true}}"#)
.unwrap();
let context5 = Context::new()
.with_settings(settings5)
.unwrap()
.with_resolver(resolver5);
let mut log5 = StatusTracker::default();
let result5 = check_ocsp_status(
&sign1_no_staple,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log5,
&context5,
);
assert!(result5.is_ok());
assert!(counter5.load(Ordering::SeqCst) > 0);
assert!(log5.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn check_ocsp_status_fetch_override_async() {
use std::{
io::{Cursor, Read},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use async_trait::async_trait;
use coset::{cbor::value::Value, CoseSign1, Header, Label, ProtectedHeader};
use http::{Request, Response};
use super::{check_ocsp_status_async, OcspFetchPolicy};
use crate::{
context::Context,
crypto::cose::{CertificateTrustPolicy, TrustAnchorType},
http::{AsyncHttpResolver, HttpResolverError},
settings::Settings,
status_tracker::StatusTracker,
validation_status::{SIGNING_CREDENTIAL_OCSP_INACCESSIBLE, SIGNING_CREDENTIAL_REVOKED},
};
let full_chain = cert_chain_pem_to_der(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"
))
.unwrap();
let revoked =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der").to_vec();
let mut unprotected = Header::default();
unprotected.rest.push((
Label::Text("x5chain".to_string()),
Value::Array(full_chain.into_iter().map(Value::Bytes).collect()),
));
unprotected.rest.push((
Label::Text("rVals".to_string()),
Value::Map(vec![(
Value::Text("ocspVals".to_string()),
Value::Array(vec![Value::Bytes(revoked)]),
)]),
));
let sign1_revoked = CoseSign1 {
protected: ProtectedHeader::default(),
unprotected,
payload: None,
signature: vec![0u8; 8],
};
let mut anchored = CertificateTrustPolicy::new();
anchored
.add_trust_anchors(
include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
anchored
.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem"
))
.unwrap();
#[derive(Clone)]
struct CountingAsyncResolver {
counter: Arc<AtomicUsize>,
status: u16,
body: Vec<u8>,
}
#[async_trait]
impl AsyncHttpResolver for CountingAsyncResolver {
async fn http_resolve_async(
&self,
_request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
self.counter.fetch_add(1, Ordering::SeqCst);
if self.status == 200 {
Ok(Response::builder()
.status(200)
.body(Box::new(Cursor::new(self.body.clone())) as Box<dyn Read>)
.unwrap())
} else {
Ok(Response::builder()
.status(self.status)
.body(Box::new(std::io::empty()) as Box<dyn Read>)
.unwrap())
}
}
}
#[cfg(feature = "openssl")]
{
let mut anchored = anchored.clone();
let live_good = trusted_fresh_good_response(&mut anchored);
let counter1 = Arc::new(AtomicUsize::new(0));
let resolver1 = CountingAsyncResolver {
counter: counter1.clone(),
status: 200,
body: live_good,
};
let settings1 = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch_should_override": true}}"#)
.unwrap();
let context1 = Context::new()
.with_settings(settings1)
.unwrap()
.with_resolver_async(resolver1);
let mut log1 = StatusTracker::default();
let result1 = check_ocsp_status_async(
&sign1_revoked,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log1,
&context1,
)
.await;
assert!(result1.is_ok());
assert!(counter1.load(Ordering::SeqCst) > 0);
assert!(log1.has_status(crate::validation_status::SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(!log1.has_status(SIGNING_CREDENTIAL_REVOKED));
}
let counter2 = Arc::new(AtomicUsize::new(0));
let resolver2 = CountingAsyncResolver {
counter: counter2.clone(),
status: 404,
body: vec![],
};
let settings2 = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch_should_override": true}}"#)
.unwrap();
let context2 = Context::new()
.with_settings(settings2)
.unwrap()
.with_resolver_async(resolver2);
let mut log2 = StatusTracker::default();
let result2 = check_ocsp_status_async(
&sign1_revoked,
b"data",
OcspFetchPolicy::FetchAllowed,
&anchored,
None,
None,
&mut log2,
&context2,
)
.await;
assert!(result2.is_err());
assert!(counter2.load(Ordering::SeqCst) > 0);
assert!(log2.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log2.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
}
#[test]
fn reader_ocsp_fetch_should_override_implies_ocsp_fetch_and_falls_back() {
use std::{
io::{Cursor, Read},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use coset::{cbor::value::Value, CoseSign1, Header, Label, ProtectedHeader};
use http::{Request, Response};
use crate::{
context::Context,
crypto::cose::{CertificateTrustPolicy, TrustAnchorType},
http::{HttpResolverError, SyncHttpResolver},
settings::Settings,
status_tracker::StatusTracker,
validation_status::{SIGNING_CREDENTIAL_NOT_REVOKED, SIGNING_CREDENTIAL_REVOKED},
Reader, ValidationState,
};
#[derive(Clone)]
struct CountingSyncResolver {
counter: Arc<AtomicUsize>,
status: u16,
body: Vec<u8>,
}
impl SyncHttpResolver for CountingSyncResolver {
fn http_resolve(
&self,
_request: Request<Vec<u8>>,
) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
self.counter.fetch_add(1, Ordering::SeqCst);
if self.status == 200 {
Ok(Response::builder()
.status(200)
.body(Box::new(Cursor::new(self.body.clone())) as Box<dyn Read>)
.unwrap())
} else {
Ok(Response::builder()
.status(self.status)
.body(Box::new(std::io::empty()) as Box<dyn Read>)
.unwrap())
}
}
}
let full_chain = cert_chain_pem_to_der(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"
))
.unwrap();
let good = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der").to_vec();
let revoked =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der").to_vec();
let mut unprotected = Header::default();
unprotected.rest.push((
Label::Text("x5chain".to_string()),
Value::Array(full_chain.into_iter().map(Value::Bytes).collect()),
));
unprotected.rest.push((
Label::Text("rVals".to_string()),
Value::Map(vec![(
Value::Text("ocspVals".to_string()),
Value::Array(vec![Value::Bytes(good)]),
)]),
));
let sign1_with_good_staple = CoseSign1 {
protected: ProtectedHeader::default(),
unprotected,
payload: None,
signature: vec![0u8; 8],
};
let mut anchored = CertificateTrustPolicy::new();
anchored
.add_trust_anchors(
include_bytes!("../../../tests/fixtures/crypto/ocsp/ocsp_chain.pem"),
"https://c2pa-rs/test",
TrustAnchorType::Manifest,
None,
)
.unwrap();
anchored
.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem"
))
.unwrap();
let counter_a = Arc::new(AtomicUsize::new(0));
let resolver_a = CountingSyncResolver {
counter: counter_a.clone(),
status: 200,
body: revoked,
};
let settings_a = Settings::new()
.with_json(r#"{"verify": {"ocsp_fetch": false, "ocsp_fetch_should_override": true}}"#)
.unwrap();
let context_a = Context::new()
.with_settings(settings_a)
.unwrap()
.with_resolver(resolver_a);
let mut log_a = StatusTracker::default();
let _ = crate::cose_validator::check_ocsp_status(
&sign1_with_good_staple,
b"data",
&anchored,
None,
None,
&mut log_a,
&context_a,
);
assert!(counter_a.load(Ordering::SeqCst) > 0);
assert!(log_a.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log_a.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
let test_settings = include_str!("../../../tests/fixtures/test_settings.toml");
let ocsp_jpg = include_bytes!("../../../tests/fixtures/ocsp.jpg");
let counter_b = Arc::new(AtomicUsize::new(0));
let resolver_b = CountingSyncResolver {
counter: counter_b.clone(),
status: 404,
body: vec![],
};
let settings_b = Settings::new()
.with_toml(test_settings)
.unwrap()
.with_json(r#"{"verify": {"verify_trust": false, "ocsp_fetch": false, "ocsp_fetch_should_override": true}}"#)
.unwrap();
let context_b = Context::new()
.with_settings(settings_b)
.unwrap()
.with_resolver(resolver_b);
let reader = Reader::from_context(context_b)
.with_stream("image/jpeg", Cursor::new(ocsp_jpg))
.unwrap();
assert!(counter_b.load(Ordering::SeqCst) > 0);
assert_eq!(reader.validation_state(), ValidationState::Valid);
}
fn staple(sign1: &mut CoseSign1, value: Value) {
sign1
.unprotected
.rest
.push((Label::Text("rVals".into()), value));
}
fn rvals(responses: Vec<Value>) -> Value {
Value::Map(vec![(
Value::Text("ocspVals".into()),
Value::Array(responses),
)])
}
fn trusted_responder() -> CertificateTrustPolicy {
let mut policy = CertificateTrustPolicy::new();
policy
.add_end_entity_credentials(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/ocsp_responder.pem"
))
.unwrap();
policy
}
#[test]
fn get_ocsp_der_requires_rvals_map_array_and_first_byte_string() {
assert!(get_ocsp_der(&CoseSign1::default()).is_none());
for value in [
Value::Null,
Value::Map(vec![(Value::Text("other".into()), Value::Array(vec![]))]),
Value::Map(vec![(
Value::Text("ocspVals".into()),
Value::Bytes(vec![1]),
)]),
rvals(vec![]),
rvals(vec![Value::Null, Value::Bytes(vec![1])]),
] {
let mut sign1 = CoseSign1::default();
staple(&mut sign1, value);
assert!(get_ocsp_der(&sign1).is_none());
}
let mut sign1 = CoseSign1::default();
sign1.unprotected.rest.push((Label::Int(42), Value::Null));
staple(
&mut sign1,
Value::Map(vec![
(Value::Text("other".into()), Value::Null),
(
Value::Text("ocspVals".into()),
Value::Array(vec![Value::Bytes(vec![1, 2]), Value::Bytes(vec![3])]),
),
]),
);
assert_eq!(get_ocsp_der(&sign1), Some(vec![1, 2]));
}
#[test]
fn chain_extension_leaves_incomplete_malformed_and_already_complete_paths_unchanged() {
let chain = leaf_and_intermediate();
for (responder, signer) in [
(vec![], chain.clone()),
(chain.clone(), chain.clone()),
(chain[..1].to_vec(), vec![]),
(chain[..1].to_vec(), chain[..1].to_vec()),
(vec![vec![0]], chain.clone()),
(chain[..1].to_vec(), vec![chain[0].clone(), vec![0]]),
] {
assert_eq!(extend_ocsp_cert_chain(&responder, &signer), responder);
}
}
struct RecordingResolver {
calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl crate::http::SyncHttpResolver for RecordingResolver {
fn http_resolve(
&self,
_: http::Request<Vec<u8>>,
) -> Result<http::Response<Box<dyn std::io::Read>>, crate::http::HttpResolverError>
{
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(http::Response::builder()
.status(404)
.body(Box::new(std::io::empty()) as Box<dyn std::io::Read>)
.unwrap())
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl crate::http::AsyncHttpResolver for RecordingResolver {
async fn http_resolve_async(
&self,
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Box<dyn std::io::Read>>, crate::http::HttpResolverError>
{
crate::http::SyncHttpResolver::http_resolve(self, request)
}
}
#[derive(Clone, Copy, Debug)]
enum StatusCase {
Missing,
Empty,
InvalidStaple,
GoodStaple,
StaleGoodStapleFetches,
StaleStapleUsesSupplied,
RevokedStaple,
SuppliedGood,
SuppliedRevoked,
SuppliedInvalid,
OverrideGood,
OverrideRevoked,
OverrideEmpty,
OverrideMissing,
StapleWins,
InvalidStapleUsesSupplied,
FetchIgnoresSupplied,
InvalidStapleFetches,
UntrustedStapleFetches,
}
struct GoodVariant {
fresh: bool,
good: Vec<u8>,
ctp: CertificateTrustPolicy,
}
fn good_variants() -> Vec<GoodVariant> {
#[allow(unused_mut)]
let mut variants = vec![GoodVariant {
fresh: false,
good: STALE_GOOD.to_vec(),
ctp: trusted_responder(),
}];
#[cfg(feature = "openssl")]
{
let mut ctp = trusted_responder();
let good = trusted_fresh_good_response(&mut ctp);
variants.push(GoodVariant {
fresh: true,
good,
ctp,
});
}
variants
}
const STALE_GOOD: &[u8] =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der");
fn status_case(
case: StatusCase,
good: &[u8],
) -> (CoseSign1, Vec<Vec<u8>>, bool, OcspFetchPolicy, bool) {
let good = good.to_vec();
let stale = STALE_GOOD.to_vec();
let revoked =
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_revoked.der").to_vec();
let mut sign1 = sign1_with_x5chain(&ocsp_signing_chain());
let mut supplied = vec![];
let mut override_status = false;
let mut fetch = OcspFetchPolicy::DoNotFetch;
let mut trusted = true;
match case {
StatusCase::Missing | StatusCase::Empty => {}
StatusCase::InvalidStaple => staple(&mut sign1, rvals(vec![Value::Bytes(vec![0])])),
StatusCase::GoodStaple => staple(&mut sign1, rvals(vec![Value::Bytes(good)])),
StatusCase::StaleGoodStapleFetches => {
fetch = OcspFetchPolicy::FetchAllowed;
staple(&mut sign1, rvals(vec![Value::Bytes(stale)]));
}
StatusCase::StaleStapleUsesSupplied => {
staple(&mut sign1, rvals(vec![Value::Bytes(stale)]));
supplied = vec![good];
}
StatusCase::RevokedStaple => staple(&mut sign1, rvals(vec![Value::Bytes(revoked)])),
StatusCase::SuppliedGood => supplied = vec![vec![0], good],
StatusCase::SuppliedRevoked => supplied = vec![vec![0], revoked],
StatusCase::SuppliedInvalid => supplied = vec![vec![0]],
StatusCase::OverrideGood => {
override_status = true;
staple(&mut sign1, rvals(vec![Value::Bytes(revoked)]));
supplied = vec![good];
}
StatusCase::OverrideRevoked => {
override_status = true;
staple(&mut sign1, rvals(vec![Value::Bytes(good)]));
supplied = vec![revoked];
}
StatusCase::OverrideEmpty | StatusCase::OverrideMissing => override_status = true,
StatusCase::StapleWins => {
staple(&mut sign1, rvals(vec![Value::Bytes(good)]));
supplied = vec![revoked];
}
StatusCase::InvalidStapleUsesSupplied => {
staple(&mut sign1, rvals(vec![Value::Bytes(vec![0])]));
supplied = vec![good];
}
StatusCase::FetchIgnoresSupplied => {
fetch = OcspFetchPolicy::FetchAllowed;
supplied = vec![good];
}
StatusCase::InvalidStapleFetches => {
fetch = OcspFetchPolicy::FetchAllowed;
staple(&mut sign1, rvals(vec![Value::Bytes(vec![0])]));
}
StatusCase::UntrustedStapleFetches => {
fetch = OcspFetchPolicy::FetchAllowed;
trusted = false;
staple(&mut sign1, rvals(vec![Value::Bytes(good)]));
}
}
(sign1, supplied, override_status, fetch, trusted)
}
fn assert_status_case(
case: StatusCase,
fresh: bool,
result: Result<OcspResponse, CoseError>,
log: &StatusTracker,
calls: usize,
) {
let revoked = matches!(
case,
StatusCase::RevokedStaple | StatusCase::SuppliedRevoked | StatusCase::OverrideRevoked
) || (!fresh && matches!(case, StatusCase::StapleWins));
let good = fresh
&& matches!(
case,
StatusCase::GoodStaple
| StatusCase::StaleStapleUsesSupplied
| StatusCase::SuppliedGood
| StatusCase::OverrideGood
| StatusCase::StapleWins
| StatusCase::InvalidStapleUsesSupplied
);
let skipped = matches!(
case,
StatusCase::Missing
| StatusCase::Empty
| StatusCase::InvalidStaple
| StatusCase::OverrideEmpty
| StatusCase::OverrideMissing
) || (!fresh
&& matches!(
case,
StatusCase::GoodStaple
| StatusCase::StaleStapleUsesSupplied
| StatusCase::InvalidStapleUsesSupplied
));
let fetched = matches!(
case,
StatusCase::FetchIgnoresSupplied
| StatusCase::InvalidStapleFetches
| StatusCase::UntrustedStapleFetches
| StatusCase::StaleGoodStapleFetches
);
let ctx = format!("{case:?} (fresh: {fresh})");
assert_eq!(result.is_err(), revoked, "{ctx}");
assert_eq!(log.has_status(SIGNING_CREDENTIAL_REVOKED), revoked, "{ctx}");
assert_eq!(
log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED),
good,
"{ctx}"
);
assert_eq!(
log.has_status(SIGNING_CREDENTIAL_OCSP_SKIPPED),
skipped,
"{ctx}"
);
assert_eq!(
log.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE),
fetched,
"{ctx}"
);
assert_eq!(calls > 0, fetched, "{ctx}");
if let Ok(response) = result {
assert_eq!(!response.ocsp_der.is_empty(), good, "{ctx}");
}
}
const STATUS_CASES: [StatusCase; 19] = [
StatusCase::Missing,
StatusCase::Empty,
StatusCase::InvalidStaple,
StatusCase::GoodStaple,
StatusCase::StaleGoodStapleFetches,
StatusCase::StaleStapleUsesSupplied,
StatusCase::RevokedStaple,
StatusCase::SuppliedGood,
StatusCase::SuppliedRevoked,
StatusCase::SuppliedInvalid,
StatusCase::OverrideGood,
StatusCase::OverrideRevoked,
StatusCase::OverrideEmpty,
StatusCase::OverrideMissing,
StatusCase::StapleWins,
StatusCase::InvalidStapleUsesSupplied,
StatusCase::FetchIgnoresSupplied,
StatusCase::InvalidStapleFetches,
StatusCase::UntrustedStapleFetches,
];
#[test]
fn check_status_policy_matrix() {
for variant in good_variants() {
for case in STATUS_CASES {
let (sign1, supplied, override_status, fetch, trusted) =
status_case(case, &variant.good);
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let context = Context::new()
.with_resolver(RecordingResolver {
calls: calls.clone(),
})
.with_settings_value(
"builder.certificate_status_should_override",
override_status,
)
.unwrap();
let untrusted = CertificateTrustPolicy::new();
let ctp = if trusted { &variant.ctp } else { &untrusted };
let mut log = StatusTracker::default();
let result = check_ocsp_status(
&sign1,
b"payload",
fetch,
ctp,
if matches!(case, StatusCase::Missing | StatusCase::OverrideMissing) {
None
} else {
Some(&supplied)
},
None,
&mut log,
&context,
);
assert_status_case(
case,
variant.fresh,
result,
&log,
calls.load(std::sync::atomic::Ordering::SeqCst),
);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn check_status_policy_matrix_async() {
for variant in good_variants() {
for case in STATUS_CASES {
let (sign1, supplied, override_status, fetch, trusted) =
status_case(case, &variant.good);
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let context = Context::new()
.with_resolver_async(RecordingResolver {
calls: calls.clone(),
})
.with_settings_value(
"builder.certificate_status_should_override",
override_status,
)
.unwrap();
let untrusted = CertificateTrustPolicy::new();
let ctp = if trusted { &variant.ctp } else { &untrusted };
let mut log = StatusTracker::default();
let result = check_ocsp_status_async(
&sign1,
b"payload",
fetch,
ctp,
if matches!(case, StatusCase::Missing | StatusCase::OverrideMissing) {
None
} else {
Some(&supplied)
},
None,
&mut log,
&context,
)
.await;
assert_status_case(
case,
variant.fresh,
result,
&log,
calls.load(std::sync::atomic::Ordering::SeqCst),
);
}
}
}
#[cfg(feature = "openssl")]
fn fresh_basic_response() -> rasn_ocsp::BasicOcspResponse {
use crate::crypto::ocsp::test_responder::good_basic_response;
let now = DateTime::from_timestamp(Utc::now().timestamp(), 0).unwrap();
let mut basic = good_basic_response();
basic.tbs_response_data.produced_at = (now - chrono::Duration::hours(1)).fixed_offset();
let single = &mut basic.tbs_response_data.responses[0];
single.this_update = (now - chrono::Duration::hours(1)).fixed_offset();
single.next_update = Some((now + chrono::Duration::hours(24)).fixed_offset());
basic
}
#[cfg(feature = "openssl")]
fn trusted_fresh_good_response(ctp: &mut CertificateTrustPolicy) -> Vec<u8> {
use crate::crypto::ocsp::test_responder::TestResponder;
let responder = TestResponder::new();
ctp.add_end_entity_credentials(&responder.cert_pem())
.unwrap();
responder.sign(fresh_basic_response())
}
#[cfg(feature = "openssl")]
fn stale_and_fresh_good_responses() -> (CertificateTrustPolicy, Vec<u8>, Vec<u8>) {
use crate::crypto::ocsp::test_responder::{good_basic_response, TestResponder};
let responder = TestResponder::new();
let mut policy = CertificateTrustPolicy::new();
policy
.add_end_entity_credentials(&responder.cert_pem())
.unwrap();
let stale = responder.sign(good_basic_response());
(policy, stale, responder.sign(fresh_basic_response()))
}
#[cfg(feature = "openssl")]
#[test]
fn out_of_window_response_with_timestamp_is_inconclusive() {
use crate::crypto::ocsp::test_responder::{good_basic_response, TestResponder};
let timestamp = supplied_timestamp();
let attested: DateTime<Utc> = timestamp.gen_time.clone().into();
let responder = TestResponder::new();
let mut ctp = CertificateTrustPolicy::new();
ctp.add_end_entity_credentials(&responder.cert_pem())
.unwrap();
let response_for = |this_update_hours: i64, next_update_hours: i64| {
let this_update = attested + chrono::Duration::hours(this_update_hours);
let mut basic = good_basic_response();
basic.tbs_response_data.produced_at = this_update.fixed_offset();
let single = &mut basic.tbs_response_data.responses[0];
single.this_update = this_update.fixed_offset();
single.next_update =
Some((attested + chrono::Duration::hours(next_update_hours)).fixed_offset());
responder.sign(basic)
};
let expired = response_for(-48, -24);
let valid = response_for(-1, 24);
let supplied = vec![expired.clone(), valid.clone()];
let mut log = StatusTracker::default();
let response = check_ocsp_status(
&sign1_with_x5chain(&ocsp_signing_chain()),
b"payload",
OcspFetchPolicy::DoNotFetch,
&ctp,
Some(&supplied),
Some(×tamp),
&mut log,
&Context::new(),
)
.unwrap();
assert_eq!(response.ocsp_der, valid);
assert!(log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
let mut sign1 = sign1_with_x5chain(&ocsp_signing_chain());
staple(&mut sign1, rvals(vec![Value::Bytes(expired)]));
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let context = Context::new().with_resolver(RecordingResolver {
calls: calls.clone(),
});
let mut log = StatusTracker::default();
check_ocsp_status(
&sign1,
b"payload",
OcspFetchPolicy::FetchAllowed,
&ctp,
None,
Some(×tamp),
&mut log,
&context,
)
.unwrap();
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(log.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[cfg(feature = "openssl")]
#[test]
fn stale_good_response_does_not_hide_fresh_good_response() {
let (ctp, stale, fresh) = stale_and_fresh_good_responses();
let sign1 = sign1_with_x5chain(&ocsp_signing_chain());
for override_status in [false, true] {
for supplied in [
vec![stale.clone(), fresh.clone()],
vec![fresh.clone(), stale.clone()],
] {
let context = Context::new()
.with_settings_value(
"builder.certificate_status_should_override",
override_status,
)
.unwrap();
let mut log = StatusTracker::default();
let response = check_ocsp_status(
&sign1,
b"payload",
OcspFetchPolicy::DoNotFetch,
&ctp,
Some(&supplied),
None,
&mut log,
&context,
)
.unwrap();
assert_eq!(response.ocsp_der, fresh);
assert!(log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
}
}
}
#[cfg(all(feature = "openssl", not(target_arch = "wasm32")))]
#[tokio::test]
async fn stale_good_response_does_not_hide_fresh_good_response_async() {
let (ctp, stale, fresh) = stale_and_fresh_good_responses();
let sign1 = sign1_with_x5chain(&ocsp_signing_chain());
let supplied = vec![stale, fresh.clone()];
let context = Context::new()
.with_settings_value("builder.certificate_status_should_override", true)
.unwrap();
let mut log = StatusTracker::default();
let response = check_ocsp_status_async(
&sign1,
b"payload",
OcspFetchPolicy::DoNotFetch,
&ctp,
Some(&supplied),
None,
&mut log,
&context,
)
.await
.unwrap();
assert_eq!(response.ocsp_der, fresh);
assert!(log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
}
fn supplied_timestamp() -> TstInfo {
use crate::crypto::asn1::{rfc3161::MessageImprint, AlgorithmIdentifier, GeneralizedTime};
TstInfo {
version: bcder::Integer::from(1u8),
policy: "1.2.3.4".parse().unwrap(),
message_imprint: MessageImprint {
hash_algorithm: AlgorithmIdentifier {
algorithm: "2.16.840.1.101.3.4.2.1".parse().unwrap(),
},
hashed_message: bcder::OctetString::new(bytes::Bytes::from_static(&[0; 32])),
},
serial_number: bcder::Integer::from(1u8),
gen_time: GeneralizedTime::from_der_bytes(b"\x18\x0f20250307150000Z").unwrap(),
accuracy: None,
ordering: None,
nonce: None,
tsa: None,
extensions: None,
}
}
#[test]
fn supplied_timestamp_bypasses_missing_cose_timestamp_for_stapled_and_fetched_paths() {
let sign1 = sign1_with_x5chain(&ocsp_signing_chain());
let timestamp = supplied_timestamp();
let ctp = trusted_responder();
let context = Context::new();
let good = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der");
let mut log = StatusTracker::default();
let response = check_stapled_ocsp_response(
&sign1,
good,
b"payload",
&ctp,
Some(×tamp),
&mut log,
context.settings(),
)
.unwrap();
assert!(!response.ocsp_der.is_empty());
assert!(log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let context = context.with_resolver(RecordingResolver {
calls: calls.clone(),
});
let mut log = StatusTracker::default();
fetch_and_check_ocsp_response(
&sign1,
b"payload",
&ctp,
Some(×tamp),
&mut log,
&context,
)
.unwrap();
assert!(log.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn supplied_timestamp_async_and_bad_signing_chain() {
let sign1 = sign1_with_x5chain(&ocsp_signing_chain());
let timestamp = supplied_timestamp();
let ctp = trusted_responder();
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let context = Context::new().with_resolver_async(RecordingResolver {
calls: calls.clone(),
});
let good = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der");
let mut log = StatusTracker::default();
let response = check_stapled_ocsp_response_async(
&sign1,
good,
b"payload",
&ctp,
Some(×tamp),
&mut log,
context.settings(),
)
.await
.unwrap();
assert!(!response.ocsp_der.is_empty());
assert!(log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
let mut log = StatusTracker::default();
fetch_and_check_ocsp_response_async(
&sign1,
b"payload",
&ctp,
Some(×tamp),
&mut log,
&context,
)
.await
.unwrap();
assert!(log.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
let mut log = StatusTracker::default();
assert!(check_stapled_ocsp_response_async(
&CoseSign1::default(),
good,
b"payload",
&ctp,
Some(×tamp),
&mut log,
context.settings(),
)
.await
.is_err());
assert!(fetch_and_check_ocsp_response_async(
&CoseSign1::default(),
b"payload",
&ctp,
Some(×tamp),
&mut log,
&context,
)
.await
.is_err());
assert!(log.logged_items().is_empty());
}
#[test]
fn bad_signing_chain_errors_are_discarded_only_by_response_list_processing() {
let sign1 = CoseSign1::default();
let ctp = trusted_responder();
let context = Context::new();
let good = include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der");
let mut log = StatusTracker::default();
assert!(check_stapled_ocsp_response(
&sign1,
good,
b"payload",
&ctp,
None,
&mut log,
context.settings(),
)
.is_err());
assert!(
fetch_and_check_ocsp_response(&sign1, b"payload", &ctp, None, &mut log, &context,)
.is_err()
);
let response = process_ocsp_responses(
&sign1,
b"payload",
&ctp,
&[good.to_vec()],
None,
&mut log,
context.settings(),
)
.unwrap();
assert!(response.ocsp_der.is_empty());
assert!(log.logged_items().is_empty());
}
fn response_without_responder_extensions() -> Vec<u8> {
let mut outer: rasn_ocsp::OcspResponse = rasn::der::decode(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/response_good.der"
))
.unwrap();
let bytes = outer.bytes.as_mut().unwrap();
let mut basic: rasn_ocsp::BasicOcspResponse = rasn::der::decode(&bytes.response).unwrap();
basic.certs.as_mut().unwrap()[0].tbs_certificate.extensions = None;
bytes.response = rasn::der::encode(&basic).unwrap().into();
rasn::der::encode(&outer).unwrap()
}
#[test]
fn invalid_responder_profile_and_untrusted_responder_do_not_assert_good_status() {
let sign1 = sign1_with_x5chain(&ocsp_signing_chain());
let der = response_without_responder_extensions();
let context = Context::new();
let ctp = trusted_responder();
let mut log = StatusTracker::default();
let response = check_stapled_ocsp_response(
&sign1,
&der,
b"payload",
&ctp,
None,
&mut log,
context.settings(),
)
.unwrap();
assert!(response.ocsp_der.is_empty());
assert!(!log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(log.has_any_error());
for (der, ctp) in [
(der.as_slice(), trusted_responder()),
(
include_bytes!("../../../tests/fixtures/crypto/ocsp/response_good.der").as_slice(),
CertificateTrustPolicy::new(),
),
] {
let mut log = StatusTracker::default();
let response = validate_fetched_ocsp(der, &ocsp_signing_chain(), &ctp, None, &mut log);
assert!(response.ocsp_der.is_empty());
assert!(log.logged_items().is_empty());
}
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn invalid_stapled_responder_profile_async() {
let mut log = StatusTracker::default();
let response = check_stapled_ocsp_response_async(
&sign1_with_x5chain(&ocsp_signing_chain()),
&response_without_responder_extensions(),
b"payload",
&trusted_responder(),
None,
&mut log,
Context::new().settings(),
)
.await
.unwrap();
assert!(response.ocsp_der.is_empty());
assert!(!log.has_status(SIGNING_CREDENTIAL_NOT_REVOKED));
assert!(log.has_any_error());
}
struct RevokedCaResolver(std::sync::Arc<std::sync::atomic::AtomicUsize>);
impl crate::http::SyncHttpResolver for RevokedCaResolver {
fn http_resolve(
&self,
_: http::Request<Vec<u8>>,
) -> Result<http::Response<Box<dyn std::io::Read>>, crate::http::HttpResolverError>
{
self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(http::Response::builder()
.status(200)
.body(Box::new(std::io::Cursor::new(include_bytes!(
"../../../tests/fixtures/crypto/ocsp/response_revoked.der"
))) as Box<dyn std::io::Read>)
.unwrap())
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl crate::http::AsyncHttpResolver for RevokedCaResolver {
async fn http_resolve_async(
&self,
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Box<dyn std::io::Read>>, crate::http::HttpResolverError>
{
crate::http::SyncHttpResolver::http_resolve(self, request)
}
}
fn chain_with_revoked_issuer_slot() -> CoseSign1 {
let chain = ocsp_signing_chain();
sign1_with_x5chain(&[chain[0].clone(), chain[0].clone(), chain[1].clone()])
}
#[test]
fn revoked_ca_stops_before_leaf_fetch_and_reports_untrusted_not_leaf_revoked() {
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let context = Context::new().with_resolver(RevokedCaResolver(calls.clone()));
let mut log = StatusTracker::default();
let result = fetch_and_check_ocsp_response(
&chain_with_revoked_issuer_slot(),
b"payload",
&trusted_responder(),
Some(&supplied_timestamp()),
&mut log,
&context,
);
assert!(matches!(
result,
Err(CoseError::CertificateTrustError(
CertificateTrustError::CertificateNotTrusted
))
));
assert!(log.has_status(SIGNING_CREDENTIAL_UNTRUSTED));
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
assert!(!log.has_status(SIGNING_CREDENTIAL_OCSP_INACCESSIBLE));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn revoked_ca_stops_before_leaf_fetch_async() {
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let context = Context::new().with_resolver_async(RevokedCaResolver(calls.clone()));
let mut log = StatusTracker::default();
let result = fetch_and_check_ocsp_response_async(
&chain_with_revoked_issuer_slot(),
b"payload",
&trusted_responder(),
Some(&supplied_timestamp()),
&mut log,
&context,
)
.await;
assert!(matches!(
result,
Err(CoseError::CertificateTrustError(
CertificateTrustError::CertificateNotTrusted
))
));
assert!(log.has_status(SIGNING_CREDENTIAL_UNTRUSTED));
assert!(!log.has_status(SIGNING_CREDENTIAL_REVOKED));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
}
}