use super::{DocumentSecurityStore, PadesLevel};
use crate::crypto::{self, HashAlgorithm};
use crate::signatures::types::SignatureInfo;
use cms::content_info::ContentInfo;
use cms::signed_data::SignedData;
use der::oid::ObjectIdentifier;
use der::{Decode, Encode, SliceReader};
const OID_SIGNATURE_TIME_STAMP: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.14");
fn has_bt_timestamp(cms: &[u8]) -> bool {
let Ok(mut reader) = SliceReader::new(cms) else {
return false;
};
let Ok(ci) = ContentInfo::decode(&mut reader) else {
return false;
};
let Ok(sd_bytes) = ci.content.to_der() else {
return false;
};
let Ok(sd) = SignedData::from_der(&sd_bytes) else {
return false;
};
let Some(signer) = sd.signer_infos.0.iter().next() else {
return false;
};
match signer.unsigned_attrs.as_ref() {
Some(attrs) => attrs.iter().any(|a| a.oid == OID_SIGNATURE_TIME_STAMP),
None => false,
}
}
pub fn vri_key(contents: &[u8]) -> Option<String> {
let mut hasher = crypto::active().hasher(HashAlgorithm::Sha1).ok()?;
hasher.update(contents);
let digest = hasher.finalize();
let mut s = String::with_capacity(digest.len() * 2);
for b in digest {
s.push_str(&format!("{b:02X}"));
}
Some(s)
}
pub fn classify_pades_level(
info: &SignatureInfo,
dss: Option<&DocumentSecurityStore>,
) -> PadesLevel {
let Some(contents) = info.contents.as_deref() else {
return PadesLevel::BB;
};
if !has_bt_timestamp(contents) {
return PadesLevel::BB;
}
if let (Some(dss), Some(key)) = (dss, vri_key(contents)) {
if dss.vri_for(&key).is_some() {
return PadesLevel::BLt;
}
}
PadesLevel::BT
}
pub fn has_document_timestamp(pdf: &[u8]) -> bool {
fn contains(hay: &[u8], needle: &[u8]) -> bool {
needle.len() <= hay.len() && hay.windows(needle.len()).any(|w| w == needle)
}
contains(pdf, b"/DocTimeStamp") && contains(pdf, b"/ETSI.RFC3161")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signatures::pades::VriEntry;
fn sig_with(contents: Option<Vec<u8>>) -> SignatureInfo {
SignatureInfo {
signer_name: None,
signing_time: None,
reason: None,
location: None,
contact_info: None,
sub_filter: None,
covers_whole_document: false,
byte_range: vec![],
certificate_cn: None,
certificate_issuer: None,
valid_from: None,
valid_to: None,
contents,
}
}
#[test]
fn no_contents_is_bb() {
assert_eq!(classify_pades_level(&sig_with(None), None), PadesLevel::BB);
}
#[test]
fn malformed_cms_is_bb_not_a_panic() {
let s = sig_with(Some(b"definitely not a CMS blob".to_vec()));
assert_eq!(classify_pades_level(&s, None), PadesLevel::BB);
let dss = DocumentSecurityStore::default();
assert_eq!(classify_pades_level(&s, Some(&dss)), PadesLevel::BB);
}
#[test]
fn vri_key_is_uppercase_hex_sha1_of_contents() {
let key = vri_key(b"").expect("provider supports SHA-1");
assert_eq!(key, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709");
assert_eq!(vri_key(b"abc").unwrap(), "A9993E364706816ABA3E25717850C26C9CD0D89D");
}
#[test]
fn bt_requires_real_timestamp_attr_not_just_dss() {
let contents = b"\x30\x03\x02\x01\x00".to_vec(); let s = sig_with(Some(contents.clone()));
let mut dss = DocumentSecurityStore::default();
if let Some(k) = vri_key(&contents) {
dss.vri.push(VriEntry {
signature_digest: k,
..VriEntry::default()
});
}
assert_eq!(classify_pades_level(&s, Some(&dss)), PadesLevel::BB);
}
}