use const_oid::ObjectIdentifier;
use x509_cert::attr::Attributes;
use super::signed_data_from_der;
const OID_REVOCATION_INFO_ARCHIVAL: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113583.1.1.8");
const OID_REVOCATION_REFS: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.22");
const OID_REVOCATION_VALUES: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.24");
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LtvStatus {
pub has_crl: bool,
pub has_ocsp: bool,
pub has_revocation_archival: bool,
pub material: RevocationMaterial,
pub details: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevocationMaterial {
None,
Unauthenticated,
Signed,
}
impl LtvStatus {
#[must_use]
pub fn ltv_enabled(&self) -> bool {
self.material == RevocationMaterial::Signed
}
#[must_use]
pub fn has_unauthenticated_material(&self) -> bool {
self.material == RevocationMaterial::Unauthenticated
}
}
fn revocation_label(oid: &ObjectIdentifier) -> Option<&'static str> {
match *oid {
OID_REVOCATION_INFO_ARCHIVAL => Some("Adobe RevocationInfoArchival"),
OID_REVOCATION_REFS => Some("CAdES revocation references"),
OID_REVOCATION_VALUES => Some("CAdES revocation values"),
_ => None,
}
}
fn scan_revocation_attrs(
attrs: &Attributes,
kind: &str,
note: &str,
details: &mut Vec<String>,
) -> bool {
let mut has_archival = false;
for attr in attrs.iter() {
if let Some(label) = revocation_label(&attr.oid) {
details.push(format!("{kind} attribute: {label}{note}"));
if attr.oid == OID_REVOCATION_INFO_ARCHIVAL {
has_archival = true;
}
}
}
has_archival
}
#[must_use]
pub fn check_ltv_status(cms_der: &[u8]) -> LtvStatus {
let mut details = Vec::new();
let Ok(signed_data) = signed_data_from_der(cms_der) else {
details.push("Cannot parse CMS structure for LTV check".to_owned());
return LtvStatus {
has_crl: false,
has_ocsp: false,
has_revocation_archival: false,
material: RevocationMaterial::None,
details,
};
};
let mut has_crl = false;
let has_ocsp = false;
let mut has_revocation_archival = false;
let mut material = RevocationMaterial::None;
if let Some(crls) = signed_data.crls.as_ref() {
let count = crls.0.len();
if count > 0 {
has_crl = true;
material = RevocationMaterial::Unauthenticated;
details.push(format!(
"Embedded CRLs: {count} (outside the signature, not counted)"
));
}
}
if let Some(signer_info) = signed_data.signer_infos.0.iter().next() {
if let Some(attrs) = signer_info.signed_attrs.as_ref() {
if scan_revocation_attrs(attrs, "Signed", "", &mut details) {
has_revocation_archival = true;
material = RevocationMaterial::Signed;
}
}
if let Some(attrs) = signer_info.unsigned_attrs.as_ref() {
if scan_revocation_attrs(
attrs,
"Unsigned",
" (outside the signature, not counted)",
&mut details,
) {
has_revocation_archival = true;
if material == RevocationMaterial::None {
material = RevocationMaterial::Unauthenticated;
}
}
}
}
if material != RevocationMaterial::Signed {
details.push("No signed revocation data (CRL/OCSP)".to_owned());
}
LtvStatus {
has_crl,
has_ocsp,
has_revocation_archival,
material,
details,
}
}
#[cfg(test)]
mod tests {
use super::*;
const CMS_PLAIN: &[u8] = include_bytes!("../pki/testdata/cms_leaf_direct.der");
const CMS_WITH_CRL: &[u8] = include_bytes!("../pki/testdata/cms_with_crl.der");
const CMS_WITH_ARCHIVAL: &[u8] = include_bytes!("../pki/testdata/cms_with_archival.der");
const CMS_UNSIGNED_ARCHIVAL: &[u8] =
include_bytes!("../pki/testdata/cms_with_unsigned_archival.der");
#[test]
fn plain_signature_is_not_ltv() {
let status = check_ltv_status(CMS_PLAIN);
assert!(!status.ltv_enabled());
assert!(!status.has_crl);
assert!(!status.has_ocsp);
assert!(status
.details
.iter()
.any(|d| d.contains("No signed revocation data")));
}
#[test]
fn embedded_crls_are_reported_but_not_counted() {
let status = check_ltv_status(CMS_WITH_CRL);
assert!(status.has_crl);
assert!(!status.ltv_enabled(), "unsigned material must not count");
assert!(status.has_unauthenticated_material());
assert!(status
.details
.iter()
.any(|d| d.contains("outside the signature")));
}
#[test]
fn an_unsigned_archival_attribute_is_not_evidence() {
let status = check_ltv_status(CMS_UNSIGNED_ARCHIVAL);
assert!(!status.ltv_enabled(), "unsigned material must not count");
assert!(status.has_unauthenticated_material());
assert!(status
.details
.iter()
.any(|d| d.contains("outside the signature")));
}
#[test]
fn detects_revocation_archival_attribute() {
let status = check_ltv_status(CMS_WITH_ARCHIVAL);
assert!(status.has_revocation_archival);
assert!(status.ltv_enabled());
assert!(!status.has_ocsp);
assert!(status
.details
.iter()
.any(|d| d.contains("Adobe RevocationInfoArchival")));
}
#[test]
fn unparsable_blob_reports_cannot_parse() {
let status = check_ltv_status(b"garbage");
assert!(!status.ltv_enabled());
assert!(status
.details
.iter()
.any(|d| d.contains("Cannot parse CMS structure")));
}
}