// Copyright 2026 Exochain Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
//! RFC 3161 timestamp request and response verification for AVC receipts.
use std::collections::BTreeSet;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use cms::{
cert::CertificateChoices,
content_info::ContentInfo,
signed_data::{SignedData, SignerIdentifier, SignerInfo},
};
use const_oid::db::rfc5911::ID_SIGNED_DATA;
use der::{Decode as _, Encode as _, asn1::OctetString};
use exo_avc::AvcReceiptEvidenceSubject;
use exo_core::{Hash256, Timestamp};
use ring::signature;
use sha2::{Digest as _, Sha256};
use x509_cert::{
Certificate,
ext::pkix::{BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectKeyIdentifier},
};
#[cfg(test)]
pub(crate) const MICROSOFT_ARTIFACT_SIGNING_POLICY_OID: &str = "1.3.6.1.4.1.601.10.3.1";
#[cfg(test)]
pub(crate) const MICROSOFT_ARTIFACT_SIGNING_TIMESTAMP_URL: &str =
"http://timestamp.acs.microsoft.com";
pub(crate) const RFC3161_SHA256_ALGORITHM_OID: &str = "2.16.840.1.101.3.4.2.1";
const RFC3161_TST_INFO_CONTENT_TYPE_OID: &str = "1.2.840.113549.1.9.16.1.4";
const CMS_MESSAGE_DIGEST_ATTRIBUTE_OID: &str = "1.2.840.113549.1.9.4";
const CMS_CONTENT_TYPE_ATTRIBUTE_OID: &str = "1.2.840.113549.1.9.3";
const RSA_ENCRYPTION_OID: &str = "1.2.840.113549.1.1.1";
const SHA256_WITH_RSA_ENCRYPTION_OID: &str = "1.2.840.113549.1.1.11";
const SHA384_WITH_RSA_ENCRYPTION_OID: &str = "1.2.840.113549.1.1.12";
const SHA512_WITH_RSA_ENCRYPTION_OID: &str = "1.2.840.113549.1.1.13";
const ID_KP_TIME_STAMPING_OID: &str = "1.3.6.1.5.5.7.3.8";
const RFC3161_NONCE_DOMAIN: &[u8] = b"exo.avc.rfc3161.nonce.v1";
const DER_BOOLEAN: u8 = 0x01;
const DER_INTEGER: u8 = 0x02;
const DER_OCTET_STRING: u8 = 0x04;
const DER_NULL: u8 = 0x05;
const DER_OBJECT_IDENTIFIER: u8 = 0x06;
const DER_SEQUENCE: u8 = 0x30;
const DER_GENERALIZED_TIME: u8 = 0x18;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Rfc3161TimestampRequest {
pub der: Vec<u8>,
pub nonce_hex: String,
pub message_imprint_sha256: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Rfc3161VerifiedTimestamp {
pub issued_at: Timestamp,
pub subject_hash: Hash256,
pub message_imprint_sha256_hex: String,
pub token_der_base64: String,
pub policy_oid: String,
pub serial_number_hex: String,
pub nonce_hex: String,
pub tsa_subject: String,
pub tsa_public_key_spki_der_hex: String,
pub trust_anchor: Rfc3161VerifiedTrustAnchor,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Rfc3161TrustAnchorKind {
SignerSpki,
IssuingCaSpki,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Rfc3161VerifiedTrustAnchor {
pub kind: Rfc3161TrustAnchorKind,
pub spki_der_hex: String,
pub subject: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Rfc3161TrustAnchors {
signer_spki_der_hexes: Vec<String>,
issuing_ca_spki_der_hexes: Vec<String>,
}
impl Rfc3161TrustAnchors {
pub(crate) fn new(
signer_spki_der_hexes: Vec<String>,
issuing_ca_spki_der_hexes: Vec<String>,
) -> anyhow::Result<Self> {
let mut seen = BTreeSet::new();
let signer_spki_der_hexes = canonicalize_trust_anchor_set(
signer_spki_der_hexes,
"RFC 3161 signer SPKI trust anchor",
&mut seen,
)?;
let issuing_ca_spki_der_hexes = canonicalize_trust_anchor_set(
issuing_ca_spki_der_hexes,
"RFC 3161 issuing CA SPKI trust anchor",
&mut seen,
)?;
if signer_spki_der_hexes.is_empty() && issuing_ca_spki_der_hexes.is_empty() {
anyhow::bail!("at least one RFC 3161 trust anchor is required");
}
Ok(Self {
signer_spki_der_hexes,
issuing_ca_spki_der_hexes,
})
}
fn signer_spki_der_hexes(&self) -> &[String] {
self.signer_spki_der_hexes.as_slice()
}
fn issuing_ca_spki_der_hexes(&self) -> &[String] {
self.issuing_ca_spki_der_hexes.as_slice()
}
}
fn canonicalize_trust_anchor_set(
raw_pins: Vec<String>,
label: &str,
seen: &mut BTreeSet<String>,
) -> anyhow::Result<Vec<String>> {
let mut pins = Vec::with_capacity(raw_pins.len());
for (index, raw_pin) in raw_pins.into_iter().enumerate() {
let pin = canonical_hex(&raw_pin, label)
.map_err(|error| anyhow::anyhow!("{label} member {} is invalid: {error}", index + 1))?;
if !seen.insert(pin.clone()) {
anyhow::bail!(
"{label} member {} duplicates an existing trust anchor",
index + 1
);
}
pins.push(pin);
}
Ok(pins)
}
#[derive(Clone, Copy)]
struct DerTlv<'a> {
tag: u8,
value: &'a [u8],
encoded: &'a [u8],
}
struct DerReader<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> DerReader<'a> {
const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
const fn is_finished(&self) -> bool {
self.offset == self.bytes.len()
}
fn read_tlv(&mut self) -> anyhow::Result<DerTlv<'a>> {
let start = self.offset;
let tag = *self.bytes.get(self.offset).ok_or_else(|| {
anyhow::anyhow!("malformed DER: missing tag at offset {}", self.offset)
})?;
self.offset += 1;
let first_len = *self.bytes.get(self.offset).ok_or_else(|| {
anyhow::anyhow!("malformed DER: missing length at offset {}", self.offset)
})?;
self.offset += 1;
let len = if first_len & 0x80 == 0 {
usize::from(first_len)
} else {
let octets = usize::from(first_len & 0x7f);
if octets == 0 {
anyhow::bail!("malformed DER: indefinite length is not allowed");
}
if octets > core::mem::size_of::<usize>() {
anyhow::bail!("malformed DER: length uses {octets} octets");
}
let mut value = 0usize;
for _ in 0..octets {
let byte = *self
.bytes
.get(self.offset)
.ok_or_else(|| anyhow::anyhow!("malformed DER: truncated long-form length"))?;
self.offset += 1;
value = value
.checked_mul(256)
.and_then(|acc| acc.checked_add(usize::from(byte)))
.ok_or_else(|| anyhow::anyhow!("malformed DER: length overflow"))?;
}
value
};
let end = self
.offset
.checked_add(len)
.ok_or_else(|| anyhow::anyhow!("malformed DER: length overflow"))?;
if end > self.bytes.len() {
anyhow::bail!("malformed DER: value overruns input");
}
let value = &self.bytes[self.offset..end];
self.offset = end;
Ok(DerTlv {
tag,
value,
encoded: &self.bytes[start..end],
})
}
}
fn der_len(len: usize) -> Vec<u8> {
if let Ok(short) = u8::try_from(len) {
if short < 128 {
return vec![short];
}
}
let bytes = len.to_be_bytes();
let first_non_zero = bytes
.iter()
.position(|byte| *byte != 0)
.unwrap_or(bytes.len() - 1);
let length_bytes = &bytes[first_non_zero..];
let mut encoded = Vec::with_capacity(1 + length_bytes.len());
encoded.push(0x80 | u8::try_from(length_bytes.len()).unwrap_or(8));
encoded.extend_from_slice(length_bytes);
encoded
}
fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut encoded = Vec::with_capacity(1 + der_len(value.len()).len() + value.len());
encoded.push(tag);
encoded.extend_from_slice(&der_len(value.len()));
encoded.extend_from_slice(value);
encoded
}
fn der_sequence(value: &[u8]) -> Vec<u8> {
der_tlv(DER_SEQUENCE, value)
}
fn der_null() -> Vec<u8> {
der_tlv(DER_NULL, &[])
}
fn der_bool(value: bool) -> Vec<u8> {
der_tlv(DER_BOOLEAN, &[if value { 0xff } else { 0x00 }])
}
fn der_integer_from_u8(value: u8) -> Vec<u8> {
der_tlv(DER_INTEGER, &[value])
}
fn positive_integer_value_bytes(bytes: &[u8]) -> Vec<u8> {
let first_non_zero = bytes
.iter()
.position(|byte| *byte != 0)
.unwrap_or(bytes.len().saturating_sub(1));
let mut value = bytes[first_non_zero..].to_vec();
if value.is_empty() {
value.push(0);
}
if value[0] & 0x80 != 0 {
value.insert(0, 0);
}
value
}
fn der_integer_from_positive_bytes(bytes: &[u8]) -> Vec<u8> {
der_tlv(DER_INTEGER, &positive_integer_value_bytes(bytes))
}
/// Minimal unsigned big-endian representation of an integer: leading zero bytes
/// removed (keeping at least one byte). This is the canonical value the TSA
/// echoes back and that `parse_positive_integer_bytes` reconstructs from the
/// response — so the request-side `nonce_hex` must use the SAME canonicalization,
/// otherwise a nonce whose first byte is `0x00` fails the nonce-equality check
/// (`canonical_hex` does not strip leading zeros) and the whole emit fails closed.
fn minimal_unsigned_integer_bytes(bytes: &[u8]) -> &[u8] {
let first_non_zero = bytes
.iter()
.position(|byte| *byte != 0)
.unwrap_or(bytes.len().saturating_sub(1));
&bytes[first_non_zero..]
}
fn encode_oid_arc(mut arc: u32, out: &mut Vec<u8>) {
let mut stack = [0u8; 5];
let mut len = 1usize;
stack[stack.len() - 1] = u8::try_from(arc & 0x7f).unwrap_or(0);
arc >>= 7;
while arc > 0 {
len += 1;
stack[stack.len() - len] = u8::try_from((arc & 0x7f) | 0x80).unwrap_or(0x80);
arc >>= 7;
}
out.extend_from_slice(&stack[stack.len() - len..]);
}
fn der_oid(oid: &str) -> anyhow::Result<Vec<u8>> {
let arcs = oid
.split('.')
.map(|part| {
part.parse::<u32>()
.map_err(|error| anyhow::anyhow!("invalid OID arc '{part}': {error}"))
})
.collect::<anyhow::Result<Vec<_>>>()?;
if arcs.len() < 2 {
anyhow::bail!("OID must contain at least two arcs");
}
if arcs[0] > 2 {
anyhow::bail!("OID first arc must be 0, 1, or 2");
}
if arcs[0] < 2 && arcs[1] > 39 {
anyhow::bail!("OID second arc must be <= 39 when first arc is {}", arcs[0]);
}
let first = arcs[0]
.checked_mul(40)
.and_then(|value| value.checked_add(arcs[1]))
.ok_or_else(|| anyhow::anyhow!("OID first two arcs overflow"))?;
let mut value = Vec::new();
encode_oid_arc(first, &mut value);
for arc in &arcs[2..] {
encode_oid_arc(*arc, &mut value);
}
Ok(der_tlv(DER_OBJECT_IDENTIFIER, &value))
}
fn der_algorithm_identifier(oid: &str) -> anyhow::Result<Vec<u8>> {
let mut value = Vec::new();
value.extend_from_slice(&der_oid(oid)?);
value.extend_from_slice(&der_null());
Ok(der_sequence(&value))
}
fn deterministic_nonce(
evidence_subject: &AvcReceiptEvidenceSubject,
policy_oid: &str,
) -> anyhow::Result<[u8; 32]> {
let mut hasher = Sha256::new();
hasher.update(RFC3161_NONCE_DOMAIN);
hasher.update(policy_oid.as_bytes());
hasher.update(evidence_subject.canonical_bytes()?);
let digest = hasher.finalize();
let mut nonce = [0u8; 32];
nonce.copy_from_slice(&digest);
Ok(nonce)
}
pub(crate) fn build_timestamp_request(
evidence_subject: &AvcReceiptEvidenceSubject,
policy_oid: &str,
) -> anyhow::Result<Rfc3161TimestampRequest> {
let message_imprint_sha256 = evidence_subject.rfc3161_sha256_message_imprint()?;
let nonce = deterministic_nonce(evidence_subject, policy_oid)?;
let mut message_imprint = Vec::new();
message_imprint.extend_from_slice(&der_algorithm_identifier(RFC3161_SHA256_ALGORITHM_OID)?);
message_imprint.extend_from_slice(&der_tlv(DER_OCTET_STRING, &message_imprint_sha256));
let mut request = Vec::new();
request.extend_from_slice(&der_integer_from_u8(1));
request.extend_from_slice(&der_sequence(&message_imprint));
request.extend_from_slice(&der_oid(policy_oid)?);
request.extend_from_slice(&der_integer_from_positive_bytes(&nonce));
request.extend_from_slice(&der_bool(true));
Ok(Rfc3161TimestampRequest {
der: der_sequence(&request),
// Canonical (leading-zero-stripped) nonce value, matching what the TSA
// echoes and what `parse_positive_integer_bytes` yields from the response.
nonce_hex: hex::encode(minimal_unsigned_integer_bytes(&nonce)),
message_imprint_sha256,
})
}
fn parse_oid_value(value: &[u8]) -> anyhow::Result<String> {
if value.is_empty() {
anyhow::bail!("malformed DER OID: empty value");
}
let mut arcs = Vec::new();
let mut current = 0u32;
let mut first_value: Option<u32> = None;
for byte in value {
current = current
.checked_mul(128)
.and_then(|acc| acc.checked_add(u32::from(byte & 0x7f)))
.ok_or_else(|| anyhow::anyhow!("malformed DER OID: arc overflow"))?;
if byte & 0x80 == 0 {
if first_value.is_none() {
first_value = Some(current);
} else {
arcs.push(current);
}
current = 0;
}
}
if value.last().is_some_and(|byte| byte & 0x80 != 0) {
anyhow::bail!("malformed DER OID: truncated base128 arc");
}
let first = first_value.ok_or_else(|| anyhow::anyhow!("malformed DER OID"))?;
let (arc0, arc1) = if first < 40 {
(0, first)
} else if first < 80 {
(1, first - 40)
} else {
(2, first - 80)
};
let mut text = format!("{arc0}.{arc1}");
for arc in arcs {
text.push('.');
text.push_str(&arc.to_string());
}
Ok(text)
}
fn parse_oid(tlv: DerTlv<'_>) -> anyhow::Result<String> {
if tlv.tag != DER_OBJECT_IDENTIFIER {
anyhow::bail!("malformed DER: expected OID, got tag 0x{:02x}", tlv.tag);
}
parse_oid_value(tlv.value)
}
fn parse_positive_integer_bytes(tlv: DerTlv<'_>) -> anyhow::Result<Vec<u8>> {
if tlv.tag != DER_INTEGER {
anyhow::bail!("malformed DER: expected INTEGER, got tag 0x{:02x}", tlv.tag);
}
if tlv.value.is_empty() {
anyhow::bail!("malformed DER INTEGER: empty value");
}
if tlv.value[0] & 0x80 != 0 {
anyhow::bail!("malformed DER INTEGER: negative values are not supported");
}
let value = if tlv.value.len() > 1 && tlv.value[0] == 0 {
tlv.value[1..].to_vec()
} else {
tlv.value.to_vec()
};
Ok(value)
}
fn parse_positive_integer_u8(tlv: DerTlv<'_>) -> anyhow::Result<u8> {
let value = parse_positive_integer_bytes(tlv)?;
if value.len() != 1 {
anyhow::bail!(
"malformed DER INTEGER: expected one byte, got {}",
value.len()
);
}
Ok(value[0])
}
fn parse_timestamp_response_status(response_der: &[u8]) -> anyhow::Result<(u8, &[u8])> {
let mut outer = DerReader::new(response_der);
let response = outer.read_tlv()?;
if !outer.is_finished() || response.tag != DER_SEQUENCE {
anyhow::bail!("malformed RFC 3161 timestamp response: expected outer sequence");
}
let mut response_fields = DerReader::new(response.value);
let status_info = response_fields.read_tlv()?;
if status_info.tag != DER_SEQUENCE {
anyhow::bail!("malformed RFC 3161 timestamp response: expected status sequence");
}
let mut status_fields = DerReader::new(status_info.value);
let status = parse_positive_integer_u8(status_fields.read_tlv()?)?;
let token_der = if response_fields.is_finished() {
&[][..]
} else {
response_fields.read_tlv()?.encoded
};
if !response_fields.is_finished() {
anyhow::bail!("malformed RFC 3161 timestamp response: trailing fields");
}
Ok((status, token_der))
}
fn canonical_hex(raw: &str, label: &str) -> anyhow::Result<String> {
let bytes = hex::decode(raw).map_err(|error| anyhow::anyhow!("{label} is not hex: {error}"))?;
if bytes.is_empty() {
anyhow::bail!("{label} must not be empty");
}
Ok(hex::encode(bytes))
}
fn parse_generalized_time_ms(value: &[u8]) -> anyhow::Result<u64> {
let text = std::str::from_utf8(value)
.map_err(|error| anyhow::anyhow!("TSTInfo genTime is not UTF-8: {error}"))?;
let without_z = text
.strip_suffix('Z')
.ok_or_else(|| anyhow::anyhow!("TSTInfo genTime must be UTC and end in Z"))?;
let (base, fraction) = match without_z.split_once('.') {
Some((base, fraction)) => (base, Some(fraction)),
None => (without_z, None),
};
if base.len() != 14 || !base.bytes().all(|byte| byte.is_ascii_digit()) {
anyhow::bail!("TSTInfo genTime must use YYYYMMDDHHMMSSZ form");
}
let parse = |range: core::ops::Range<usize>, label: &str| -> anyhow::Result<u32> {
base[range]
.parse::<u32>()
.map_err(|error| anyhow::anyhow!("TSTInfo genTime {label} is invalid: {error}"))
};
let year = parse(0..4, "year")?;
let month = parse(4..6, "month")?;
let day = parse(6..8, "day")?;
let hour = parse(8..10, "hour")?;
let minute = parse(10..12, "minute")?;
let second = parse(12..14, "second")?;
let millisecond = match fraction {
Some(fraction) => {
if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
anyhow::bail!("TSTInfo genTime fractional seconds are invalid");
}
let mut digits = fraction
.as_bytes()
.iter()
.copied()
.take(3)
.collect::<Vec<_>>();
while digits.len() < 3 {
digits.push(b'0');
}
std::str::from_utf8(&digits)
.map_err(|error| anyhow::anyhow!("TSTInfo milliseconds are invalid: {error}"))?
.parse::<u32>()
.map_err(|error| anyhow::anyhow!("TSTInfo milliseconds are invalid: {error}"))?
}
None => 0,
};
let year = i32::try_from(year).map_err(|_| anyhow::anyhow!("TSTInfo year is too large"))?;
let date = chrono::NaiveDate::from_ymd_opt(year, month, day)
.ok_or_else(|| anyhow::anyhow!("TSTInfo genTime date is invalid"))?;
let time = date
.and_hms_milli_opt(hour, minute, second, millisecond)
.ok_or_else(|| anyhow::anyhow!("TSTInfo genTime clock is invalid"))?;
let epoch_ms = time.and_utc().timestamp_millis();
u64::try_from(epoch_ms).map_err(|_| anyhow::anyhow!("TSTInfo genTime predates Unix epoch"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedTstInfo {
policy_oid: String,
message_imprint_sha256: [u8; 32],
serial_number_hex: String,
issued_at: Timestamp,
nonce_hex: String,
}
fn parse_message_imprint(tlv: DerTlv<'_>) -> anyhow::Result<[u8; 32]> {
if tlv.tag != DER_SEQUENCE {
anyhow::bail!("TSTInfo messageImprint must be a sequence");
}
let mut fields = DerReader::new(tlv.value);
let algorithm_identifier = fields.read_tlv()?;
if algorithm_identifier.tag != DER_SEQUENCE {
anyhow::bail!("TSTInfo messageImprint algorithm must be a sequence");
}
let mut algorithm_fields = DerReader::new(algorithm_identifier.value);
let algorithm_oid = parse_oid(algorithm_fields.read_tlv()?)?;
if algorithm_oid != RFC3161_SHA256_ALGORITHM_OID {
anyhow::bail!("TSTInfo messageImprint hash algorithm {algorithm_oid} is not SHA-256");
}
if !algorithm_fields.is_finished() {
let params = algorithm_fields.read_tlv()?;
if params.tag != DER_NULL || !params.value.is_empty() {
anyhow::bail!("TSTInfo messageImprint SHA-256 parameters must be NULL when present");
}
}
if !algorithm_fields.is_finished() {
anyhow::bail!("TSTInfo messageImprint algorithm has trailing fields");
}
let imprint = fields.read_tlv()?;
if imprint.tag != DER_OCTET_STRING || imprint.value.len() != 32 {
anyhow::bail!("TSTInfo messageImprint hashedMessage must be a 32-byte OCTET STRING");
}
if !fields.is_finished() {
anyhow::bail!("TSTInfo messageImprint has trailing fields");
}
let mut message_imprint = [0u8; 32];
message_imprint.copy_from_slice(imprint.value);
Ok(message_imprint)
}
fn parse_tst_info(tst_info_der: &[u8]) -> anyhow::Result<ParsedTstInfo> {
let mut outer = DerReader::new(tst_info_der);
let sequence = outer.read_tlv()?;
if !outer.is_finished() || sequence.tag != DER_SEQUENCE {
anyhow::bail!("TSTInfo must be a DER sequence");
}
let mut fields = DerReader::new(sequence.value);
let version = parse_positive_integer_u8(fields.read_tlv()?)?;
if version != 1 {
anyhow::bail!("TSTInfo version {version} is not supported");
}
let policy_oid = parse_oid(fields.read_tlv()?)?;
let message_imprint_sha256 = parse_message_imprint(fields.read_tlv()?)?;
let serial_number_hex = hex::encode(parse_positive_integer_bytes(fields.read_tlv()?)?);
let gen_time = fields.read_tlv()?;
if gen_time.tag != DER_GENERALIZED_TIME {
anyhow::bail!("TSTInfo genTime must be GeneralizedTime");
}
let issued_at = Timestamp::new(parse_generalized_time_ms(gen_time.value)?, 0);
let mut nonce_hex = None;
while !fields.is_finished() {
let field = fields.read_tlv()?;
match field.tag {
DER_SEQUENCE | DER_BOOLEAN | 0xa0 | 0xa1 => {}
DER_INTEGER => {
if nonce_hex.is_some() {
anyhow::bail!("TSTInfo contains multiple nonce integers");
}
nonce_hex = Some(hex::encode(parse_positive_integer_bytes(field)?));
}
_ => anyhow::bail!("TSTInfo contains unsupported field tag 0x{:02x}", field.tag),
}
}
let nonce_hex = nonce_hex.ok_or_else(|| anyhow::anyhow!("TSTInfo nonce is missing"))?;
Ok(ParsedTstInfo {
policy_oid,
message_imprint_sha256,
serial_number_hex,
issued_at,
nonce_hex,
})
}
fn certificate_set(signed_data: &SignedData) -> anyhow::Result<Vec<&Certificate>> {
let certs = signed_data
.certificates
.as_ref()
.ok_or_else(|| anyhow::anyhow!("RFC 3161 TimeStampToken has no embedded certificates"))?;
let certificates = certs
.0
.iter()
.filter_map(|choice| match choice {
CertificateChoices::Certificate(cert) => Some(cert),
CertificateChoices::Other(_) => None,
})
.collect::<Vec<_>>();
if certificates.is_empty() {
anyhow::bail!("RFC 3161 TimeStampToken has no X.509 signer certificate");
}
Ok(certificates)
}
fn signer_certificate<'a>(
signer_info: &SignerInfo,
certificates: &'a [&'a Certificate],
) -> anyhow::Result<&'a Certificate> {
for cert in certificates {
match &signer_info.sid {
SignerIdentifier::IssuerAndSerialNumber(isn) => {
if cert.tbs_certificate().issuer() == &isn.issuer
&& cert.tbs_certificate().serial_number() == &isn.serial_number
{
return Ok(cert);
}
}
SignerIdentifier::SubjectKeyIdentifier(signer_ski) => {
if let Ok(Some((_critical, cert_ski))) = cert
.tbs_certificate()
.get_extension::<SubjectKeyIdentifier>()
&& cert_ski.0.as_bytes() == signer_ski.0.as_bytes()
{
return Ok(cert);
}
}
}
}
anyhow::bail!("RFC 3161 TimeStampToken signer certificate is missing")
}
fn signed_attribute_octet_string(
signer_info: &SignerInfo,
attr_oid: &str,
) -> anyhow::Result<Vec<u8>> {
let attrs = signer_info
.signed_attrs
.as_ref()
.ok_or_else(|| anyhow::anyhow!("CMS signed attributes are missing"))?;
let mut values = attrs
.iter()
.filter(|attr| attr.oid.to_string() == attr_oid)
.flat_map(|attr| attr.values.iter())
.collect::<Vec<_>>();
if values.len() != 1 {
anyhow::bail!("CMS signed attribute {attr_oid} must appear exactly once");
}
let value = values.remove(0);
let octet = OctetString::from_der(&value.to_der()?)
.map_err(|error| anyhow::anyhow!("CMS signed attribute {attr_oid} is invalid: {error}"))?;
Ok(octet.as_bytes().to_vec())
}
fn signed_attribute_oid(signer_info: &SignerInfo, attr_oid: &str) -> anyhow::Result<String> {
let attrs = signer_info
.signed_attrs
.as_ref()
.ok_or_else(|| anyhow::anyhow!("CMS signed attributes are missing"))?;
let mut values = attrs
.iter()
.filter(|attr| attr.oid.to_string() == attr_oid)
.flat_map(|attr| attr.values.iter())
.collect::<Vec<_>>();
if values.len() != 1 {
anyhow::bail!("CMS signed attribute {attr_oid} must appear exactly once");
}
let value = values.remove(0);
parse_oid_value(value.value())
.map_err(|error| anyhow::anyhow!("CMS signed attribute {attr_oid} is invalid: {error}"))
}
fn der_positive_integer_bytes(value: &[u8], field_name: &str) -> anyhow::Result<Vec<u8>> {
if value.is_empty() {
anyhow::bail!("RSA public key {field_name} integer is empty");
}
if value[0] & 0x80 != 0 {
anyhow::bail!("RSA public key {field_name} integer is negative");
}
let first_non_zero = value
.iter()
.position(|byte| *byte != 0)
.unwrap_or(value.len() - 1);
let positive = &value[first_non_zero..];
if positive.iter().all(|byte| *byte == 0) {
anyhow::bail!("RSA public key {field_name} integer is zero");
}
Ok(positive.to_vec())
}
fn rsa_public_key_components_from_certificate(
signer_cert: &Certificate,
) -> anyhow::Result<(String, Vec<u8>, Vec<u8>)> {
let spki = signer_cert.tbs_certificate().subject_public_key_info();
let spki_algorithm_oid = spki.algorithm.oid.to_string();
if spki_algorithm_oid != RSA_ENCRYPTION_OID {
anyhow::bail!("TSA signer public key algorithm {spki_algorithm_oid} is not RSA");
}
let spki_der = spki
.to_der()
.map_err(|error| anyhow::anyhow!("TSA signer SPKI DER encoding failed: {error}"))?;
let rsa_public_key_der = spki
.subject_public_key
.as_bytes()
.ok_or_else(|| anyhow::anyhow!("TSA signer RSA public key has unused BIT STRING bits"))?;
let mut public_key_reader = DerReader::new(rsa_public_key_der);
let public_key_sequence = public_key_reader
.read_tlv()
.map_err(|error| anyhow::anyhow!("TSA signer RSA public key DER is malformed: {error}"))?;
if public_key_sequence.tag != DER_SEQUENCE || !public_key_reader.is_finished() {
anyhow::bail!("TSA signer RSA public key is not a single DER SEQUENCE");
}
let mut components_reader = DerReader::new(public_key_sequence.value);
let modulus = components_reader
.read_tlv()
.map_err(|error| anyhow::anyhow!("TSA signer RSA modulus is malformed: {error}"))?;
let exponent = components_reader
.read_tlv()
.map_err(|error| anyhow::anyhow!("TSA signer RSA exponent is malformed: {error}"))?;
if modulus.tag != DER_INTEGER {
anyhow::bail!("TSA signer RSA modulus is not a DER INTEGER");
}
if exponent.tag != DER_INTEGER {
anyhow::bail!("TSA signer RSA exponent is not a DER INTEGER");
}
if !components_reader.is_finished() {
anyhow::bail!("TSA signer RSA public key has trailing DER fields");
}
Ok((
hex::encode(spki_der),
der_positive_integer_bytes(modulus.value, "modulus")?,
der_positive_integer_bytes(exponent.value, "exponent")?,
))
}
fn verify_cms_signature(
signer_info: &SignerInfo,
signer_cert: &Certificate,
signed_attrs_der: &[u8],
) -> anyhow::Result<String> {
let digest_oid = signer_info.digest_alg.oid.to_string();
if digest_oid != RFC3161_SHA256_ALGORITHM_OID {
anyhow::bail!("CMS signer digest algorithm {digest_oid} is not SHA-256");
}
let signature_oid = signer_info.signature_algorithm.oid.to_string();
if signature_oid != SHA256_WITH_RSA_ENCRYPTION_OID && signature_oid != RSA_ENCRYPTION_OID {
anyhow::bail!("CMS signer signature algorithm {signature_oid} is not RSA SHA-256");
}
let (spki_der_hex, modulus, exponent) =
rsa_public_key_components_from_certificate(signer_cert)?;
let public_key = signature::RsaPublicKeyComponents {
n: modulus.as_slice(),
e: exponent.as_slice(),
};
public_key
.verify(
&signature::RSA_PKCS1_2048_8192_SHA256,
signed_attrs_der,
signer_info.signature.as_bytes(),
)
.map_err(|_| anyhow::anyhow!("CMS signature verification failed"))?;
Ok(spki_der_hex)
}
fn timestamp_ms_within_certificate_validity(
cert: &Certificate,
timestamp: Timestamp,
role: &str,
) -> anyhow::Result<()> {
let validity = cert.tbs_certificate().validity();
let issued_at_ms = u128::from(timestamp.physical_ms);
let not_before_ms = validity.not_before.to_unix_duration().as_millis();
let not_after_ms = validity.not_after.to_unix_duration().as_millis();
if issued_at_ms < not_before_ms {
anyhow::bail!("TSA {role} certificate was not valid at RFC 3161 genTime");
}
if issued_at_ms > not_after_ms {
anyhow::bail!("TSA {role} certificate expired before RFC 3161 genTime");
}
Ok(())
}
fn validate_tsa_signer_certificate(cert: &Certificate, issued_at: Timestamp) -> anyhow::Result<()> {
timestamp_ms_within_certificate_validity(cert, issued_at, "signer")?;
let eku = cert
.tbs_certificate()
.get_extension::<ExtendedKeyUsage>()
.map_err(|error| anyhow::anyhow!("TSA signer EKU extension parse failed: {error}"))?
.ok_or_else(|| {
anyhow::anyhow!("TSA signer certificate has no Extended Key Usage extension")
})?;
if !eku
.1
.0
.iter()
.any(|oid| oid.to_string() == ID_KP_TIME_STAMPING_OID)
{
anyhow::bail!("TSA signer certificate EKU does not assert id-kp-timeStamping");
}
if let Some((_critical, basic_constraints)) = cert
.tbs_certificate()
.get_extension::<BasicConstraints>()
.map_err(|error| {
anyhow::anyhow!("TSA signer basicConstraints extension parse failed: {error}")
})?
&& basic_constraints.ca
{
anyhow::bail!("TSA signer certificate must not assert CA:TRUE");
}
if let Some((_critical, key_usage)) = cert
.tbs_certificate()
.get_extension::<KeyUsage>()
.map_err(|error| anyhow::anyhow!("TSA signer keyUsage extension parse failed: {error}"))?
&& !key_usage.digital_signature()
&& !key_usage.non_repudiation()
{
anyhow::bail!("TSA signer keyUsage does not permit timestamp signing");
}
Ok(())
}
fn validate_tsa_issuing_ca_certificate(
cert: &Certificate,
issued_at: Timestamp,
) -> anyhow::Result<()> {
timestamp_ms_within_certificate_validity(cert, issued_at, "issuing CA")?;
let basic_constraints = cert
.tbs_certificate()
.get_extension::<BasicConstraints>()
.map_err(|error| {
anyhow::anyhow!("TSA issuing CA basicConstraints extension parse failed: {error}")
})?
.ok_or_else(|| {
anyhow::anyhow!("TSA issuing CA certificate has no basicConstraints extension")
})?;
if !basic_constraints.1.ca {
anyhow::bail!("TSA issuing CA certificate does not assert CA:TRUE");
}
let key_usage = cert
.tbs_certificate()
.get_extension::<KeyUsage>()
.map_err(|error| {
anyhow::anyhow!("TSA issuing CA keyUsage extension parse failed: {error}")
})?
.ok_or_else(|| anyhow::anyhow!("TSA issuing CA certificate has no keyUsage extension"))?;
if !key_usage.1.key_cert_sign() {
anyhow::bail!("TSA issuing CA keyUsage does not assert keyCertSign");
}
Ok(())
}
fn rsa_verification_params_for_oid(
sig_alg_oid: &str,
) -> anyhow::Result<&'static signature::RsaParameters> {
if sig_alg_oid == SHA256_WITH_RSA_ENCRYPTION_OID {
Ok(&signature::RSA_PKCS1_2048_8192_SHA256)
} else if sig_alg_oid == SHA384_WITH_RSA_ENCRYPTION_OID {
Ok(&signature::RSA_PKCS1_2048_8192_SHA384)
} else if sig_alg_oid == SHA512_WITH_RSA_ENCRYPTION_OID {
Ok(&signature::RSA_PKCS1_2048_8192_SHA512)
} else {
anyhow::bail!(
"TSA certificate signature algorithm {sig_alg_oid} is not RSA-PKCS1 SHA-256/384/512"
)
}
}
fn verify_certificate_signed_by(
cert: &Certificate,
issuer_modulus: &[u8],
issuer_exponent: &[u8],
) -> anyhow::Result<()> {
let verification_params =
rsa_verification_params_for_oid(&cert.signature_algorithm().oid.to_string())?;
let tbs_der = cert.tbs_certificate().to_der().map_err(|error| {
anyhow::anyhow!("TSA signer tbsCertificate DER encoding failed: {error}")
})?;
let signature_bytes = cert.signature().as_bytes().ok_or_else(|| {
anyhow::anyhow!("TSA signer certificate signature has unused BIT STRING bits")
})?;
signature::RsaPublicKeyComponents {
n: issuer_modulus,
e: issuer_exponent,
}
.verify(verification_params, &tbs_der, signature_bytes)
.map_err(|_| {
anyhow::anyhow!("TSA signer certificate signature did not verify against the pinned CA")
})?;
Ok(())
}
fn direct_signer_trust_anchor(
signer_spki_der_hex: &str,
signer_subject: &str,
trust_anchors: &Rfc3161TrustAnchors,
) -> Option<Rfc3161VerifiedTrustAnchor> {
trust_anchors
.signer_spki_der_hexes()
.iter()
.any(|expected| expected == signer_spki_der_hex)
.then(|| Rfc3161VerifiedTrustAnchor {
kind: Rfc3161TrustAnchorKind::SignerSpki,
spki_der_hex: signer_spki_der_hex.to_owned(),
subject: signer_subject.to_owned(),
})
}
fn signer_chains_to_pinned_ca(
signer_cert: &Certificate,
certificates: &[&Certificate],
trust_anchors: &Rfc3161TrustAnchors,
issued_at: Timestamp,
) -> anyhow::Result<Rfc3161VerifiedTrustAnchor> {
let signer_issuer = signer_cert.tbs_certificate().issuer();
for candidate in certificates {
if candidate.tbs_certificate().subject() != signer_issuer {
continue;
}
let (ca_spki_hex, ca_modulus, ca_exponent) =
match rsa_public_key_components_from_certificate(candidate) {
Ok(components) => components,
Err(_) => continue,
};
if !trust_anchors
.issuing_ca_spki_der_hexes()
.iter()
.any(|expected| expected == &ca_spki_hex)
{
continue;
}
validate_tsa_issuing_ca_certificate(candidate, issued_at)?;
verify_certificate_signed_by(signer_cert, &ca_modulus, &ca_exponent)?;
return Ok(Rfc3161VerifiedTrustAnchor {
kind: Rfc3161TrustAnchorKind::IssuingCaSpki,
spki_der_hex: ca_spki_hex,
subject: candidate.tbs_certificate().subject().to_string(),
});
}
anyhow::bail!("RFC 3161 TSA signer certificate did not chain to any pinned CA trust anchor")
}
fn verify_timestamp_response_with_optional_trust_anchors(
response_der: &[u8],
expected_subject_hash: Hash256,
expected_message_imprint_sha256: [u8; 32],
expected_nonce_hex: &str,
expected_policy_oid: &str,
trust_anchors: Option<&Rfc3161TrustAnchors>,
) -> anyhow::Result<Rfc3161VerifiedTimestamp> {
let (status, token_der) = parse_timestamp_response_status(response_der)
.map_err(|error| anyhow::anyhow!("malformed RFC 3161 timestamp response: {error}"))?;
if status != 0 && status != 1 {
anyhow::bail!("RFC 3161 timestamp response denied with status {status}");
}
if token_der.is_empty() {
anyhow::bail!("malformed RFC 3161 timestamp response: missing TimeStampToken");
}
let content_info = ContentInfo::from_der(token_der)
.map_err(|error| anyhow::anyhow!("malformed CMS TimeStampToken: {error}"))?;
if content_info.content_type != ID_SIGNED_DATA {
anyhow::bail!("CMS TimeStampToken content type is not signedData");
}
let signed_data = SignedData::from_der(&content_info.content.to_der()?)
.map_err(|error| anyhow::anyhow!("malformed CMS SignedData: {error}"))?;
if signed_data.encap_content_info.econtent_type.to_string() != RFC3161_TST_INFO_CONTENT_TYPE_OID
{
anyhow::bail!("CMS SignedData content type is not RFC 3161 TSTInfo");
}
let tst_info_der = signed_data
.encap_content_info
.econtent
.as_ref()
.ok_or_else(|| anyhow::anyhow!("CMS SignedData TSTInfo content is missing"))?
.value();
let tst_info = parse_tst_info(tst_info_der)?;
if tst_info.policy_oid != expected_policy_oid {
anyhow::bail!(
"RFC 3161 policy {} did not match expected {}",
tst_info.policy_oid,
expected_policy_oid
);
}
if tst_info.message_imprint_sha256 != expected_message_imprint_sha256 {
anyhow::bail!("RFC 3161 message imprint did not match evidence subject");
}
if tst_info.nonce_hex != canonical_hex(expected_nonce_hex, "RFC 3161 expected nonce")? {
anyhow::bail!("RFC 3161 nonce did not match request nonce");
}
let mut tst_info_digest = Sha256::new();
tst_info_digest.update(tst_info_der);
let computed_message_digest = tst_info_digest.finalize().to_vec();
let signer_infos = signed_data.signer_infos.0.iter().collect::<Vec<_>>();
if signer_infos.len() != 1 {
anyhow::bail!(
"CMS SignedData must contain exactly one signer info, got {}",
signer_infos.len()
);
}
let certificates = certificate_set(&signed_data)?;
let certificate_refs = certificates.into_iter().collect::<Vec<_>>();
let signer_info = signer_infos[0];
let content_type = signed_attribute_oid(signer_info, CMS_CONTENT_TYPE_ATTRIBUTE_OID)?;
if content_type != RFC3161_TST_INFO_CONTENT_TYPE_OID {
anyhow::bail!("CMS signed content-type attribute is not RFC 3161 TSTInfo");
}
let message_digest =
signed_attribute_octet_string(signer_info, CMS_MESSAGE_DIGEST_ATTRIBUTE_OID)?;
if message_digest != computed_message_digest {
anyhow::bail!("CMS signed messageDigest does not match TSTInfo content");
}
let signed_attrs = signer_info
.signed_attrs
.as_ref()
.ok_or_else(|| anyhow::anyhow!("CMS signed attributes are missing"))?;
let signed_attrs_der = signed_attrs
.to_der()
.map_err(|error| anyhow::anyhow!("CMS signed attributes DER encoding failed: {error}"))?;
let signer_cert = signer_certificate(signer_info, &certificate_refs)?;
let signer_spki_der_hex = verify_cms_signature(signer_info, signer_cert, &signed_attrs_der)?;
let verified_signer_subject = signer_cert.tbs_certificate().subject().to_string();
let trust_anchor = if let Some(trust_anchors) = trust_anchors {
validate_tsa_signer_certificate(signer_cert, tst_info.issued_at)?;
direct_signer_trust_anchor(
&signer_spki_der_hex,
&verified_signer_subject,
trust_anchors,
)
.map(Ok)
.unwrap_or_else(|| {
signer_chains_to_pinned_ca(
signer_cert,
&certificate_refs,
trust_anchors,
tst_info.issued_at,
)
.map_err(|error| {
anyhow::anyhow!(
"RFC 3161 TSA signer did not match any configured trust anchor: {error}"
)
})
})?
} else {
Rfc3161VerifiedTrustAnchor {
kind: Rfc3161TrustAnchorKind::SignerSpki,
spki_der_hex: signer_spki_der_hex.clone(),
subject: verified_signer_subject.clone(),
}
};
Ok(Rfc3161VerifiedTimestamp {
issued_at: tst_info.issued_at,
subject_hash: expected_subject_hash,
message_imprint_sha256_hex: hex::encode(tst_info.message_imprint_sha256),
token_der_base64: BASE64_STANDARD.encode(token_der),
policy_oid: tst_info.policy_oid,
serial_number_hex: tst_info.serial_number_hex,
nonce_hex: tst_info.nonce_hex,
tsa_subject: verified_signer_subject,
tsa_public_key_spki_der_hex: signer_spki_der_hex,
trust_anchor,
})
}
#[cfg(test)]
fn verify_timestamp_response(
response_der: &[u8],
expected_subject_hash: Hash256,
expected_message_imprint_sha256: [u8; 32],
expected_nonce_hex: &str,
expected_policy_oid: &str,
expected_tsa_spki_der_hex: &str,
) -> anyhow::Result<Rfc3161VerifiedTimestamp> {
let trust_anchors =
Rfc3161TrustAnchors::new(vec![expected_tsa_spki_der_hex.to_owned()], Vec::new())?;
verify_timestamp_response_with_trust_anchors(
response_der,
expected_subject_hash,
expected_message_imprint_sha256,
expected_nonce_hex,
expected_policy_oid,
&trust_anchors,
)
}
pub(crate) fn verify_timestamp_response_with_trust_anchors(
response_der: &[u8],
expected_subject_hash: Hash256,
expected_message_imprint_sha256: [u8; 32],
expected_nonce_hex: &str,
expected_policy_oid: &str,
trust_anchors: &Rfc3161TrustAnchors,
) -> anyhow::Result<Rfc3161VerifiedTimestamp> {
verify_timestamp_response_with_optional_trust_anchors(
response_der,
expected_subject_hash,
expected_message_imprint_sha256,
expected_nonce_hex,
expected_policy_oid,
Some(trust_anchors),
)
}
#[cfg(test)]
fn inspect_timestamp_response_without_spki_pin(
response_der: &[u8],
expected_subject_hash: Hash256,
expected_message_imprint_sha256: [u8; 32],
expected_nonce_hex: &str,
expected_policy_oid: &str,
) -> anyhow::Result<Rfc3161VerifiedTimestamp> {
verify_timestamp_response_with_optional_trust_anchors(
response_der,
expected_subject_hash,
expected_message_imprint_sha256,
expected_nonce_hex,
expected_policy_oid,
None,
)
}
#[cfg(test)]
pub(crate) fn microsoft_fixture_timestamp_token_der_base64() -> anyhow::Result<String> {
let response_der = BASE64_STANDARD
.decode(include_str!("fixtures/microsoft_rfc3161_timestamp_response.b64").trim())?;
let (_status, token_der) = parse_timestamp_response_status(&response_der)?;
Ok(BASE64_STANDARD.encode(token_der))
}
#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedTimestampRequestForTest {
hash_algorithm_oid: String,
message_imprint_sha256: [u8; 32],
policy_oid: String,
nonce_hex: String,
cert_req: bool,
}
#[cfg(test)]
fn parse_timestamp_request_for_test(der: &[u8]) -> anyhow::Result<ParsedTimestampRequestForTest> {
let mut outer = DerReader::new(der);
let request = outer.read_tlv()?;
if !outer.is_finished() || request.tag != DER_SEQUENCE {
anyhow::bail!("timestamp request must be an outer DER sequence");
}
let mut fields = DerReader::new(request.value);
assert_eq!(parse_positive_integer_u8(fields.read_tlv()?)?, 1);
let message_imprint = fields.read_tlv()?;
assert_eq!(message_imprint.tag, DER_SEQUENCE);
let mut imprint_fields = DerReader::new(message_imprint.value);
let algorithm_identifier = imprint_fields.read_tlv()?;
assert_eq!(algorithm_identifier.tag, DER_SEQUENCE);
let mut algorithm_fields = DerReader::new(algorithm_identifier.value);
let hash_algorithm_oid = parse_oid(algorithm_fields.read_tlv()?)?;
let algorithm_null = algorithm_fields.read_tlv()?;
assert_eq!(algorithm_null.tag, DER_NULL);
assert!(algorithm_fields.is_finished());
let imprint = imprint_fields.read_tlv()?;
if imprint.tag != DER_OCTET_STRING || imprint.value.len() != 32 {
anyhow::bail!("timestamp request imprint must be a 32-byte OCTET STRING");
}
let mut message_imprint_sha256 = [0u8; 32];
message_imprint_sha256.copy_from_slice(imprint.value);
assert!(imprint_fields.is_finished());
let policy_oid = parse_oid(fields.read_tlv()?)?;
let nonce_hex = hex::encode(parse_positive_integer_bytes(fields.read_tlv()?)?);
let cert_req = fields.read_tlv()?;
if cert_req.tag != DER_BOOLEAN || cert_req.value != [0xff] {
anyhow::bail!("timestamp request certReq must be BOOLEAN TRUE");
}
assert!(fields.is_finished());
Ok(ParsedTimestampRequestForTest {
hash_algorithm_oid,
message_imprint_sha256,
policy_oid,
nonce_hex,
cert_req: true,
})
}
#[cfg(test)]
mod tests {
use base64::Engine as _;
use exo_avc::AvcReceiptEvidenceSubject;
use exo_core::{Hash256, Timestamp};
use super::*;
#[test]
fn minimal_unsigned_integer_bytes_strips_leading_zeros() {
assert_eq!(
minimal_unsigned_integer_bytes(&[0u8, 0, 0x12, 0x34]).to_vec(),
vec![0x12u8, 0x34]
);
assert_eq!(
minimal_unsigned_integer_bytes(&[0x80u8, 0x01]).to_vec(),
vec![0x80u8, 0x01]
);
assert_eq!(
minimal_unsigned_integer_bytes(&[0u8, 0, 0]).to_vec(),
vec![0u8]
);
}
#[test]
fn nonce_hex_matches_der_roundtrip_for_leading_zero_nonce() {
// Regression: a nonce whose first byte is 0x00. The DER INTEGER encoding
// strips the leading zero, so the request-side nonce_hex must use the same
// canonical (leading-zero-stripped) form that parse_positive_integer_bytes
// reconstructs from the TSA's echoed response. Before the fix the request
// stored hex of the full 32 bytes (with the 0x00), so the nonce-equality
// check failed and the whole emit fell through to a fail-closed 503.
let mut nonce = [0u8; 32];
nonce[1] = 0x12;
nonce[31] = 0x34;
let request_nonce_hex = hex::encode(minimal_unsigned_integer_bytes(&nonce));
let der = der_integer_from_positive_bytes(&nonce);
let mut reader = DerReader::new(&der);
let tlv = reader.read_tlv().expect("nonce DER integer");
let parsed = parse_positive_integer_bytes(tlv).expect("parse nonce integer");
assert_eq!(
request_nonce_hex,
hex::encode(parsed),
"request nonce_hex must equal the canonical value parsed back from the echoed DER integer"
);
}
const MICROSOFT_FIXTURE_RESPONSE_DER_BASE64: &str =
include_str!("fixtures/microsoft_rfc3161_timestamp_response.b64");
const MICROSOFT_FIXTURE_IMPRINT_HEX: &str =
"891d95ab4a3aedc63c9c32b800ad15679ecd94917eb35967004f9882ac6ae69a";
const MICROSOFT_FIXTURE_NONCE_HEX: &str = "a173ce171bc853e8";
const MICROSOFT_FIXTURE_SIGNER_SPKI_HEX: &str = "30820222300d06092a864886f70d01010105000382020f003082020a0282020100b4a59f9bfba5d36eff77c4656fc327fe0d1052fbcba98d95b32ded23c536b454aca53668999383dc11d3f0b911f91ae130981bd558c0285372b1a2bd70b49789f3c648806b3c282cf4fe32db896b2449ab57a439cf8066a8c8483eb66112f6675a9092e073bb8d849e8bf9f1982effd44afe9792e0dcf992c5bf1dd8855c011c52c350789b107a5c8d2791e97dc1ad5d61bdb07c6a687eb6859b164ec53f5e361b782c7d1105256e79b6ba64da634bfd20b5f9bbaa2222c8fea9e8f4734d36cc9d5aac1e757f77fad6d331f1f90f90359e7052a2a64d9241f6153ce77fb6a57e6b0df2b7dae358f7f5813809b36ea82911d4246e231abd43325034a19b2708be01dd4274b6d3bb138fc33e9092f7b4e75a84fb8fa8cc2c6820a075fc30431d0ef5329eec54af6c0118b3502795d0a5fca1c6642395bd436a8f22f5d092ded3ff860fdff29ea5c6585a573a36ae9ef67f70a44e8633783397bac71d1bda68aa70f8a2e3f8a2d9985e29a9652444fb08a96915286cdf0ca0e85fdfa2343142f3e76d60f8372c7a9618d68f09a82dcc7ac351520ad6af2c2972df704b452953538a8a53169af1ded837b12aa67f573b4498d2e98ebca157ad61fbaf197ef626a2722b5d9d34e4b009d18ef7a474a4f7960ee544c7e67d953cbd73623745182734fd123aa3466d2e37f874a17c4f84d7cf62a7856f23d7186c73698533eb3c77a9370203010001";
const MICROSOFT_FIXTURE_CA_SPKI_HEX: &str = "30820222300d06092a864886f70d01010105000382020f003082020a02820201009e7ce75263fde0c59f057d63b50622a31c1ed7e79733d11305bd6546477791c15d706f7fb2ab43970c4aa1521c6aa0dbfa89858a8e431c2e1105c6f24078d70b0324fe5dd3398b60a018f19c6fde5624b8b0ec7ccb8812abc660e3d44401fe61b9784891044a7b7431b3c4a0a74d8a1c0ce711afd2b1a87c9d6a39849335c739e446c14fbbaadf0c7799786d566b5c084af964a4e428a1350b166f34f59d1962543c2e9ee2e45f58722165c802b09faca337f911e1f92ab9459f1a6328a4dabf07c53fa5da199196506f1365a893a20468025a9c7af6e2aa2a14cf562de0544ae773faa2f9d47c036322033d243749e1ed2a883466e6c39388442d04b19df5585dd4c69dc6819c1eb442b12e6b3bdca1bf67e3247ae6950d042179a9e0384306278a50647e799e02344ddcb56e2ebd20d055e4a9f61d5268f57c51611fc93c601a33ac46979ec48bde47530f4d57fb82df2163ae1734f3ba8b2506b0482df1cd8fc45f3b13e08eec0dbc4e98cdab978b8a2ba784a6ead176e390da14e4986d614ae59806e9c518dbf6d4ab78376d002a66deb929c69ec04277672344a1bbf7e4d7fac4de85ac0ea317de38efe347bc28de58b09067733c9607827279e14c5b72417dd7802a1ce88457bc539c3d5aebdc3f513c708c4ba0a483cc20813aed2159d8f328dbbc6394b007596de5d421001632cd1dddc443bf4f52bf055177ad5ebd0203010001";
fn evidence_subject() -> AvcReceiptEvidenceSubject {
AvcReceiptEvidenceSubject {
credential_id: Hash256::from_bytes([0x11; 32]),
action_id: Hash256::from_bytes([0x22; 32]),
action_commitment_hash: Hash256::from_bytes([0x33; 32]),
action_descriptor_hash: Hash256::from_bytes([0x44; 32]),
previous_receipt_hash: Some(Hash256::from_bytes([0x55; 32])),
}
}
fn denied_timestamp_response() -> Vec<u8> {
vec![0x30, 0x05, 0x30, 0x03, 0x02, 0x01, 0x02]
}
fn microsoft_fixture_subject_hash() -> Hash256 {
Hash256::from_bytes([0x66; 32])
}
fn microsoft_fixture_response_der() -> Vec<u8> {
BASE64_STANDARD
.decode(MICROSOFT_FIXTURE_RESPONSE_DER_BASE64.trim())
.unwrap()
}
fn microsoft_fixture_message_imprint() -> [u8; 32] {
let bytes = hex::decode(MICROSOFT_FIXTURE_IMPRINT_HEX).unwrap();
let mut imprint = [0u8; 32];
imprint.copy_from_slice(&bytes);
imprint
}
fn verify_microsoft_fixture(response_der: &[u8]) -> anyhow::Result<Rfc3161VerifiedTimestamp> {
verify_timestamp_response(
response_der,
microsoft_fixture_subject_hash(),
microsoft_fixture_message_imprint(),
MICROSOFT_FIXTURE_NONCE_HEX,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX,
)
}
fn verify_microsoft_fixture_with_trust_anchors(
anchors: Rfc3161TrustAnchors,
) -> anyhow::Result<Rfc3161VerifiedTimestamp> {
verify_timestamp_response_with_trust_anchors(
µsoft_fixture_response_der(),
microsoft_fixture_subject_hash(),
microsoft_fixture_message_imprint(),
MICROSOFT_FIXTURE_NONCE_HEX,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
&anchors,
)
}
fn microsoft_fixture_signed_data() -> SignedData {
let response_der = microsoft_fixture_response_der();
let (_status, token_der) = parse_timestamp_response_status(&response_der).unwrap();
let content_info = ContentInfo::from_der(token_der).unwrap();
SignedData::from_der(&content_info.content.to_der().unwrap()).unwrap()
}
fn read_tlv_error(bytes: &[u8]) -> String {
let mut reader = DerReader::new(bytes);
match reader.read_tlv() {
Ok(_) => panic!("expected malformed DER to fail"),
Err(error) => error.to_string(),
}
}
fn single_tlv(bytes: &[u8]) -> DerTlv<'_> {
let mut reader = DerReader::new(bytes);
let tlv = reader.read_tlv().unwrap();
assert!(reader.is_finished());
tlv
}
fn strip_embedded_certificates(response_der: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut outer = DerReader::new(response_der);
let response = outer.read_tlv()?;
if response.tag != DER_SEQUENCE || !outer.is_finished() {
anyhow::bail!("test fixture response must be one DER sequence");
}
let mut response_fields = DerReader::new(response.value);
let status_info = response_fields.read_tlv()?;
let token = response_fields.read_tlv()?;
if !response_fields.is_finished() {
anyhow::bail!("test fixture response has trailing fields");
}
let stripped_token = strip_token_certificates(token.encoded)?;
let mut stripped_response = Vec::new();
stripped_response.extend_from_slice(status_info.encoded);
stripped_response.extend_from_slice(&stripped_token);
Ok(der_sequence(&stripped_response))
}
fn strip_token_certificates(token_der: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut outer = DerReader::new(token_der);
let content_info = outer.read_tlv()?;
if content_info.tag != DER_SEQUENCE || !outer.is_finished() {
anyhow::bail!("test fixture token must be one DER sequence");
}
let mut content_fields = DerReader::new(content_info.value);
let content_type = content_fields.read_tlv()?;
let explicit_content = content_fields.read_tlv()?;
if explicit_content.tag != 0xa0 || !content_fields.is_finished() {
anyhow::bail!("test fixture token content must be explicit signedData");
}
let mut explicit_fields = DerReader::new(explicit_content.value);
let signed_data = explicit_fields.read_tlv()?;
if signed_data.tag != DER_SEQUENCE || !explicit_fields.is_finished() {
anyhow::bail!("test fixture signedData must be one DER sequence");
}
let stripped_signed_data = strip_signed_data_certificates(signed_data.encoded)?;
let mut stripped_content_info = Vec::new();
stripped_content_info.extend_from_slice(content_type.encoded);
stripped_content_info.extend_from_slice(&der_tlv(0xa0, &stripped_signed_data));
Ok(der_sequence(&stripped_content_info))
}
fn strip_signed_data_certificates(signed_data_der: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut outer = DerReader::new(signed_data_der);
let signed_data = outer.read_tlv()?;
if signed_data.tag != DER_SEQUENCE || !outer.is_finished() {
anyhow::bail!("test fixture signedData must be one DER sequence");
}
let mut fields = DerReader::new(signed_data.value);
let mut stripped = Vec::new();
for _ in 0..3 {
stripped.extend_from_slice(fields.read_tlv()?.encoded);
}
let next = fields.read_tlv()?;
if next.tag != 0xa0 {
anyhow::bail!("test fixture signedData did not contain certificate set");
}
while !fields.is_finished() {
stripped.extend_from_slice(fields.read_tlv()?.encoded);
}
Ok(der_sequence(&stripped))
}
#[test]
fn request_generation_uses_sha256_deterministic_nonce_certs_and_exact_imprint() {
let subject = evidence_subject();
let request =
build_timestamp_request(&subject, MICROSOFT_ARTIFACT_SIGNING_POLICY_OID).unwrap();
let replay =
build_timestamp_request(&subject, MICROSOFT_ARTIFACT_SIGNING_POLICY_OID).unwrap();
let parsed = parse_timestamp_request_for_test(&request.der).unwrap();
assert_eq!(request.der, replay.der);
assert_eq!(
request.message_imprint_sha256,
subject.rfc3161_sha256_message_imprint().unwrap()
);
assert_eq!(parsed.hash_algorithm_oid, RFC3161_SHA256_ALGORITHM_OID);
assert_eq!(
parsed.message_imprint_sha256,
subject.rfc3161_sha256_message_imprint().unwrap()
);
assert_eq!(parsed.policy_oid, MICROSOFT_ARTIFACT_SIGNING_POLICY_OID);
assert_eq!(parsed.nonce_hex, request.nonce_hex);
assert!(
parsed.cert_req,
"RFC 3161 request must ask Microsoft to embed signing certificates"
);
}
#[test]
fn verifier_rejects_denied_and_malformed_timestamp_responses_before_receipt_storage() {
let subject = evidence_subject();
let request =
build_timestamp_request(&subject, MICROSOFT_ARTIFACT_SIGNING_POLICY_OID).unwrap();
let expected_spki = "30820122300d06092a864886f70d01010105000382010f";
let denied = verify_timestamp_response(
&denied_timestamp_response(),
subject.hash().unwrap(),
subject.rfc3161_sha256_message_imprint().unwrap(),
&request.nonce_hex,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
expected_spki,
)
.unwrap_err()
.to_string();
assert!(denied.contains("denied"));
let malformed = verify_timestamp_response(
&[0x30, 0x03, 0x02],
subject.hash().unwrap(),
subject.rfc3161_sha256_message_imprint().unwrap(),
&request.nonce_hex,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
expected_spki,
)
.unwrap_err()
.to_string();
assert!(malformed.contains("malformed"));
}
#[test]
fn verifier_accepts_microsoft_artifact_signing_fixture_with_pinned_signer_spki() {
let verified = verify_microsoft_fixture(µsoft_fixture_response_der()).unwrap();
assert_eq!(verified.subject_hash, microsoft_fixture_subject_hash());
assert_eq!(
verified.message_imprint_sha256_hex,
MICROSOFT_FIXTURE_IMPRINT_HEX
);
assert_eq!(verified.policy_oid, MICROSOFT_ARTIFACT_SIGNING_POLICY_OID);
assert_eq!(verified.serial_number_hex, "6a1c57054080");
assert_eq!(verified.nonce_hex, MICROSOFT_FIXTURE_NONCE_HEX);
assert_eq!(verified.issued_at, Timestamp::new(1_782_571_620_539, 0));
assert_eq!(
verified.tsa_public_key_spki_der_hex,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX
);
assert!(
verified
.tsa_subject
.contains("Microsoft Public RSA Time Stamping Authority")
);
assert!(!verified.token_der_base64.is_empty());
}
#[test]
fn verifier_accepts_microsoft_fixture_with_only_pinned_issuing_ca_spki() {
let anchors =
Rfc3161TrustAnchors::new(Vec::new(), vec![MICROSOFT_FIXTURE_CA_SPKI_HEX.to_owned()])
.unwrap();
let verified = verify_microsoft_fixture_with_trust_anchors(anchors).unwrap();
assert_eq!(
verified.tsa_public_key_spki_der_hex,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX
);
assert_eq!(
verified.trust_anchor.kind,
Rfc3161TrustAnchorKind::IssuingCaSpki
);
assert_eq!(
verified.trust_anchor.spki_der_hex,
MICROSOFT_FIXTURE_CA_SPKI_HEX
);
assert!(
verified
.trust_anchor
.subject
.contains("Microsoft Public RSA Timestamping CA 2020")
);
}
#[test]
fn verifier_records_direct_signer_pin_as_signer_trust_anchor() {
let anchors = Rfc3161TrustAnchors::new(
vec![MICROSOFT_FIXTURE_SIGNER_SPKI_HEX.to_owned()],
Vec::new(),
)
.unwrap();
let verified = verify_microsoft_fixture_with_trust_anchors(anchors).unwrap();
assert_eq!(
verified.trust_anchor.kind,
Rfc3161TrustAnchorKind::SignerSpki
);
assert_eq!(
verified.trust_anchor.spki_der_hex,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX
);
assert_eq!(verified.trust_anchor.subject, verified.tsa_subject);
}
#[test]
fn verifier_prefers_direct_signer_pin_when_leaf_and_ca_anchors_both_match() {
let anchors = Rfc3161TrustAnchors::new(
vec![MICROSOFT_FIXTURE_SIGNER_SPKI_HEX.to_owned()],
vec![MICROSOFT_FIXTURE_CA_SPKI_HEX.to_owned()],
)
.unwrap();
let verified = verify_microsoft_fixture_with_trust_anchors(anchors).unwrap();
assert_eq!(
verified.trust_anchor.kind,
Rfc3161TrustAnchorKind::SignerSpki
);
assert_eq!(
verified.trust_anchor.spki_der_hex,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX
);
assert_eq!(verified.trust_anchor.subject, verified.tsa_subject);
}
#[test]
fn verifier_inspection_without_pins_records_signer_identity_without_falling_open_for_runtime() {
let verified = inspect_timestamp_response_without_spki_pin(
µsoft_fixture_response_der(),
microsoft_fixture_subject_hash(),
microsoft_fixture_message_imprint(),
MICROSOFT_FIXTURE_NONCE_HEX,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
)
.unwrap();
assert_eq!(
verified.trust_anchor.kind,
Rfc3161TrustAnchorKind::SignerSpki
);
assert_eq!(
verified.trust_anchor.spki_der_hex,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX
);
assert_eq!(verified.trust_anchor.subject, verified.tsa_subject);
}
#[test]
fn trust_anchor_config_canonicalizes_case_and_rejects_cross_set_duplicates() {
let uppercase_signer = MICROSOFT_FIXTURE_SIGNER_SPKI_HEX.to_ascii_uppercase();
let anchors = Rfc3161TrustAnchors::new(vec![uppercase_signer], Vec::new()).unwrap();
assert_eq!(
anchors.signer_spki_der_hexes(),
&[MICROSOFT_FIXTURE_SIGNER_SPKI_HEX.to_owned()]
);
let duplicate = Rfc3161TrustAnchors::new(
vec![MICROSOFT_FIXTURE_CA_SPKI_HEX.to_owned()],
vec![MICROSOFT_FIXTURE_CA_SPKI_HEX.to_owned()],
)
.unwrap_err()
.to_string();
assert!(duplicate.contains("duplicates an existing trust anchor"));
}
#[test]
fn verifier_rejects_unrelated_trust_anchors_without_falling_open() {
let anchors = Rfc3161TrustAnchors::new(
Vec::new(),
vec!["30820122300d06092a864886f70d01010105000382010f".to_owned()],
)
.unwrap();
let err = verify_microsoft_fixture_with_trust_anchors(anchors)
.unwrap_err()
.to_string();
assert!(
err.contains("trust anchor"),
"unrelated CA pins must fail closed with trust-anchor context, got: {err}"
);
}
#[test]
fn trust_anchor_config_requires_at_least_one_non_empty_pin_set() {
let err = Rfc3161TrustAnchors::new(Vec::new(), Vec::new())
.unwrap_err()
.to_string();
assert!(err.contains("at least one RFC 3161 trust anchor"));
}
#[test]
fn chain_policy_requires_timestamping_leaf_and_real_ca_issuer() {
let signed_data = microsoft_fixture_signed_data();
let certificate_refs = certificate_set(&signed_data).unwrap();
let issued_at = Timestamp::new(1_782_571_620_539, 0);
let signer = certificate_refs
.iter()
.copied()
.find(|cert| {
cert.tbs_certificate()
.subject()
.to_string()
.contains("Microsoft Public RSA Time Stamping Authority")
})
.unwrap();
let issuing_ca = certificate_refs
.iter()
.copied()
.find(|cert| {
cert.tbs_certificate()
.subject()
.to_string()
.contains("Microsoft Public RSA Timestamping CA 2020")
})
.unwrap();
validate_tsa_signer_certificate(signer, issued_at).unwrap();
validate_tsa_issuing_ca_certificate(issuing_ca, issued_at).unwrap();
let signer_as_ca = validate_tsa_issuing_ca_certificate(signer, issued_at)
.unwrap_err()
.to_string();
assert!(signer_as_ca.contains("CA:TRUE"));
let ca_as_signer = validate_tsa_signer_certificate(issuing_ca, issued_at)
.unwrap_err()
.to_string();
assert!(ca_as_signer.contains("must not assert CA:TRUE"));
}
#[test]
fn certificate_policy_rejects_out_of_validity_window_and_unsupported_signature_algorithms() {
let signed_data = microsoft_fixture_signed_data();
let certificate_refs = certificate_set(&signed_data).unwrap();
let signer = certificate_refs
.iter()
.copied()
.find(|cert| {
cert.tbs_certificate()
.subject()
.to_string()
.contains("Microsoft Public RSA Time Stamping Authority")
})
.unwrap();
let not_yet_valid = validate_tsa_signer_certificate(signer, Timestamp::new(0, 0))
.unwrap_err()
.to_string();
assert!(not_yet_valid.contains("not valid"));
let expired = validate_tsa_signer_certificate(signer, Timestamp::new(4_102_444_800_000, 0))
.unwrap_err()
.to_string();
assert!(expired.contains("expired"));
assert!(rsa_verification_params_for_oid(SHA256_WITH_RSA_ENCRYPTION_OID).is_ok());
assert!(rsa_verification_params_for_oid(SHA384_WITH_RSA_ENCRYPTION_OID).is_ok());
assert!(rsa_verification_params_for_oid(SHA512_WITH_RSA_ENCRYPTION_OID).is_ok());
let unsupported = rsa_verification_params_for_oid(RSA_ENCRYPTION_OID)
.unwrap_err()
.to_string();
assert!(unsupported.contains("not RSA-PKCS1 SHA-256/384/512"));
}
#[test]
fn verifier_rejects_fixture_with_wrong_nonce_imprint_policy_missing_cert_pin_or_signature() {
let response_der = microsoft_fixture_response_der();
let wrong_nonce = verify_timestamp_response(
&response_der,
microsoft_fixture_subject_hash(),
microsoft_fixture_message_imprint(),
"a173ce171bc853e9",
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX,
)
.unwrap_err()
.to_string();
assert!(wrong_nonce.contains("nonce"));
let mut wrong_imprint = microsoft_fixture_message_imprint();
wrong_imprint[0] ^= 0x01;
let wrong_imprint = verify_timestamp_response(
&response_der,
microsoft_fixture_subject_hash(),
wrong_imprint,
MICROSOFT_FIXTURE_NONCE_HEX,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX,
)
.unwrap_err()
.to_string();
assert!(wrong_imprint.contains("message imprint"));
let wrong_policy = verify_timestamp_response(
&response_der,
microsoft_fixture_subject_hash(),
microsoft_fixture_message_imprint(),
MICROSOFT_FIXTURE_NONCE_HEX,
"1.2.3.4",
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX,
)
.unwrap_err()
.to_string();
assert!(wrong_policy.contains("policy"));
let missing_cert =
verify_microsoft_fixture(&strip_embedded_certificates(&response_der).unwrap())
.unwrap_err()
.to_string();
assert!(missing_cert.contains("certificate"));
let wrong_pin = verify_timestamp_response(
&response_der,
microsoft_fixture_subject_hash(),
microsoft_fixture_message_imprint(),
MICROSOFT_FIXTURE_NONCE_HEX,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
"30820122300d06092a864886f70d01010105000382010f",
)
.unwrap_err()
.to_string();
assert!(wrong_pin.contains("trust anchor"));
let signer_trust_anchors = Rfc3161TrustAnchors::new(
vec![
"30820122300d06092a864886f70d01010105000382010f".to_owned(),
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX.to_owned(),
],
Vec::new(),
)
.unwrap();
let accepted_by_second_pin = verify_timestamp_response_with_trust_anchors(
&response_der,
microsoft_fixture_subject_hash(),
microsoft_fixture_message_imprint(),
MICROSOFT_FIXTURE_NONCE_HEX,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
&signer_trust_anchors,
)
.unwrap();
assert_eq!(
accepted_by_second_pin.tsa_public_key_spki_der_hex,
MICROSOFT_FIXTURE_SIGNER_SPKI_HEX
);
let empty_pin_set = Rfc3161TrustAnchors::new(Vec::new(), Vec::new())
.unwrap_err()
.to_string();
assert!(empty_pin_set.contains("at least one RFC 3161 trust anchor"));
let mut bad_signature = response_der;
*bad_signature.last_mut().unwrap() ^= 0x01;
let bad_signature = verify_microsoft_fixture(&bad_signature)
.unwrap_err()
.to_string();
assert!(bad_signature.contains("signature"));
}
#[test]
fn der_parser_rejects_non_canonical_rfc3161_primitives() {
assert!(read_tlv_error(&[]).contains("missing tag"));
assert!(read_tlv_error(&[DER_SEQUENCE, 0x80]).contains("indefinite length"));
assert!(
read_tlv_error(&[DER_SEQUENCE, 0x89, 0, 0, 0, 0, 0, 0, 0, 0, 1])
.contains("length uses")
);
assert!(read_tlv_error(&[DER_SEQUENCE, 0x02, 0x00]).contains("overruns"));
assert!(
der_oid("3.1")
.unwrap_err()
.to_string()
.contains("first arc")
);
assert!(
der_oid("1.40")
.unwrap_err()
.to_string()
.contains("second arc")
);
assert!(
der_oid("1.bad")
.unwrap_err()
.to_string()
.contains("invalid OID arc")
);
assert!(
parse_oid_value(&[0x80])
.unwrap_err()
.to_string()
.contains("truncated")
);
assert!(
parse_oid(DerTlv {
tag: DER_INTEGER,
value: &[1],
encoded: &[DER_INTEGER, 1, 1],
})
.unwrap_err()
.to_string()
.contains("expected OID")
);
assert!(
parse_positive_integer_bytes(DerTlv {
tag: DER_OCTET_STRING,
value: &[1],
encoded: &[DER_OCTET_STRING, 1, 1],
})
.unwrap_err()
.to_string()
.contains("expected INTEGER")
);
assert!(
parse_positive_integer_bytes(DerTlv {
tag: DER_INTEGER,
value: &[],
encoded: &[DER_INTEGER, 0],
})
.unwrap_err()
.to_string()
.contains("empty")
);
assert!(
parse_positive_integer_bytes(DerTlv {
tag: DER_INTEGER,
value: &[0x80],
encoded: &[DER_INTEGER, 1, 0x80],
})
.unwrap_err()
.to_string()
.contains("negative")
);
assert!(
parse_positive_integer_u8(DerTlv {
tag: DER_INTEGER,
value: &[0x01, 0x00],
encoded: &[DER_INTEGER, 2, 1, 0],
})
.unwrap_err()
.to_string()
.contains("one byte")
);
assert!(
canonical_hex("zz", "test hex")
.unwrap_err()
.to_string()
.contains("not hex")
);
assert!(
canonical_hex("", "test hex")
.unwrap_err()
.to_string()
.contains("must not be empty")
);
assert!(
der_positive_integer_bytes(&[], "modulus")
.unwrap_err()
.to_string()
.contains("empty")
);
assert!(
der_positive_integer_bytes(&[0x80], "modulus")
.unwrap_err()
.to_string()
.contains("negative")
);
assert!(
der_positive_integer_bytes(&[0], "modulus")
.unwrap_err()
.to_string()
.contains("zero")
);
assert_eq!(
der_positive_integer_bytes(&[0, 0, 1], "modulus").unwrap(),
vec![1]
);
let status_info = der_sequence(&der_integer_from_u8(0));
let mut response_with_trailing_field = Vec::new();
response_with_trailing_field.extend_from_slice(&status_info);
response_with_trailing_field.extend_from_slice(&der_null());
response_with_trailing_field.extend_from_slice(&der_null());
assert!(
parse_timestamp_response_status(&der_sequence(&response_with_trailing_field))
.unwrap_err()
.to_string()
.contains("trailing fields")
);
assert!(
parse_generalized_time_ms(b"20260627120000")
.unwrap_err()
.to_string()
.contains("end in Z")
);
assert!(
parse_generalized_time_ms(b"20260627120000.badZ")
.unwrap_err()
.to_string()
.contains("fractional seconds")
);
assert!(
parse_generalized_time_ms(b"20261327120000Z")
.unwrap_err()
.to_string()
.contains("date")
);
assert!(
parse_generalized_time_ms(b"20260627250000Z")
.unwrap_err()
.to_string()
.contains("clock")
);
assert_eq!(
parse_generalized_time_ms(b"20260627120000.7Z").unwrap(),
1_782_561_600_700
);
assert_eq!(
parse_generalized_time_ms(b"20260627120000.7899Z").unwrap(),
1_782_561_600_789
);
}
#[test]
fn message_imprint_parser_rejects_ambiguous_algorithm_and_imprint_shapes() {
let imprint = [0x7a; 32];
let algorithm_without_params =
der_sequence(&der_oid(RFC3161_SHA256_ALGORITHM_OID).unwrap());
let mut valid_without_params = Vec::new();
valid_without_params.extend_from_slice(&algorithm_without_params);
valid_without_params.extend_from_slice(&der_tlv(DER_OCTET_STRING, &imprint));
assert_eq!(
parse_message_imprint(single_tlv(&der_sequence(&valid_without_params))).unwrap(),
imprint
);
let wrong_algorithm = der_algorithm_identifier("1.2.3.4").unwrap();
let mut wrong_algorithm_imprint = Vec::new();
wrong_algorithm_imprint.extend_from_slice(&wrong_algorithm);
wrong_algorithm_imprint.extend_from_slice(&der_tlv(DER_OCTET_STRING, &imprint));
assert!(
parse_message_imprint(single_tlv(&der_sequence(&wrong_algorithm_imprint)))
.unwrap_err()
.to_string()
.contains("not SHA-256")
);
let mut non_null_algorithm = Vec::new();
non_null_algorithm.extend_from_slice(&der_oid(RFC3161_SHA256_ALGORITHM_OID).unwrap());
non_null_algorithm.extend_from_slice(&der_tlv(DER_OCTET_STRING, &[0]));
let mut non_null_imprint = Vec::new();
non_null_imprint.extend_from_slice(&der_sequence(&non_null_algorithm));
non_null_imprint.extend_from_slice(&der_tlv(DER_OCTET_STRING, &imprint));
assert!(
parse_message_imprint(single_tlv(&der_sequence(&non_null_imprint)))
.unwrap_err()
.to_string()
.contains("parameters")
);
let mut trailing_algorithm = Vec::new();
trailing_algorithm.extend_from_slice(&der_oid(RFC3161_SHA256_ALGORITHM_OID).unwrap());
trailing_algorithm.extend_from_slice(&der_null());
trailing_algorithm.extend_from_slice(&der_null());
let mut trailing_algorithm_imprint = Vec::new();
trailing_algorithm_imprint.extend_from_slice(&der_sequence(&trailing_algorithm));
trailing_algorithm_imprint.extend_from_slice(&der_tlv(DER_OCTET_STRING, &imprint));
assert!(
parse_message_imprint(single_tlv(&der_sequence(&trailing_algorithm_imprint)))
.unwrap_err()
.to_string()
.contains("trailing fields")
);
let mut short_imprint = Vec::new();
short_imprint.extend_from_slice(&algorithm_without_params);
short_imprint.extend_from_slice(&der_tlv(DER_OCTET_STRING, &[0x7a; 31]));
assert!(
parse_message_imprint(single_tlv(&der_sequence(&short_imprint)))
.unwrap_err()
.to_string()
.contains("32-byte")
);
let mut trailing_imprint = valid_without_params;
trailing_imprint.extend_from_slice(&der_null());
assert!(
parse_message_imprint(single_tlv(&der_sequence(&trailing_imprint)))
.unwrap_err()
.to_string()
.contains("messageImprint has trailing")
);
assert!(
parse_generalized_time_ms(b"2026062712000AZ")
.unwrap_err()
.to_string()
.contains("YYYYMMDDHHMMSSZ")
);
assert!(
parse_generalized_time_ms(b"19691231235959Z")
.unwrap_err()
.to_string()
.contains("predates Unix epoch")
);
}
#[tokio::test]
#[ignore = "live Microsoft TSA preflight; set EXO_AVC_RFC3161_LIVE_PREFLIGHT=1"]
async fn live_microsoft_rfc3161_preflight_extracts_and_verifies_pinned_spki() {
if std::env::var("EXO_AVC_RFC3161_LIVE_PREFLIGHT")
.ok()
.as_deref()
!= Some("1")
{
eprintln!(
"skipping live Microsoft RFC 3161 preflight; set EXO_AVC_RFC3161_LIVE_PREFLIGHT=1"
);
return;
}
let subject = evidence_subject();
let request =
build_timestamp_request(&subject, MICROSOFT_ARTIFACT_SIGNING_POLICY_OID).unwrap();
let response = reqwest::Client::new()
.post(MICROSOFT_ARTIFACT_SIGNING_TIMESTAMP_URL)
.header("Content-Type", "application/timestamp-query")
.header("Accept", "application/timestamp-reply")
.body(request.der)
.send()
.await
.unwrap();
assert!(
response.status().is_success(),
"Microsoft TSA returned {}",
response.status()
);
let response_der = response.bytes().await.unwrap();
let verified = inspect_timestamp_response_without_spki_pin(
&response_der,
subject.hash().unwrap(),
request.message_imprint_sha256,
&request.nonce_hex,
MICROSOFT_ARTIFACT_SIGNING_POLICY_OID,
)
.unwrap();
if let Ok(existing_pin) =
std::env::var(crate::avc::AVC_EXTERNAL_TIMESTAMP_AUTHORITY_PUBLIC_KEY_HEX_ENV)
{
let configured_pins = existing_pin
.split(',')
.map(|pin| {
canonical_hex(
pin.trim(),
crate::avc::AVC_EXTERNAL_TIMESTAMP_AUTHORITY_PUBLIC_KEY_HEX_ENV,
)
.unwrap()
})
.collect::<Vec<_>>();
assert!(
configured_pins.contains(&verified.tsa_public_key_spki_der_hex),
"live Microsoft TSA signer SPKI was not present in configured pin set"
);
}
println!(
"{}={}",
crate::avc::AVC_EXTERNAL_TIMESTAMP_AUTHORITY_KIND_ENV,
crate::avc::AVC_EXTERNAL_TIMESTAMP_AUTHORITY_KIND_RFC3161
);
println!(
"{}={}",
crate::avc::AVC_EXTERNAL_TIMESTAMP_AUTHORITY_URL_ENV,
MICROSOFT_ARTIFACT_SIGNING_TIMESTAMP_URL
);
println!(
"{}=did:exo:microsoft-public-rsa-tsa",
crate::avc::AVC_EXTERNAL_TIMESTAMP_AUTHORITY_DID_ENV
);
println!(
"{}={}",
crate::avc::AVC_EXTERNAL_TIMESTAMP_AUTHORITY_PUBLIC_KEY_HEX_ENV,
verified.tsa_public_key_spki_der_hex
);
println!(
"{}={}",
crate::avc::AVC_RFC3161_TIMESTAMP_POLICY_OID_ENV,
verified.policy_oid
);
println!(
"{}=true",
crate::avc::AVC_REQUIRE_EXTERNAL_TIMESTAMP_AUTHORITY_ENV
);
println!("RFC3161_TSA_SUBJECT={}", verified.tsa_subject);
println!("RFC3161_SERIAL_NUMBER={}", verified.serial_number_hex);
println!("RFC3161_NONCE={}", verified.nonce_hex);
println!(
"RFC3161_MESSAGE_IMPRINT_SHA256={}",
verified.message_imprint_sha256_hex
);
}
}