use const_oid::ObjectIdentifier;
use der::asn1::OctetString;
use der::{Decode, Encode};
use spki::AlgorithmIdentifierOwned;
use x509_cert::Certificate;
use crate::crypto::algorithm::DigestAlgorithm;
use crate::der_utils;
use crate::error::{TrustError, TspError};
use crate::trust::TrustStore;
pub const ID_CT_TST_INFO: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.1.4");
pub const ID_SIGNED_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.2");
const ID_CONTENT_TYPE_ATTR: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.3");
const ID_MESSAGE_DIGEST_ATTR: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.4");
const ID_CE_EXT_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37");
const ID_CE_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
const ID_KP_TIME_STAMPING: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.8");
const OID_RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const OID_EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
const OID_RSASSA_PSS: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.10");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PkiStatus {
Granted,
GrantedWithMods,
Rejection,
Waiting,
RevocationWarning,
RevocationNotification,
Unknown(u64),
}
impl PkiStatus {
fn from_u64(v: u64) -> Self {
match v {
0 => Self::Granted,
1 => Self::GrantedWithMods,
2 => Self::Rejection,
3 => Self::Waiting,
4 => Self::RevocationWarning,
5 => Self::RevocationNotification,
_ => Self::Unknown(v),
}
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Granted | Self::GrantedWithMods)
}
}
impl std::fmt::Display for PkiStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Granted => write!(f, "granted (0)"),
Self::GrantedWithMods => write!(f, "grantedWithMods (1)"),
Self::Rejection => write!(f, "rejection (2)"),
Self::Waiting => write!(f, "waiting (3)"),
Self::RevocationWarning => write!(f, "revocationWarning (4)"),
Self::RevocationNotification => write!(f, "revocationNotification (5)"),
Self::Unknown(v) => write!(f, "unknown ({v})"),
}
}
}
pub fn build_timestamp_request(
digest_algorithm: DigestAlgorithm,
message_hash: &[u8],
policy_oid: Option<&ObjectIdentifier>,
nonce: Option<u64>,
cert_req: bool,
) -> Result<Vec<u8>, TspError> {
let mut parts: Vec<Vec<u8>> = Vec::new();
parts.push(der_utils::encode_integer_u64(1));
let hash_alg = digest_algorithm_identifier(digest_algorithm);
let hash_alg_der = hash_alg
.to_der()
.map_err(|e| TspError::InvalidResponse(format!("failed to encode hash algorithm: {e}")))?;
let hashed_message = OctetString::new(message_hash.to_vec()).map_err(|e| {
TspError::InvalidResponse(format!("failed to create hash octet string: {e}"))
})?;
let hashed_message_der = hashed_message
.to_der()
.map_err(|e| TspError::InvalidResponse(format!("failed to encode hash: {e}")))?;
let msg_imprint = der_utils::encode_sequence_from_parts(&[&hash_alg_der, &hashed_message_der]);
parts.push(msg_imprint);
if let Some(oid) = policy_oid {
let oid_der = oid
.to_der()
.map_err(|e| TspError::InvalidResponse(format!("failed to encode policy OID: {e}")))?;
parts.push(oid_der);
}
if let Some(n) = nonce {
parts.push(der_utils::encode_integer_u64(n));
}
if cert_req {
parts.push(der_utils::encode_boolean(true));
}
let body: Vec<u8> = parts.iter().flat_map(|p| p.iter().copied()).collect();
Ok(der_utils::encode_sequence_raw(&body))
}
#[derive(Debug)]
pub struct TimeStampResp {
pub status: PkiStatus,
pub status_string: Option<String>,
pub failure_info: Option<Vec<u8>>,
pub token_der: Option<Vec<u8>>,
}
pub fn parse_timestamp_response(der_bytes: &[u8]) -> Result<TimeStampResp, TspError> {
let (tag, resp_body) = der_utils::parse_tlv(der_bytes)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse TimeStampResp: {e}")))?;
if tag != 0x30 {
return Err(TspError::InvalidResponse(format!(
"expected SEQUENCE tag 0x30, got 0x{tag:02x}"
)));
}
let (status_tag, status_body, rest) = der_utils::parse_tlv_with_rest(&resp_body)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse PKIStatusInfo: {e}")))?;
if status_tag != 0x30 {
return Err(TspError::InvalidResponse(format!(
"expected PKIStatusInfo SEQUENCE, got 0x{status_tag:02x}"
)));
}
let (int_tag, int_body, status_rest) = der_utils::parse_tlv_with_rest(status_body)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse PKIStatus: {e}")))?;
if int_tag != 0x02 {
return Err(TspError::InvalidResponse(format!(
"expected INTEGER tag 0x02 for PKIStatus, got 0x{int_tag:02x}"
)));
}
let status_val = der_utils::decode_integer_u64(int_body)
.map_err(|e| TspError::InvalidResponse(format!("failed to decode PKIStatus: {e}")))?;
let status = PkiStatus::from_u64(status_val);
let mut status_string = None;
let mut failure_info = None;
let mut remaining = status_rest;
while !remaining.is_empty() {
let (stag, sbody, srest) = der_utils::parse_tlv_with_rest(remaining).map_err(|e| {
TspError::InvalidResponse(format!("malformed PKIStatusInfo field: {e}"))
})?;
match stag {
0x30 => {
if sbody.is_empty() {
return Err(TspError::InvalidResponse(
"statusString PKIFreeText SEQUENCE is empty (violates SIZE (1..MAX))"
.into(),
));
}
let mut elems = sbody;
while !elems.is_empty() {
let (inner_tag, inner_body, inner_rest) = der_utils::parse_tlv_with_rest(elems)
.map_err(|e| {
TspError::InvalidResponse(format!("malformed statusString: {e}"))
})?;
if inner_tag != 0x0C {
return Err(TspError::InvalidResponse(format!(
"statusString element is not a UTF8String (tag 0x0c), got 0x{inner_tag:02x}"
)));
}
let text = std::str::from_utf8(inner_body).map_err(|e| {
TspError::InvalidResponse(format!("statusString is not valid UTF-8: {e}"))
})?;
if status_string.is_none() {
status_string = Some(text.to_string());
}
elems = inner_rest;
}
}
0x03 => {
failure_info = Some(sbody.to_vec());
}
_ => {}
}
remaining = srest;
}
let token_der = if !rest.is_empty() {
let (token_tag, _, after) = der_utils::parse_tlv_with_rest(rest)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse token TLV: {e}")))?;
if token_tag != 0x30 {
return Err(TspError::InvalidResponse(format!(
"expected TimeStampToken SEQUENCE (0x30), got 0x{token_tag:02x}"
)));
}
if !after.is_empty() {
return Err(TspError::InvalidResponse(
"trailing data after TimeStampToken in TimeStampResp".into(),
));
}
Some(rest.to_vec())
} else {
None
};
Ok(TimeStampResp {
status,
status_string,
failure_info,
token_der,
})
}
pub fn validate_timestamp_response(
resp: &TimeStampResp,
expected_hash: &[u8],
expected_nonce: Option<u64>,
digest_algorithm: DigestAlgorithm,
extra_certs: &[Certificate],
) -> Result<Vec<u8>, TspError> {
if !resp.status.is_success() {
let msg = match &resp.status_string {
Some(s) => format!("status={}, message={s}", resp.status),
None => format!("status={}", resp.status),
};
return Err(TspError::TsaError(msg));
}
let token_der = resp.token_der.as_ref().ok_or_else(|| {
TspError::InvalidResponse("no token in response despite success status".into())
})?;
let (_verified, tst_info) = verify_token_cms(token_der, extra_certs)?;
check_tst_info_matches(&tst_info, expected_hash, expected_nonce, digest_algorithm)?;
Ok(token_der.clone())
}
pub fn verify_timestamp_token(
token_der: &[u8],
expected_hash: &[u8],
digest_algorithm: DigestAlgorithm,
expected_nonce: Option<u64>,
trust_store: Option<&TrustStore>,
validation_time: Option<der::DateTime>,
extra_certs: &[Certificate],
) -> Result<TstInfo, TspError> {
let (verified, tst_info) = verify_token_cms(token_der, extra_certs)?;
check_tst_info_matches(&tst_info, expected_hash, expected_nonce, digest_algorithm)?;
if let Some(store) = trust_store {
let chain = order_chain(
&verified.signer,
&verified.embedded,
store.signature_policy(),
);
let effective_time = match validation_time {
Some(t) => Some(t),
None => Some(gen_time_datetime(&tst_info)?),
};
#[cfg(feature = "ltv")]
let chain_result = store.verify_chain_for_purpose(
&chain,
effective_time,
crate::ltv::CertRole::TimestampSigner,
);
#[cfg(not(feature = "ltv"))]
let chain_result = store.verify_chain(&chain, effective_time);
chain_result.map_err(|e| {
TspError::VerificationFailed(format!("TSA certificate chain validation failed: {e}"))
})?;
}
Ok(tst_info)
}
struct VerifiedToken {
signer: Certificate,
embedded: Vec<Certificate>,
}
fn verify_token_cms(
token_der: &[u8],
extra_certs: &[Certificate],
) -> Result<(VerifiedToken, TstInfo), TspError> {
use cms::cert::CertificateChoices;
use cms::content_info::ContentInfo;
use cms::signed_data::SignedData;
let ci = ContentInfo::from_der(token_der)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse ContentInfo: {e}")))?;
if ci.content_type != ID_SIGNED_DATA {
return Err(TspError::InvalidResponse(format!(
"ContentInfo contentType is not id-signedData (got {})",
ci.content_type
)));
}
let signed_data: SignedData = ci
.content
.decode_as()
.map_err(|e| TspError::InvalidResponse(format!("failed to parse SignedData: {e}")))?;
let eci = &signed_data.encap_content_info;
if eci.econtent_type != ID_CT_TST_INFO {
return Err(TspError::InvalidResponse(format!(
"encapContentInfo eContentType is not id-ct-TSTInfo (got {})",
eci.econtent_type
)));
}
let econtent = eci.econtent.as_ref().ok_or_else(|| {
TspError::InvalidResponse("SignedData has no encapsulated content".into())
})?;
let tst_info_der = econtent.value().to_vec();
let signer_infos = &signed_data.signer_infos.0;
if signer_infos.len() != 1 {
return Err(TspError::InvalidResponse(format!(
"expected exactly one SignerInfo, found {}",
signer_infos.len()
)));
}
let signer_info = signer_infos.iter().next().expect("checked len == 1");
let signed_attrs = signer_info
.signed_attrs
.as_ref()
.ok_or_else(|| TspError::VerificationFailed("SignerInfo has no signedAttrs".into()))?;
let mut embedded: Vec<Certificate> = Vec::new();
if let Some(cert_set) = &signed_data.certificates {
for choice in cert_set.0.iter() {
if let CertificateChoices::Certificate(cert) = choice {
embedded.push(cert.clone());
}
}
}
for cert in extra_certs {
if !embedded
.iter()
.any(|c| c.tbs_certificate == cert.tbs_certificate)
{
embedded.push(cert.clone());
}
}
if embedded.is_empty() {
return Err(TspError::InvalidResponse(
"timestamp token contains no certificates and no extra_certs were supplied; \
request the token with certReq=true, or pass the TSA certificate via extra_certs"
.into(),
));
}
let signer = find_signer_cert(&signer_info.sid, &embedded).ok_or_else(|| {
TspError::VerificationFailed(
"signing certificate identified by SignerInfo not found in token or extra_certs".into(),
)
})?;
let digest_alg = DigestAlgorithm::from_oid(&signer_info.digest_alg.oid).ok_or_else(|| {
TspError::VerificationFailed(format!(
"unsupported SignerInfo digestAlgorithm OID: {}",
signer_info.digest_alg.oid
))
})?;
let content_type_attr =
find_attribute(signed_attrs, &ID_CONTENT_TYPE_ATTR)?.ok_or_else(|| {
TspError::VerificationFailed("signedAttrs missing content-type attribute".into())
})?;
let signed_content_type: ObjectIdentifier = content_type_attr.decode_as().map_err(|e| {
TspError::VerificationFailed(format!("invalid content-type attribute: {e}"))
})?;
if signed_content_type != ID_CT_TST_INFO {
return Err(TspError::VerificationFailed(format!(
"signed content-type is not id-ct-TSTInfo (got {signed_content_type})"
)));
}
let message_digest_attr =
find_attribute(signed_attrs, &ID_MESSAGE_DIGEST_ATTR)?.ok_or_else(|| {
TspError::VerificationFailed("signedAttrs missing message-digest attribute".into())
})?;
let signed_digest = message_digest_attr
.decode_as::<OctetString>()
.map_err(|e| {
TspError::VerificationFailed(format!(
"message-digest attribute is not an OCTET STRING: {e}"
))
})?;
let computed_digest = digest_alg.digest(&tst_info_der)?;
if signed_digest.as_bytes() != computed_digest.as_slice() {
return Err(TspError::VerificationFailed(
"message-digest signed attribute does not match the TSTInfo content".into(),
));
}
let signed_attrs_der = signed_attrs.to_der().map_err(|e| {
TspError::VerificationFailed(format!("failed to re-encode signedAttrs: {e}"))
})?;
let spki_der = signer
.tbs_certificate
.subject_public_key_info
.to_der()
.map_err(|e| TspError::VerificationFailed(format!("failed to encode signer SPKI: {e}")))?;
verify_cms_signature(
&signed_attrs_der,
signer_info.signature.as_bytes(),
&spki_der,
&signer_info.signature_algorithm,
digest_alg,
)
.map_err(|e| TspError::VerificationFailed(format!("TSA signature verification failed: {e}")))?;
require_timestamping_eku(&signer)?;
require_not_ca(&signer)?;
let tst_info = parse_tst_info_body(&tst_info_der)?;
check_gen_time_within_validity(&signer, &tst_info)?;
Ok((VerifiedToken { signer, embedded }, tst_info))
}
fn check_tst_info_matches(
tst_info: &TstInfo,
expected_hash: &[u8],
expected_nonce: Option<u64>,
digest_algorithm: DigestAlgorithm,
) -> Result<(), TspError> {
use subtle::ConstantTimeEq;
let hash_matches = tst_info.message_hash.len() == expected_hash.len()
&& bool::from(tst_info.message_hash.ct_eq(expected_hash));
if !hash_matches {
return Err(TspError::InvalidResponse(
"TSTInfo messageImprint hash does not match request".into(),
));
}
if tst_info.hash_algorithm != digest_algorithm {
return Err(TspError::InvalidResponse(format!(
"TSTInfo hash algorithm mismatch: expected {:?}, got {:?}",
digest_algorithm, tst_info.hash_algorithm,
)));
}
if let Some(expected) = expected_nonce {
match tst_info.nonce {
Some(actual) if bool::from(actual.ct_eq(&expected)) => {}
Some(actual) => {
return Err(TspError::InvalidResponse(format!(
"nonce mismatch: expected {expected}, got {actual}"
)));
}
None => {
return Err(TspError::InvalidResponse(
"expected nonce in TSTInfo but none present".into(),
));
}
}
}
Ok(())
}
fn find_signer_cert(
sid: &cms::signed_data::SignerIdentifier,
certs: &[Certificate],
) -> Option<Certificate> {
use cms::signed_data::SignerIdentifier;
match sid {
SignerIdentifier::IssuerAndSerialNumber(iasn) => certs
.iter()
.find(|c| {
c.tbs_certificate.issuer == iasn.issuer
&& c.tbs_certificate.serial_number == iasn.serial_number
})
.cloned(),
SignerIdentifier::SubjectKeyIdentifier(skid) => {
let want = skid.0.as_bytes();
certs
.iter()
.find(|c| cert_ski(c).as_deref() == Some(want))
.cloned()
}
}
}
fn cert_ski(cert: &Certificate) -> Option<Vec<u8>> {
let ski_oid = ObjectIdentifier::new_unwrap("2.5.29.14");
let exts = cert.tbs_certificate.extensions.as_ref()?;
let ext = exts.iter().find(|e| e.extn_id == ski_oid)?;
let (tag, body) = der_utils::parse_tlv(ext.extn_value.as_bytes()).ok()?;
if tag != 0x04 {
return None;
}
Some(body)
}
fn find_attribute<'a>(
attrs: &'a x509_cert::attr::Attributes,
oid: &ObjectIdentifier,
) -> Result<Option<&'a der::Any>, TspError> {
let mut matching = attrs.iter().filter(|attr| attr.oid == *oid);
let attr = match matching.next() {
Some(a) => a,
None => return Ok(None),
};
if matching.next().is_some() {
return Err(TspError::VerificationFailed(format!(
"duplicate signed attribute {oid}"
)));
}
if attr.values.len() != 1 {
return Err(TspError::VerificationFailed(format!(
"signed attribute {oid} must have exactly one value (has {})",
attr.values.len()
)));
}
Ok(attr.values.iter().next())
}
fn verify_cms_signature(
signed_attrs_der: &[u8],
signature: &[u8],
spki_der: &[u8],
signature_algorithm: &AlgorithmIdentifierOwned,
digest_alg: DigestAlgorithm,
) -> Result<(), TrustError> {
use crate::crypto::verify::verify_signature_by_oid;
use const_oid::db;
let sig_alg_oid = &signature_algorithm.oid;
if *sig_alg_oid == OID_RSASSA_PSS {
return verify_pss_signature(
signed_attrs_der,
signature,
spki_der,
signature_algorithm,
digest_alg,
);
}
let resolved = if *sig_alg_oid == OID_RSA_ENCRYPTION {
match digest_alg {
DigestAlgorithm::Sha256 => db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
DigestAlgorithm::Sha384 => db::rfc5912::SHA_384_WITH_RSA_ENCRYPTION,
DigestAlgorithm::Sha512 => db::rfc5912::SHA_512_WITH_RSA_ENCRYPTION,
_ => *sig_alg_oid,
}
} else if *sig_alg_oid == OID_EC_PUBLIC_KEY {
match digest_alg {
DigestAlgorithm::Sha256 => db::rfc5912::ECDSA_WITH_SHA_256,
DigestAlgorithm::Sha384 => db::rfc5912::ECDSA_WITH_SHA_384,
DigestAlgorithm::Sha512 => db::rfc5912::ECDSA_WITH_SHA_512,
_ => *sig_alg_oid,
}
} else {
if let Some(embedded) = combined_oid_digest(sig_alg_oid) {
if embedded != digest_alg {
return Err(TrustError::SignatureVerification(format!(
"signatureAlgorithm hash ({}) disagrees with SignerInfo.digestAlgorithm ({})",
embedded.name(),
digest_alg.name(),
)));
}
}
*sig_alg_oid
};
verify_signature_by_oid(signed_attrs_der, signature, spki_der, &resolved)
}
fn verify_pss_signature(
signed_attrs_der: &[u8],
signature: &[u8],
spki_der: &[u8],
signature_algorithm: &AlgorithmIdentifierOwned,
digest_alg: DigestAlgorithm,
) -> Result<(), TrustError> {
use crate::crypto::verify::verify_rsa_pss_signature_strict;
let pss_hash = verify_rsa_pss_signature_strict(
signed_attrs_der,
signature,
spki_der,
signature_algorithm.parameters.as_ref(),
)?;
if pss_hash != digest_alg {
return Err(TrustError::SignatureVerification(format!(
"RSASSA-PSS hashAlgorithm ({}) disagrees with SignerInfo.digestAlgorithm ({})",
pss_hash.name(),
digest_alg.name(),
)));
}
Ok(())
}
fn combined_oid_digest(oid: &ObjectIdentifier) -> Option<DigestAlgorithm> {
use const_oid::db;
if *oid == db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION || *oid == db::rfc5912::ECDSA_WITH_SHA_256 {
Some(DigestAlgorithm::Sha256)
} else if *oid == db::rfc5912::SHA_384_WITH_RSA_ENCRYPTION
|| *oid == db::rfc5912::ECDSA_WITH_SHA_384
{
Some(DigestAlgorithm::Sha384)
} else if *oid == db::rfc5912::SHA_512_WITH_RSA_ENCRYPTION
|| *oid == db::rfc5912::ECDSA_WITH_SHA_512
{
Some(DigestAlgorithm::Sha512)
} else {
None
}
}
fn require_timestamping_eku(cert: &Certificate) -> Result<(), TspError> {
let exts = cert.tbs_certificate.extensions.as_ref().ok_or_else(|| {
TspError::VerificationFailed("TSA certificate has no extensions (no EKU)".into())
})?;
let eku_ext = exts
.iter()
.find(|e| e.extn_id == ID_CE_EXT_KEY_USAGE)
.ok_or_else(|| {
TspError::VerificationFailed(
"TSA certificate lacks an extendedKeyUsage extension".into(),
)
})?;
if !eku_ext.critical {
return Err(TspError::VerificationFailed(
"TSA certificate extendedKeyUsage is not marked critical (RFC 3161 §2.3)".into(),
));
}
if !eku_contains(eku_ext.extn_value.as_bytes(), &ID_KP_TIME_STAMPING) {
return Err(TspError::VerificationFailed(
"TSA certificate extendedKeyUsage does not include id-kp-timeStamping".into(),
));
}
Ok(())
}
fn require_not_ca(cert: &Certificate) -> Result<(), TspError> {
let Some(exts) = cert.tbs_certificate.extensions.as_ref() else {
return Ok(()); };
let Some(bc_ext) = exts.iter().find(|e| e.extn_id == ID_CE_BASIC_CONSTRAINTS) else {
return Ok(()); };
let (is_ca, _) = der_utils::parse_basic_constraints(bc_ext.extn_value.as_bytes())
.map_err(|e| TspError::VerificationFailed(format!("TSA basicConstraints: {e}")))?;
if is_ca {
return Err(TspError::VerificationFailed(
"TSA certificate asserts basicConstraints CA:TRUE (RFC 3161 §2.3)".into(),
));
}
Ok(())
}
fn eku_contains(eku_der: &[u8], target: &ObjectIdentifier) -> bool {
let Ok((tag, body)) = der_utils::parse_tlv(eku_der) else {
return false;
};
if tag != 0x30 {
return false;
}
let mut pos = &body[..];
while !pos.is_empty() {
let Ok((oid_tag, oid_body, rest)) = der_utils::parse_tlv_with_rest(pos) else {
break;
};
if oid_tag == 0x06 {
if let Ok(oid) = ObjectIdentifier::from_der(&der_utils::encode_tlv(0x06, oid_body)) {
if oid == *target {
return true;
}
}
}
pos = rest;
}
false
}
fn order_chain(
signer: &Certificate,
embedded: &[Certificate],
policy: crate::crypto::verify::SignaturePolicy,
) -> Vec<Certificate> {
let mut chain = vec![signer.clone()];
for _ in 0..16 {
let current = chain.last().unwrap().clone();
if current.tbs_certificate.issuer == current.tbs_certificate.subject {
break; }
let candidates: Vec<&Certificate> = embedded
.iter()
.filter(|c| {
c.tbs_certificate.subject == current.tbs_certificate.issuer
&& !chain
.iter()
.any(|existing| existing.tbs_certificate == c.tbs_certificate)
})
.collect();
let chosen = candidates
.iter()
.find(|c| {
crate::crypto::verify::verify_certificate_signature_with_policy(
¤t, c, &policy,
)
.is_ok()
})
.or_else(|| candidates.first());
match chosen {
Some(c) => chain.push((*c).clone()),
None => break,
}
}
chain
}
fn gen_time_datetime(tst_info: &TstInfo) -> Result<der::DateTime, TspError> {
let gt_tlv = der_utils::encode_tlv(0x18, &tst_info.gen_time_der);
Ok(der::asn1::GeneralizedTime::from_der(>_tlv)
.map_err(|e| TspError::VerificationFailed(format!("invalid genTime: {e}")))?
.to_date_time())
}
fn check_gen_time_within_validity(
signer: &Certificate,
tst_info: &TstInfo,
) -> Result<(), TspError> {
let gen_time = gen_time_datetime(tst_info)?;
let validity = &signer.tbs_certificate.validity;
let not_before = validity.not_before.to_date_time();
let not_after = validity.not_after.to_date_time();
if gen_time < not_before || gen_time > not_after {
return Err(TspError::VerificationFailed(format!(
"timestamp genTime {gen_time} is outside the TSA certificate validity \
({not_before} .. {not_after})"
)));
}
Ok(())
}
#[derive(Debug)]
pub struct TstInfo {
pub hash_algorithm: DigestAlgorithm,
pub message_hash: Vec<u8>,
pub serial_number: Vec<u8>,
pub gen_time_der: Vec<u8>,
pub nonce: Option<u64>,
pub policy_oid: Option<String>,
}
pub fn extract_tst_info(token_der: &[u8]) -> Result<TstInfo, TspError> {
let (tag, ci_body) = der_utils::parse_tlv(token_der)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse ContentInfo: {e}")))?;
if tag != 0x30 {
return Err(TspError::InvalidResponse(
"ContentInfo: expected SEQUENCE".into(),
));
}
let (_oid_tag, _oid_body, ci_rest) = der_utils::parse_tlv_with_rest(&ci_body)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse contentType: {e}")))?;
let (ctx_tag, sd_inner, _) = der_utils::parse_tlv_with_rest(ci_rest)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse content [0]: {e}")))?;
if ctx_tag != 0xA0 {
return Err(TspError::InvalidResponse(format!(
"expected [0] EXPLICIT tag 0xA0, got 0x{ctx_tag:02x}"
)));
}
let (sd_tag, sd_body) = der_utils::parse_tlv(sd_inner)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse SignedData: {e}")))?;
if sd_tag != 0x30 {
return Err(TspError::InvalidResponse(
"SignedData: expected SEQUENCE".into(),
));
}
let (_ver_tag, _ver_body, sd_rest) = der_utils::parse_tlv_with_rest(&sd_body)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse SD version: {e}")))?;
let (_da_tag, _da_body, sd_rest2) = der_utils::parse_tlv_with_rest(sd_rest)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse digestAlgorithms: {e}")))?;
let (_eci_tag, eci_body, _sd_rest3) = der_utils::parse_tlv_with_rest(sd_rest2)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse encapContentInfo: {e}")))?;
let (_ect_tag, _ect_body, eci_rest) = der_utils::parse_tlv_with_rest(eci_body)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse eContentType: {e}")))?;
let (ec_tag, ec_inner, _) = der_utils::parse_tlv_with_rest(eci_rest)
.map_err(|e| TspError::InvalidResponse(format!("failed to parse eContent [0]: {e}")))?;
if ec_tag != 0xA0 {
return Err(TspError::InvalidResponse(format!(
"expected eContent [0] tag 0xA0, got 0x{ec_tag:02x}"
)));
}
let (os_tag, tst_info_der, _) = der_utils::parse_tlv_with_rest(ec_inner).map_err(|e| {
TspError::InvalidResponse(format!("failed to parse eContent OCTET STRING: {e}"))
})?;
if os_tag != 0x04 {
return Err(TspError::InvalidResponse(format!(
"expected OCTET STRING 0x04 for eContent, got 0x{os_tag:02x}"
)));
}
parse_tst_info_body(tst_info_der)
}
fn parse_tst_info_body(der_bytes: &[u8]) -> Result<TstInfo, TspError> {
let (tag, body) = der_utils::parse_tlv(der_bytes).map_err(|e| {
TspError::InvalidResponse(format!("TSTInfo: failed to parse SEQUENCE: {e}"))
})?;
if tag != 0x30 {
return Err(TspError::InvalidResponse(
"TSTInfo: expected SEQUENCE".into(),
));
}
let mut pos = &body[..];
let (_vtag, _vbody, rest) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| TspError::InvalidResponse(format!("TSTInfo: failed to parse version: {e}")))?;
pos = rest;
let (_ptag, pbody, rest) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| TspError::InvalidResponse(format!("TSTInfo: failed to parse policy: {e}")))?;
let policy_oid = ObjectIdentifier::from_der(&der_utils::encode_tlv(0x06, pbody))
.ok()
.map(|oid| oid.to_string());
pos = rest;
let (_mi_tag, mi_body, rest) = der_utils::parse_tlv_with_rest(pos).map_err(|e| {
TspError::InvalidResponse(format!("TSTInfo: failed to parse messageImprint: {e}"))
})?;
pos = rest;
let (hash_algorithm, message_hash) = parse_message_imprint(mi_body)?;
let (_sn_tag, sn_body, rest) = der_utils::parse_tlv_with_rest(pos).map_err(|e| {
TspError::InvalidResponse(format!("TSTInfo: failed to parse serialNumber: {e}"))
})?;
let serial_number = sn_body.to_vec();
pos = rest;
let (_gt_tag, gt_body, rest) = der_utils::parse_tlv_with_rest(pos)
.map_err(|e| TspError::InvalidResponse(format!("TSTInfo: failed to parse genTime: {e}")))?;
let gen_time_der = gt_body.to_vec();
pos = rest;
let mut nonce = None;
while !pos.is_empty() {
if let Ok((ftag, fbody, frest)) = der_utils::parse_tlv_with_rest(pos) {
match ftag {
0x30 => {
}
0x01 => {
}
0x02 => {
nonce =
Some(der_utils::decode_integer_u64(fbody).map_err(|e| {
TspError::InvalidResponse(format!("TSTInfo nonce: {e}"))
})?);
}
0xA0 => {
}
0xA1 => {
}
_ => {
}
}
pos = frest;
} else {
break;
}
}
Ok(TstInfo {
hash_algorithm,
message_hash,
serial_number,
gen_time_der,
nonce,
policy_oid,
})
}
fn parse_message_imprint(body: &[u8]) -> Result<(DigestAlgorithm, Vec<u8>), TspError> {
let (_alg_tag, alg_body, rest) = der_utils::parse_tlv_with_rest(body).map_err(|e| {
TspError::InvalidResponse(format!(
"messageImprint: failed to parse hashAlgorithm: {e}"
))
})?;
let (_oid_tag, oid_body, _) = der_utils::parse_tlv_with_rest(alg_body).map_err(|e| {
TspError::InvalidResponse(format!(
"messageImprint: failed to parse algorithm OID: {e}"
))
})?;
let alg_oid =
ObjectIdentifier::from_der(&der_utils::encode_tlv(0x06, oid_body)).map_err(|e| {
TspError::InvalidResponse(format!("messageImprint: invalid algorithm OID: {e}"))
})?;
let digest_alg = oid_to_digest_algorithm(&alg_oid)?;
let (_hash_tag, hash_body, _) = der_utils::parse_tlv_with_rest(rest).map_err(|e| {
TspError::InvalidResponse(format!(
"messageImprint: failed to parse hashedMessage: {e}"
))
})?;
Ok((digest_alg, hash_body.to_vec()))
}
fn oid_to_digest_algorithm(oid: &ObjectIdentifier) -> Result<DigestAlgorithm, TspError> {
DigestAlgorithm::from_oid(oid)
.ok_or_else(|| TspError::InvalidResponse(format!("unsupported hash algorithm OID: {oid}")))
}
fn digest_algorithm_identifier(alg: DigestAlgorithm) -> AlgorithmIdentifierOwned {
AlgorithmIdentifierOwned {
oid: alg.oid(),
parameters: None,
}
}
pub fn generate_nonce() -> kryptering::Result<u64> {
let buf = kryptering::random_bytes(8)?;
Ok(u64::from_ne_bytes(
buf.try_into()
.expect("requested exactly eight random bytes"),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_timestamp_request_basic() {
let hash = vec![0xAA; 32]; let req =
build_timestamp_request(DigestAlgorithm::Sha256, &hash, None, None, true).unwrap();
assert_eq!(req[0], 0x30, "should start with SEQUENCE tag");
let (tag, _body) = der_utils::parse_tlv(&req).unwrap();
assert_eq!(tag, 0x30);
}
#[test]
fn test_build_timestamp_request_with_nonce() {
let hash = vec![0xBB; 32];
let nonce = 12345678u64;
let req = build_timestamp_request(DigestAlgorithm::Sha256, &hash, None, Some(nonce), true)
.unwrap();
let (tag, _body) = der_utils::parse_tlv(&req).unwrap();
assert_eq!(tag, 0x30);
}
#[test]
fn test_encode_integer_u64() {
let encoded = der_utils::encode_integer_u64(1);
assert_eq!(encoded, vec![0x02, 0x01, 0x01]);
let encoded = der_utils::encode_integer_u64(128);
assert_eq!(encoded, vec![0x02, 0x02, 0x00, 0x80]);
let encoded = der_utils::encode_integer_u64(0);
assert_eq!(encoded, vec![0x02, 0x01, 0x00]);
}
#[test]
fn test_pki_status_display() {
assert_eq!(PkiStatus::Granted.to_string(), "granted (0)");
assert_eq!(PkiStatus::Rejection.to_string(), "rejection (2)");
assert!(PkiStatus::Granted.is_success());
assert!(PkiStatus::GrantedWithMods.is_success());
assert!(!PkiStatus::Rejection.is_success());
}
#[test]
fn test_der_length_roundtrip() {
for len in [0, 1, 127, 128, 255, 256, 65535, 65536] {
let mut buf = Vec::new();
der_utils::encode_der_length(&mut buf, len);
let (parsed_len, consumed) = der_utils::parse_der_length(&buf).unwrap();
assert_eq!(parsed_len, len, "length roundtrip failed for {len}");
assert_eq!(consumed, buf.len());
}
}
#[test]
fn test_parse_timestamp_response_error_status() {
let status_info = der_utils::encode_sequence_raw(&der_utils::encode_integer_u64(2));
let resp_der = der_utils::encode_sequence_raw(&status_info);
let resp = parse_timestamp_response(&resp_der).unwrap();
assert_eq!(resp.status, PkiStatus::Rejection);
assert!(resp.token_der.is_none());
}
#[test]
fn test_parse_timestamp_response_rejects_malformed_status_field() {
let mut status_body = der_utils::encode_integer_u64(0);
status_body.extend_from_slice(&[0x30, 0x05, 0x01]); let status_info = der_utils::encode_sequence_raw(&status_body);
let resp_der = der_utils::encode_sequence_raw(&status_info);
let err = parse_timestamp_response(&resp_der)
.expect_err("malformed PKIStatusInfo field must be rejected");
assert!(
matches!(err, TspError::InvalidResponse(_)),
"expected InvalidResponse, got {err:?}"
);
}
fn resp_with_status_string(inner_tag: u8, inner_body: &[u8]) -> Vec<u8> {
let status_int = der_utils::encode_integer_u64(2); let free_text =
der_utils::encode_sequence_raw(&der_utils::encode_tlv(inner_tag, inner_body));
let mut status_body = status_int;
status_body.extend_from_slice(&free_text);
let status_info = der_utils::encode_sequence_raw(&status_body);
der_utils::encode_sequence_raw(&status_info)
}
#[test]
fn test_parse_timestamp_response_accepts_valid_status_string() {
let resp_der = resp_with_status_string(0x0C, "rejected: bad request".as_bytes());
let resp = parse_timestamp_response(&resp_der).unwrap();
assert_eq!(resp.status, PkiStatus::Rejection);
assert_eq!(resp.status_string.as_deref(), Some("rejected: bad request"));
}
#[test]
fn test_parse_timestamp_response_rejects_non_utf8string_status() {
let resp_der = resp_with_status_string(0x04, b"not a utf8string"); let err = parse_timestamp_response(&resp_der)
.expect_err("non-UTF8String statusString must be rejected");
assert!(
matches!(err, TspError::InvalidResponse(ref m) if m.contains("UTF8String")),
"expected UTF8String tag rejection, got {err:?}"
);
}
#[test]
fn test_parse_timestamp_response_rejects_invalid_utf8_status() {
let resp_der = resp_with_status_string(0x0C, &[0xFF, 0xFE, 0xFD]);
let err = parse_timestamp_response(&resp_der)
.expect_err("invalid UTF-8 statusString must be rejected");
assert!(
matches!(err, TspError::InvalidResponse(ref m) if m.contains("UTF-8")),
"expected invalid-UTF-8 rejection, got {err:?}"
);
}
#[test]
fn test_parse_timestamp_response_rejects_malformed_trailing_status_element() {
let status_int = der_utils::encode_integer_u64(2); let mut free_text_body = der_utils::encode_tlv(0x0C, b"ok");
free_text_body.extend_from_slice(&der_utils::encode_tlv(0x04, b"bad")); let free_text = der_utils::encode_sequence_raw(&free_text_body);
let mut status_body = status_int;
status_body.extend_from_slice(&free_text);
let status_info = der_utils::encode_sequence_raw(&status_body);
let resp_der = der_utils::encode_sequence_raw(&status_info);
let err = parse_timestamp_response(&resp_der)
.expect_err("a trailing non-UTF8String statusString element must be rejected");
assert!(
matches!(err, TspError::InvalidResponse(ref m) if m.contains("UTF8String")),
"expected UTF8String rejection of the trailing element, got {err:?}"
);
}
#[test]
fn test_parse_timestamp_response_rejects_empty_status_string() {
let status_int = der_utils::encode_integer_u64(2); let free_text = der_utils::encode_sequence_raw(&[]); let mut status_body = status_int;
status_body.extend_from_slice(&free_text);
let status_info = der_utils::encode_sequence_raw(&status_body);
let resp_der = der_utils::encode_sequence_raw(&status_info);
let err = parse_timestamp_response(&resp_der)
.expect_err("an empty statusString SEQUENCE must be rejected");
assert!(
matches!(err, TspError::InvalidResponse(ref m) if m.contains("empty")),
"expected empty-PKIFreeText rejection, got {err:?}"
);
}
#[test]
fn test_parse_timestamp_response_rejects_trailing_bytes_after_token() {
let status_info = der_utils::encode_sequence_raw(&der_utils::encode_integer_u64(0));
let token = der_utils::encode_sequence_raw(&der_utils::encode_integer_u64(1)); let mut body = status_info;
body.extend_from_slice(&token);
body.extend_from_slice(&der_utils::encode_integer_u64(9)); let resp_der = der_utils::encode_sequence_raw(&body);
let err = parse_timestamp_response(&resp_der)
.expect_err("trailing bytes after the TimeStampToken must be rejected");
assert!(
matches!(err, TspError::InvalidResponse(ref m) if m.contains("trailing")),
"expected trailing-data rejection, got {err:?}"
);
}
#[test]
fn test_parse_timestamp_response_rejects_non_sequence_token() {
let status_info = der_utils::encode_sequence_raw(&der_utils::encode_integer_u64(0));
let mut body = status_info;
body.extend_from_slice(&der_utils::encode_integer_u64(7));
let resp_der = der_utils::encode_sequence_raw(&body);
let err = parse_timestamp_response(&resp_der)
.expect_err("non-SEQUENCE TimeStampToken must be rejected");
assert!(
matches!(err, TspError::InvalidResponse(_)),
"expected InvalidResponse, got {err:?}"
);
}
#[test]
fn test_generate_nonce() {
let n1 = generate_nonce().unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
let n2 = generate_nonce().unwrap();
assert_ne!(n1, n2, "nonces should be unique");
}
use cms::cert::CertificateChoices;
use cms::content_info::{CmsVersion, ContentInfo};
use cms::signed_data::{
CertificateSet, EncapsulatedContentInfo, SignerIdentifier, SignerInfo, SignerInfos,
};
use der::asn1::{Any, SetOfVec};
use der::{Decode, Tag};
use rsa::pkcs8::DecodePrivateKey;
use rsa::RsaPrivateKey;
use spki::AlgorithmIdentifierOwned;
use std::sync::OnceLock;
use x509_cert::attr::Attribute;
use x509_cert::Certificate;
const INTERMEDIATE_CERT_PEM: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/intermediate_ca_cert.pem"
));
const INTERMEDIATE_KEY_PEM: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/intermediate_ca_key.pem"
));
const ROOT_CERT_PEM: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ca_cert.pem"
));
fn load_cert(pem: &str) -> Certificate {
let (_, der) = pem_rfc7468::decode_vec(pem.as_bytes()).unwrap();
Certificate::from_der(&der).unwrap()
}
fn load_key(pem: &str) -> RsaPrivateKey {
let der = pem_rfc7468::decode_vec(pem.as_bytes()).unwrap().1;
RsaPrivateKey::from_pkcs8_der(&der).unwrap()
}
fn intermediate_cert() -> Certificate {
load_cert(INTERMEDIATE_CERT_PEM)
}
fn intermediate_key() -> RsaPrivateKey {
load_key(INTERMEDIATE_KEY_PEM)
}
struct TsaIdentity {
cert: Certificate,
key: RsaPrivateKey,
}
fn tsa_identity() -> &'static TsaIdentity {
use rsa::pkcs1v15::SigningKey;
use rsa::signature::Keypair;
use sha2::Sha256;
use x509_cert::builder::{Builder, CertificateBuilder, Profile};
use x509_cert::ext::pkix::ExtendedKeyUsage;
use x509_cert::name::Name;
use x509_cert::serial_number::SerialNumber;
use x509_cert::spki::SubjectPublicKeyInfoOwned;
use x509_cert::time::Validity;
static ID: OnceLock<TsaIdentity> = OnceLock::new();
ID.get_or_init(|| {
let mut rng = rand::thread_rng();
let tsa_key = RsaPrivateKey::new(&mut rng, 2048).expect("RSA keygen");
let tsa_signing = SigningKey::<Sha256>::new(tsa_key.clone());
let spki = SubjectPublicKeyInfoOwned::from_key(tsa_signing.verifying_key())
.expect("SPKI from key");
let issuer = intermediate_cert();
let ca_signer = SigningKey::<Sha256>::new(intermediate_key());
let profile = Profile::Leaf {
issuer: issuer.tbs_certificate.subject.clone(),
enable_key_agreement: false,
enable_key_encipherment: false,
};
let serial = SerialNumber::new(&[0x2A]).unwrap();
let validity =
Validity::from_now(std::time::Duration::from_secs(3650 * 24 * 3600)).unwrap();
let subject: Name = "CN=Runtime Test TSA,O=tsp-ltv tests".parse().unwrap();
let mut builder =
CertificateBuilder::new(profile, serial, validity, subject, spki, &ca_signer)
.expect("cert builder");
builder
.add_extension(&ExtendedKeyUsage(vec![ID_KP_TIME_STAMPING]))
.expect("add EKU");
let cert = builder
.build::<rsa::pkcs1v15::Signature>()
.expect("sign cert");
TsaIdentity { cert, key: tsa_key }
})
}
fn tsa_cert() -> Certificate {
tsa_identity().cert.clone()
}
fn tsa_key() -> RsaPrivateKey {
tsa_identity().key.clone()
}
const SHA256_OID_DER: &[u8] = &[
0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
];
fn fmt_generalized(dt: chrono::DateTime<chrono::Utc>) -> Vec<u8> {
dt.format("%Y%m%d%H%M%SZ").to_string().into_bytes()
}
fn gen_time_within() -> Vec<u8> {
fmt_generalized(chrono::Utc::now())
}
fn gen_time_after_validity() -> Vec<u8> {
fmt_generalized(chrono::Utc::now() + chrono::Duration::days(365 * 20))
}
fn validation_time() -> der::DateTime {
use chrono::{Datelike, Timelike};
let n = chrono::Utc::now();
der::DateTime::new(
n.year() as u16,
n.month() as u8,
n.day() as u8,
n.hour() as u8,
n.minute() as u8,
n.second() as u8,
)
.unwrap()
}
fn build_tst_info(hash: &[u8], nonce: u64, gen_time_bytes: &[u8]) -> Vec<u8> {
let version = der_utils::encode_integer_u64(1);
let policy = der_utils::encode_tlv(0x06, &[0x2B, 0x06, 0x01, 0x04, 0x01]);
let alg = der_utils::encode_sequence_from_parts(&[SHA256_OID_DER, &[0x05, 0x00]]);
let hashed = der_utils::encode_tlv(0x04, hash);
let message_imprint = der_utils::encode_sequence_from_parts(&[&alg, &hashed]);
let serial = der_utils::encode_integer_u64(42);
let gen_time = der_utils::encode_tlv(0x18, gen_time_bytes);
let nonce_int = der_utils::encode_integer_u64(nonce);
let body = [
version,
policy,
message_imprint,
serial,
gen_time,
nonce_int,
]
.concat();
der_utils::encode_sequence_raw(&body)
}
fn null_params() -> Option<Any> {
Some(Any::null())
}
fn build_signed_token(
signer_cert: &Certificate,
signer_key: &RsaPrivateKey,
extra_certs: &[Certificate],
hash: &[u8],
nonce: u64,
corrupt_sig: bool,
) -> Vec<u8> {
build_signed_token_gt(
signer_cert,
signer_key,
extra_certs,
hash,
nonce,
&gen_time_within(),
true,
corrupt_sig,
)
}
#[allow(clippy::too_many_arguments)]
fn build_signed_token_gt(
signer_cert: &Certificate,
signer_key: &RsaPrivateKey,
extra_certs: &[Certificate],
hash: &[u8],
nonce: u64,
gen_time_bytes: &[u8],
embed_certs: bool,
corrupt_sig: bool,
) -> Vec<u8> {
use rsa::pkcs1v15::{Signature, SigningKey};
use rsa::signature::{SignatureEncoding, Signer};
use sha2::{Digest, Sha256};
let tst_info_der = build_tst_info(hash, nonce, gen_time_bytes);
let digest = Sha256::digest(&tst_info_der).to_vec();
let ct_value = Any::encode_from(&ID_CT_TST_INFO).unwrap();
let ct_attr = Attribute {
oid: ID_CONTENT_TYPE_ATTR,
values: SetOfVec::try_from(vec![ct_value]).unwrap(),
};
let md_value = Any::new(Tag::OctetString, digest).unwrap();
let md_attr = Attribute {
oid: ID_MESSAGE_DIGEST_ATTR,
values: SetOfVec::try_from(vec![md_value]).unwrap(),
};
let signed_attrs: x509_cert::attr::Attributes =
SetOfVec::try_from(vec![ct_attr, md_attr]).unwrap();
let signed_attrs_der = signed_attrs.to_der().unwrap();
let signing_key = SigningKey::<Sha256>::new(signer_key.clone());
let to_sign: &[u8] = if corrupt_sig {
b"not the signed attributes"
} else {
&signed_attrs_der
};
let signature: Signature = signing_key.sign(to_sign);
let sha256_alg = AlgorithmIdentifierOwned {
oid: DigestAlgorithm::Sha256.oid(),
parameters: null_params(),
};
let signer_info = SignerInfo {
version: CmsVersion::V1,
sid: SignerIdentifier::IssuerAndSerialNumber(cms::cert::IssuerAndSerialNumber {
issuer: signer_cert.tbs_certificate.issuer.clone(),
serial_number: signer_cert.tbs_certificate.serial_number.clone(),
}),
digest_alg: sha256_alg.clone(),
signed_attrs: Some(signed_attrs),
signature_algorithm: AlgorithmIdentifierOwned {
oid: OID_RSA_ENCRYPTION,
parameters: null_params(),
},
signature: OctetString::new(signature.to_vec()).unwrap(),
unsigned_attrs: None,
};
let certificates = if embed_certs {
let mut cert_choices = vec![CertificateChoices::Certificate(signer_cert.clone())];
for cert in extra_certs {
cert_choices.push(CertificateChoices::Certificate(cert.clone()));
}
Some(CertificateSet::from(
SetOfVec::try_from(cert_choices).unwrap(),
))
} else {
None
};
let signed_data = cms::signed_data::SignedData {
version: CmsVersion::V3,
digest_algorithms: SetOfVec::try_from(vec![sha256_alg]).unwrap(),
encap_content_info: EncapsulatedContentInfo {
econtent_type: ID_CT_TST_INFO,
econtent: Some(Any::new(Tag::OctetString, tst_info_der).unwrap()),
},
certificates,
crls: None,
signer_infos: SignerInfos::from(SetOfVec::try_from(vec![signer_info]).unwrap()),
};
let content_info = ContentInfo {
content_type: ID_SIGNED_DATA,
content: Any::encode_from(&signed_data).unwrap(),
};
content_info.to_der().unwrap()
}
#[test]
fn test_verify_valid_token_no_trust_store() {
let hash = vec![0xABu8; 32];
let nonce = 0xDEAD_BEEFu64;
let token = build_signed_token(&tsa_cert(), &tsa_key(), &[], &hash, nonce, false);
let tst = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
Some(nonce),
None,
None,
&[],
)
.expect("validly-signed token must verify");
assert_eq!(tst.message_hash, hash);
assert_eq!(tst.nonce, Some(nonce));
}
#[test]
fn test_verify_valid_token_with_trust_store() {
let hash = vec![0x11u8; 32];
let nonce = 7u64;
let token = build_signed_token(
&tsa_cert(),
&tsa_key(),
&[intermediate_cert()],
&hash,
nonce,
false,
);
let mut store = TrustStore::new();
let (_, root_der) = pem_rfc7468::decode_vec(ROOT_CERT_PEM.as_bytes()).unwrap();
store.add_der_certificate(&root_der).unwrap();
let tst = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
Some(nonce),
Some(&store),
Some(validation_time()),
&[],
)
.expect("token chaining to a trusted root must verify");
assert_eq!(tst.message_hash, hash);
}
#[test]
fn test_trust_store_validation_time_defaults_to_gen_time() {
let hash = vec![0x88u8; 32];
let token = build_signed_token(
&tsa_cert(),
&tsa_key(),
&[intermediate_cert()],
&hash,
1,
false,
);
let mut store = TrustStore::new();
let (_, root_der) = pem_rfc7468::decode_vec(ROOT_CERT_PEM.as_bytes()).unwrap();
store.add_der_certificate(&root_der).unwrap();
verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
Some(&store),
None, &[],
)
.expect("chain must verify at genTime when validation_time is None");
}
#[test]
fn test_reject_gen_time_outside_validity_without_trust_store() {
let hash = vec![0x77u8; 32];
let token = build_signed_token_gt(
&tsa_cert(),
&tsa_key(),
&[],
&hash,
1,
&gen_time_after_validity(),
true, false, );
let err = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(
matches!(err, TspError::VerificationFailed(_)),
"genTime outside TSA cert validity must be rejected, got {err:?}"
);
}
#[test]
fn test_reject_tampered_signature() {
let hash = vec![0x22u8; 32];
let token = build_signed_token(&tsa_cert(), &tsa_key(), &[], &hash, 1, true);
let err = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(
matches!(err, TspError::VerificationFailed(_)),
"tampered signature must be rejected, got {err:?}"
);
}
#[test]
fn test_reject_untrusted_root() {
let hash = vec![0x33u8; 32];
let token = build_signed_token(
&tsa_cert(),
&tsa_key(),
&[intermediate_cert()],
&hash,
1,
false,
);
let empty_store = TrustStore::new();
let err = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
Some(&empty_store),
Some(validation_time()),
&[],
)
.unwrap_err();
assert!(
matches!(err, TspError::VerificationFailed(_)),
"token not chaining to a trust anchor must be rejected, got {err:?}"
);
}
#[test]
fn test_reject_signer_without_timestamping_eku() {
let hash = vec![0x44u8; 32];
let token = build_signed_token(
&intermediate_cert(),
&intermediate_key(),
&[],
&hash,
1,
false,
);
let err = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(
matches!(err, TspError::VerificationFailed(_)),
"signer without timeStamping EKU must be rejected, got {err:?}"
);
}
#[test]
fn test_reject_ca_signer_with_timestamping_eku() {
use rsa::pkcs1v15::SigningKey;
use rsa::signature::Keypair;
use sha2::Sha256;
use x509_cert::builder::{Builder, CertificateBuilder, Profile};
use x509_cert::ext::pkix::ExtendedKeyUsage;
use x509_cert::name::Name;
use x509_cert::serial_number::SerialNumber;
use x509_cert::spki::SubjectPublicKeyInfoOwned;
use x509_cert::time::Validity;
let mut rng = rand::thread_rng();
let ca_key = RsaPrivateKey::new(&mut rng, 2048).expect("RSA keygen");
let ca_signing = SigningKey::<Sha256>::new(ca_key.clone());
let spki = SubjectPublicKeyInfoOwned::from_key(ca_signing.verifying_key()).expect("SPKI");
let subject: Name = "CN=Rogue CA TSA,O=tsp-ltv tests".parse().unwrap();
let validity =
Validity::from_now(std::time::Duration::from_secs(3650 * 24 * 3600)).unwrap();
let mut builder = CertificateBuilder::new(
Profile::Root,
SerialNumber::new(&[0x77]).unwrap(),
validity,
subject,
spki,
&ca_signing,
)
.expect("cert builder");
builder
.add_extension(&ExtendedKeyUsage(vec![ID_KP_TIME_STAMPING]))
.expect("add EKU");
let ca_cert = builder
.build::<rsa::pkcs1v15::Signature>()
.expect("sign cert");
let hash = vec![0x66u8; 32];
let token = build_signed_token(&ca_cert, &ca_key, &[], &hash, 1, false);
let err = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(
matches!(err, TspError::VerificationFailed(ref m) if m.contains("CA:TRUE")),
"CA certificate must be rejected as a TSA signer, got {err:?}"
);
}
#[test]
fn test_reject_unsigned_token_old_behavior() {
let hash = vec![0x55u8; 32];
let tst_info_der = build_tst_info(&hash, 1, &gen_time_within());
let signed_data = cms::signed_data::SignedData {
version: CmsVersion::V3,
digest_algorithms: SetOfVec::new(),
encap_content_info: EncapsulatedContentInfo {
econtent_type: ID_CT_TST_INFO,
econtent: Some(Any::new(Tag::OctetString, tst_info_der).unwrap()),
},
certificates: None,
crls: None,
signer_infos: SignerInfos::from(SetOfVec::<SignerInfo>::new()),
};
let content_info = ContentInfo {
content_type: ID_SIGNED_DATA,
content: Any::encode_from(&signed_data).unwrap(),
};
let token = content_info.to_der().unwrap();
let err = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(
matches!(err, TspError::InvalidResponse(_)),
"unsigned token must be rejected, got {err:?}"
);
}
#[test]
fn test_reject_garbage_token() {
let err = verify_timestamp_token(
&[0x30, 0x03, 0x02, 0x01, 0x01],
&[0u8; 32],
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(matches!(err, TspError::InvalidResponse(_)));
}
#[test]
fn test_reject_wrong_message_imprint() {
let real_hash = vec![0x66u8; 32];
let token = build_signed_token(&tsa_cert(), &tsa_key(), &[], &real_hash, 1, false);
let expected = vec![0x99u8; 32];
let err = verify_timestamp_token(
&token,
&expected,
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(matches!(err, TspError::InvalidResponse(_)));
}
fn pss_algid<D>(salt_len: u8) -> AlgorithmIdentifierOwned
where
D: const_oid::AssociatedOid,
{
use rsa::pkcs1::RsaPssParams;
let params = RsaPssParams::new::<D>(salt_len);
let params_der = params.to_der().unwrap();
AlgorithmIdentifierOwned {
oid: OID_RSASSA_PSS,
parameters: Some(der::Any::from_der(¶ms_der).unwrap()),
}
}
#[test]
fn test_pss_signature_bound_to_signerinfo_digest() {
use rsa::pkcs8::EncodePublicKey;
use rsa::pss::SigningKey;
use rsa::signature::{RandomizedSigner, SignatureEncoding};
use sha2::Sha256;
let key = tsa_key();
let spki_der = rsa::RsaPublicKey::from(&key)
.to_public_key_der()
.unwrap()
.as_bytes()
.to_vec();
let signing = SigningKey::<Sha256>::new(key);
let msg = b"the DER-encoded signed attributes";
let mut rng = rand::thread_rng();
let sig = signing.sign_with_rng(&mut rng, msg).to_vec();
let algid = pss_algid::<Sha256>(32);
verify_cms_signature(msg, &sig, &spki_der, &algid, DigestAlgorithm::Sha256)
.expect("PSS-SHA256 signature must verify when bound to SHA-256");
assert!(
verify_cms_signature(msg, &sig, &spki_der, &algid, DigestAlgorithm::Sha384).is_err(),
"PSS signature must be rejected when the bound digest does not match"
);
}
#[test]
fn test_pss_signature_honours_nondefault_salt_length() {
use rsa::pkcs8::EncodePublicKey;
use rsa::pss::SigningKey;
use rsa::signature::{RandomizedSigner, SignatureEncoding};
use sha2::Sha256;
let key = tsa_key();
let spki_der = rsa::RsaPublicKey::from(&key)
.to_public_key_der()
.unwrap()
.as_bytes()
.to_vec();
let signing = SigningKey::<Sha256>::new_with_salt_len(key, 48);
let msg = b"the DER-encoded signed attributes";
let mut rng = rand::thread_rng();
let sig = signing.sign_with_rng(&mut rng, msg).to_vec();
let algid = pss_algid::<Sha256>(48);
let matching = verify_cms_signature(msg, &sig, &spki_der, &algid, DigestAlgorithm::Sha256);
#[cfg(feature = "aws-lc")]
assert!(
matches!(matching, Err(TrustError::UnsupportedAlgorithm(ref message)) if message.contains("salt length 48")),
"AWS-LC must report non-digest-length PSS salt as unsupported: {matching:?}"
);
#[cfg(not(feature = "aws-lc"))]
matching.expect("PSS signature with non-default salt must verify when params declare it");
let wrong = pss_algid::<Sha256>(32);
assert!(
verify_cms_signature(msg, &sig, &spki_der, &wrong, DigestAlgorithm::Sha256).is_err(),
"PSS signature must not verify when the declared salt length is wrong"
);
}
#[test]
fn test_pss_signature_requires_parameters() {
let bare = AlgorithmIdentifierOwned {
oid: OID_RSASSA_PSS,
parameters: None,
};
let err =
verify_cms_signature(b"x", b"y", b"z", &bare, DigestAlgorithm::Sha256).unwrap_err();
assert!(
matches!(err, TrustError::UnsupportedAlgorithm(_)),
"PSS without parameters must be rejected, got {err:?}"
);
}
#[test]
fn test_combined_sig_algorithm_hash_must_match_digest_algorithm() {
use const_oid::db;
use rsa::pkcs1v15::{Signature, SigningKey};
use rsa::pkcs8::EncodePublicKey;
use rsa::signature::{SignatureEncoding, Signer};
use sha2::Sha256;
let key = tsa_key();
let spki_der = rsa::RsaPublicKey::from(&key)
.to_public_key_der()
.unwrap()
.as_bytes()
.to_vec();
let signing = SigningKey::<Sha256>::new(key);
let msg = b"the DER-encoded signed attributes";
let sig: Signature = signing.sign(msg);
let sig = sig.to_vec();
let algid = AlgorithmIdentifierOwned {
oid: db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
parameters: None,
};
verify_cms_signature(msg, &sig, &spki_der, &algid, DigestAlgorithm::Sha256)
.expect("sha256WithRSA must verify when digestAlgorithm is SHA-256");
let err = verify_cms_signature(msg, &sig, &spki_der, &algid, DigestAlgorithm::Sha512)
.unwrap_err();
assert!(
matches!(err, TrustError::SignatureVerification(ref m) if m.contains("disagrees")),
"mismatched signatureAlgorithm/digestAlgorithm must be rejected, got {err:?}"
);
}
#[test]
fn test_verify_token_without_embedded_certs_via_extra_certs() {
let hash = vec![0xC0u8; 32];
let token = build_signed_token_gt(
&tsa_cert(),
&tsa_key(),
&[],
&hash,
1,
&gen_time_within(),
false, false,
);
let err = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
None,
None,
&[],
)
.unwrap_err();
assert!(
matches!(err, TspError::InvalidResponse(_)),
"token without certs and no extra_certs must fail as InvalidResponse, got {err:?}"
);
let tst = verify_timestamp_token(
&token,
&hash,
DigestAlgorithm::Sha256,
None,
None,
None,
&[tsa_cert()],
)
.expect("token must verify with externally-supplied signer cert");
assert_eq!(tst.message_hash, hash);
}
#[test]
fn test_order_chain_prefers_legacy_valid_issuer_among_same_subject_candidates() {
use crate::crypto::verify::SignaturePolicy;
use der::asn1::BitString;
use der::{Any, Decode, Encode};
use rsa::pkcs1v15::SigningKey;
use rsa::signature::Keypair;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
use sha1::{Digest as _, Sha1};
use sha2::Sha256;
use spki::AlgorithmIdentifierOwned;
use x509_cert::builder::{Builder, CertificateBuilder, Profile};
use x509_cert::name::Name;
use x509_cert::serial_number::SerialNumber;
use x509_cert::spki::SubjectPublicKeyInfoOwned;
use x509_cert::time::Validity;
let mut rng = rand::thread_rng();
let validity =
Validity::from_now(std::time::Duration::from_secs(3650 * 24 * 3600)).unwrap();
let issuer_name: Name = "CN=Ambiguous Issuer,O=tsp-ltv tests".parse().unwrap();
let self_signed = |name: &Name, key: &RsaPrivateKey, serial: u8| {
let signer = SigningKey::<Sha256>::new(key.clone());
let spki = SubjectPublicKeyInfoOwned::from_key(signer.verifying_key()).unwrap();
CertificateBuilder::new(
Profile::Root,
SerialNumber::new(&[serial]).unwrap(),
validity,
name.clone(),
spki,
&signer,
)
.unwrap()
.build()
.unwrap()
};
let real_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let real_issuer = self_signed(&issuer_name, &real_key, 0x01);
let decoy_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let decoy = self_signed(&issuer_name, &decoy_key, 0x02);
let leaf_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let leaf_spki = SubjectPublicKeyInfoOwned::from_key(
SigningKey::<Sha256>::new(leaf_key).verifying_key(),
)
.unwrap();
let real_signer = SigningKey::<Sha256>::new(real_key.clone());
let base = CertificateBuilder::new(
Profile::Leaf {
issuer: issuer_name.clone(),
enable_key_agreement: false,
enable_key_encipherment: false,
},
SerialNumber::new(&[0x03]).unwrap(),
validity,
"CN=Ambiguous Leaf,O=tsp-ltv tests".parse().unwrap(),
leaf_spki,
&real_signer,
)
.unwrap()
.build()
.unwrap();
let sha1_algid = AlgorithmIdentifierOwned {
oid: crate::crypto::algorithm::OID_SHA1_WITH_RSA,
parameters: Some(Any::from_der(&[0x05, 0x00]).unwrap()),
};
let mut tbs = base.tbs_certificate.clone();
tbs.signature = sha1_algid.clone();
let tbs_der = tbs.to_der().unwrap();
let hash = Sha1::digest(&tbs_der);
let sig = real_key
.sign(Pkcs1v15Sign::new::<Sha1>(), &hash)
.expect("SHA-1 RSA sign");
let leaf = Certificate {
tbs_certificate: tbs,
signature_algorithm: sha1_algid,
signature: BitString::from_bytes(&sig).unwrap(),
};
let embedded = vec![decoy.clone(), real_issuer.clone()];
let real_spki = real_issuer
.tbs_certificate
.subject_public_key_info
.to_der()
.unwrap();
let decoy_spki = decoy
.tbs_certificate
.subject_public_key_info
.to_der()
.unwrap();
let strict = order_chain(&leaf, &embedded, SignaturePolicy::strict());
assert_eq!(strict.len(), 2);
assert_eq!(
strict[1]
.tbs_certificate
.subject_public_key_info
.to_der()
.unwrap(),
decoy_spki,
"strict precheck falls back to the wrong same-subject cert"
);
let legacy = order_chain(&leaf, &embedded, SignaturePolicy::allow_legacy());
assert_eq!(legacy.len(), 2);
assert_eq!(
legacy[1]
.tbs_certificate
.subject_public_key_info
.to_der()
.unwrap(),
real_spki,
"allow_legacy prefers the issuer whose key actually signed the leaf"
);
}
}