use anyhow::{anyhow, Result};
use cms::content_info::ContentInfo;
use cms::signed_data::{SignedData, SignerIdentifier, SignerInfo, SignerInfos};
use der::{Decode, Encode, Reader, SliceReader, Tag};
use crate::cert::extract_rdn;
use x509_cert::attr::Attributes;
pub const OID_MS_TIMESTAMP: &str = "1.3.6.1.4.1.311.3.3.1";
pub const OID_COUNTERSIGNATURE: &str = "1.2.840.113549.1.9.6";
const OID_SIGNING_TIME: &str = "1.2.840.113549.1.9.5";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimestampKind {
Rfc3161,
Countersignature,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TimestampInfo {
pub signed_at_unix: i64,
pub authority: Option<String>,
pub kind: TimestampKind,
}
pub fn from_unsigned_attrs(attrs: Option<&Attributes>) -> Option<TimestampInfo> {
let attrs = attrs?;
for a in attrs.iter() {
let oid = a.oid.to_string();
let value = a.values.get(0)?.to_der().ok()?;
let parsed = if oid == OID_MS_TIMESTAMP {
from_rfc3161_token(&value)
} else if oid == OID_COUNTERSIGNATURE {
from_countersignature(&value)
} else {
continue;
};
if let Ok(info) = parsed {
return Some(info);
}
}
None
}
pub fn from_rfc3161_token(der: &[u8]) -> Result<TimestampInfo> {
let ci = ContentInfo::from_der(der)?;
let sd = SignedData::from_der(&ci.content.to_der()?)?;
let econtent = sd
.encap_content_info
.econtent
.as_ref()
.ok_or_else(|| anyhow!("timestamp token has no encapsulated content"))?;
let signed_at_unix = gen_time_from_tstinfo(econtent.value())?;
Ok(TimestampInfo {
signed_at_unix,
authority: tsa_from_token(&sd),
kind: TimestampKind::Rfc3161,
})
}
pub fn from_countersignature(der: &[u8]) -> Result<TimestampInfo> {
let cs = SignerInfo::from_der(der)?;
let attrs = cs
.signed_attrs
.as_ref()
.ok_or_else(|| anyhow!("countersignature has no signed attributes"))?;
let mut signed_at_unix = None;
for a in attrs.iter() {
if a.oid.to_string() == OID_SIGNING_TIME {
let v = a
.values
.get(0)
.ok_or_else(|| anyhow!("signingTime attribute is empty"))?
.to_der()?;
signed_at_unix = Some(parse_asn1_time(&v)?);
}
}
let signed_at_unix =
signed_at_unix.ok_or_else(|| anyhow!("countersignature carries no signingTime"))?;
Ok(TimestampInfo {
signed_at_unix,
authority: org_from_sid(&cs.sid),
kind: TimestampKind::Countersignature,
})
}
fn gen_time_from_tstinfo(der: &[u8]) -> Result<i64> {
let mut outer = SliceReader::new(der)?;
let seq: der::asn1::AnyRef = der::asn1::AnyRef::decode(&mut outer)?;
let mut r = SliceReader::new(seq.value())?;
while !r.is_finished() {
let header = r.peek_header()?;
let field: der::asn1::AnyRef = der::asn1::AnyRef::decode(&mut r)?;
if header.tag == Tag::GeneralizedTime {
return parse_generalized(field.value());
}
}
Err(anyhow!("TSTInfo has no genTime"))
}
fn tsa_from_token(sd: &SignedData) -> Option<String> {
let sid = &first_signer(&sd.signer_infos)?.sid;
let certs = sd.certificates.as_ref()?;
for choice in certs.0.iter() {
if let cms::cert::CertificateChoices::Certificate(c) = choice {
if let SignerIdentifier::IssuerAndSerialNumber(ias) = sid {
if c.tbs_certificate.issuer == ias.issuer
&& c.tbs_certificate.serial_number == ias.serial_number
{
return extract_rdn(&c.tbs_certificate.subject.to_string(), "O")
.or_else(|| extract_rdn(&c.tbs_certificate.subject.to_string(), "CN"));
}
}
}
}
None
}
fn first_signer(infos: &SignerInfos) -> Option<&SignerInfo> {
infos.0.iter().next()
}
fn org_from_sid(sid: &SignerIdentifier) -> Option<String> {
match sid {
SignerIdentifier::IssuerAndSerialNumber(ias) => {
let issuer = ias.issuer.to_string();
extract_rdn(&issuer, "O").or_else(|| extract_rdn(&issuer, "CN"))
}
_ => None,
}
}
fn parse_asn1_time(der: &[u8]) -> Result<i64> {
let mut r = SliceReader::new(der)?;
let header = r.peek_header()?;
let any: der::asn1::AnyRef = der::asn1::AnyRef::decode(&mut r)?;
match header.tag {
Tag::UtcTime => parse_utc(any.value()),
Tag::GeneralizedTime => parse_generalized(any.value()),
other => Err(anyhow!("unexpected time tag {other:?}")),
}
}
fn parse_utc(bytes: &[u8]) -> Result<i64> {
let s = std::str::from_utf8(bytes)?;
if s.len() < 13 {
return Err(anyhow!("UTCTime too short: {s}"));
}
let yy: i64 = s[0..2].parse()?;
let year = if yy <= 49 { 2000 + yy } else { 1900 + yy };
to_unix(year, &s[2..])
}
fn parse_generalized(bytes: &[u8]) -> Result<i64> {
let s = std::str::from_utf8(bytes)?;
if s.len() < 15 {
return Err(anyhow!("GeneralizedTime too short: {s}"));
}
let year: i64 = s[0..4].parse()?;
to_unix(year, &s[4..])
}
fn to_unix(year: i64, rest: &str) -> Result<i64> {
if rest.len() < 10 {
return Err(anyhow!("truncated time: {rest}"));
}
let month: i64 = rest[0..2].parse()?;
let day: i64 = rest[2..4].parse()?;
let hour: i64 = rest[4..6].parse()?;
let minute: i64 = rest[6..8].parse()?;
let second: i64 = rest[8..10].parse()?;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return Err(anyhow!("implausible date {year}-{month}-{day}"));
}
Ok(days_from_civil(year, month, day) * 86_400 + hour * 3600 + minute * 60 + second)
}
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = (m + 9) % 12;
let doy = (153 * mp + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
#[cfg(test)]
mod tests {
use super::*;
const RFC3161: &[u8] = include_bytes!("../fixtures/ts-rfc3161.der");
const COUNTERSIG: &[u8] = include_bytes!("../fixtures/ts-countersig.der");
#[test]
fn rfc3161_token_yields_time_and_authority() {
let info = from_rfc3161_token(RFC3161).expect("parse token");
assert_eq!(info.signed_at_unix, 1_771_374_952);
assert_eq!(info.kind, TimestampKind::Rfc3161);
assert_eq!(info.authority.as_deref(), Some("DigiCert, Inc."));
}
#[test]
fn countersignature_yields_time_and_authority() {
let info = from_countersignature(COUNTERSIG).expect("parse countersignature");
assert_eq!(info.signed_at_unix, 1_711_455_192);
assert_eq!(info.kind, TimestampKind::Countersignature);
assert_eq!(info.authority.as_deref(), Some("DigiCert, Inc."));
}
#[test]
fn utctime_pivots_at_fifty() {
assert_eq!(parse_utc(b"490101000000Z").unwrap(), to_unix(2049, "0101000000").unwrap());
assert_eq!(parse_utc(b"500101000000Z").unwrap(), to_unix(1950, "0101000000").unwrap());
}
#[test]
fn epoch_is_zero_and_a_known_date_is_right() {
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(parse_generalized(b"19700101000000Z").unwrap(), 0);
assert_eq!(parse_generalized(b"20000301000000Z").unwrap(), 951_868_800);
}
#[test]
fn a_malformed_token_is_absent_rather_than_fatal() {
assert!(from_rfc3161_token(b"not der at all").is_err());
assert!(from_countersignature(&[0x30, 0x00]).is_err());
}
}