#![cfg(feature = "signatures")]
use crate::error::{Error, Result};
use cms::cert::x509::ext::pkix::name::GeneralName;
use cms::signed_data::SignedData;
use der::{Decode, Encode};
use x509_tsp::TstInfo;
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashAlgorithm {
Sha1 = 1,
Sha256 = 2,
Sha384 = 3,
Sha512 = 4,
Unknown = 0,
}
#[derive(Debug)]
pub struct Timestamp {
token_bytes: Vec<u8>,
tst: TstInfo,
}
impl Timestamp {
pub fn from_der(token: &[u8]) -> Result<Self> {
let token_bytes = token.to_vec();
if let Some(tst) = decode_cms_wrapped(token) {
return Ok(Self { token_bytes, tst });
}
let tst = TstInfo::from_der(token).map_err(|e| {
Error::InvalidPdf(format!("not a valid TimeStampToken or TSTInfo: {e}"))
})?;
Ok(Self { token_bytes, tst })
}
pub fn token_bytes(&self) -> &[u8] {
&self.token_bytes
}
pub fn time(&self) -> i64 {
self.tst.gen_time.to_unix_duration().as_secs() as i64
}
pub fn serial(&self) -> String {
hex_upper(self.tst.serial_number.as_bytes())
}
pub fn policy_oid(&self) -> String {
self.tst.policy.to_string()
}
pub fn tsa_name(&self) -> String {
match &self.tst.tsa {
Some(GeneralName::DirectoryName(dn)) => dn.to_string(),
Some(GeneralName::UniformResourceIdentifier(s)) => s.to_string(),
Some(GeneralName::DnsName(s)) => s.to_string(),
Some(GeneralName::Rfc822Name(s)) => s.to_string(),
_ => String::new(),
}
}
pub fn hash_algorithm(&self) -> HashAlgorithm {
super::crypto::hash_algorithm_from_oid(self.tst.message_imprint.hash_algorithm.oid)
}
pub fn message_imprint(&self) -> Vec<u8> {
self.tst.message_imprint.hashed_message.as_bytes().to_vec()
}
pub fn message_imprint_ref(&self) -> &[u8] {
self.tst.message_imprint.hashed_message.as_bytes()
}
}
fn decode_cms_wrapped(bytes: &[u8]) -> Option<TstInfo> {
let content = cms::content_info::ContentInfo::from_der(bytes).ok()?;
let sd = SignedData::from_der(&content.content.to_der().ok()?).ok()?;
let econtent = sd.encap_content_info.econtent?;
TstInfo::from_der(econtent.value()).ok()
}
fn hex_upper(bytes: &[u8]) -> String {
static HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0F) as usize] as char);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
const TSP_RESPONSE: &[u8] = &hex!(
"3082028403030201003082027B06092A864886F70D010702A082026C30820268020103310F300D06096086480165030402010500"
);
#[test]
fn timestamp_from_bare_tstinfo() {
let bare_tstinfo: &[u8] = &hex!(
"3081B302010106042A0304013031300D060960864801650304020105000420BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD020104180F32303233303630373131323632365A300A020101800201F48101640101FF0208314CFCE4E0651827A048A4463044310B30090603550406130255533113301106035504080C0A536F6D652D5374617465310D300B060355040A0C04546573743111300F06035504030C085465737420545341"
);
let ts = Timestamp::from_der(bare_tstinfo).expect("parse bare TSTInfo");
assert_eq!(ts.time(), 1_686_137_186); assert_eq!(ts.serial(), "04");
assert_eq!(ts.policy_oid(), "1.2.3.4.1");
assert_eq!(ts.hash_algorithm(), HashAlgorithm::Sha256);
assert_eq!(
ts.message_imprint(),
hex!("BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD")
);
assert_eq!(ts.tsa_name(), "CN=Test TSA,O=Test,ST=Some-State,C=US");
assert_eq!(ts.token_bytes(), bare_tstinfo);
}
#[test]
fn timestamp_rejects_garbage() {
let err = Timestamp::from_der(b"not a timestamp").unwrap_err();
assert!(matches!(err, Error::InvalidPdf(_)), "expected InvalidPdf, got {err:?}");
}
#[test]
fn hash_algorithm_variants_match_ffi_enum() {
assert_eq!(HashAlgorithm::Sha1 as i32, 1);
assert_eq!(HashAlgorithm::Sha256 as i32, 2);
assert_eq!(HashAlgorithm::Sha384 as i32, 3);
assert_eq!(HashAlgorithm::Sha512 as i32, 4);
assert_eq!(HashAlgorithm::Unknown as i32, 0);
}
}