use crate::asn1::{self, PkiStatus, TimeStampResp, TstInfo};
use crate::error::{Error, Result};
use cms::cert::CertificateChoices;
use cms::signed_data::{SignedData, SignerIdentifier};
use const_oid::ObjectIdentifier;
use jiff::Timestamp;
use rustls_pki_types::CertificateDer;
use x509_cert::Certificate;
use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage, ALL_VERIFICATION_ALGS};
const ID_KP_TIME_STAMPING: ObjectIdentifier = const_oid::db::rfc5280::ID_KP_TIME_STAMPING;
const ID_SIGNED_DATA: ObjectIdentifier = const_oid::db::rfc5911::ID_SIGNED_DATA;
const OID_MESSAGE_DIGEST: ObjectIdentifier = const_oid::db::rfc6268::ID_MESSAGE_DIGEST;
const OID_SHA256: ObjectIdentifier = const_oid::db::rfc5912::ID_SHA_256;
const OID_SHA384: ObjectIdentifier = const_oid::db::rfc5912::ID_SHA_384;
const OID_SHA512: ObjectIdentifier = const_oid::db::rfc5912::ID_SHA_512;
const OID_EC_PUBLIC_KEY: ObjectIdentifier = const_oid::db::rfc5912::ID_EC_PUBLIC_KEY;
const OID_SECP256R1: ObjectIdentifier = const_oid::db::rfc5912::SECP_256_R_1;
const OID_SECP384R1: ObjectIdentifier = const_oid::db::rfc5912::SECP_384_R_1;
#[derive(Debug, Clone)]
pub struct VerifyOpts<'a> {
pub roots: Vec<CertificateDer<'a>>,
pub intermediates: Vec<CertificateDer<'a>>,
pub tsa_certificates: Vec<CertificateDer<'a>>,
}
impl<'a> VerifyOpts<'a> {
pub fn new() -> Self {
Self {
roots: Vec::new(),
intermediates: Vec::new(),
tsa_certificates: Vec::new(),
}
}
pub fn with_root(mut self, root: CertificateDer<'a>) -> Self {
self.roots.push(root);
self
}
pub fn with_roots(mut self, roots: Vec<CertificateDer<'a>>) -> Self {
self.roots = roots;
self
}
pub fn with_intermediate(mut self, intermediate: CertificateDer<'a>) -> Self {
self.intermediates.push(intermediate);
self
}
pub fn with_intermediates(mut self, intermediates: Vec<CertificateDer<'a>>) -> Self {
self.intermediates = intermediates;
self
}
pub fn with_tsa_certificates(mut self, certs: Vec<CertificateDer<'a>>) -> Self {
self.tsa_certificates = certs;
self
}
}
impl Default for VerifyOpts<'_> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TimestampResult {
pub time: Timestamp,
}
pub fn verify_timestamp_response(
timestamp_token_bytes: &[u8],
signature_bytes: &[u8],
opts: VerifyOpts<'_>,
) -> Result<TimestampResult> {
use cms::content_info::ContentInfo;
use x509_cert::der::{Decode, Encode};
tracing::debug!("Starting RFC 3161 timestamp verification");
let (content_info, _token_bytes) = match TimeStampResp::from_der(timestamp_token_bytes) {
Ok(resp) => {
if resp.status.status != PkiStatus::Granted as u8
&& resp.status.status != PkiStatus::GrantedWithMods as u8
{
return Err(Error::ParseError(format!(
"Timestamp request not granted: {}",
resp.status.status
)));
}
let token_any = resp.time_stamp_token.ok_or(Error::ParseError(
"TimeStampResp missing timeStampToken".to_string(),
))?;
let bytes = token_any
.to_der()
.map_err(|e| Error::ParseError(format!("failed to re-encode token: {}", e)))?;
let token = ContentInfo::from_der(&bytes).map_err(|e| {
Error::ParseError(format!("failed to decode ContentInfo from token: {}", e))
})?;
(token, bytes)
}
Err(_) => {
let token = ContentInfo::from_der(timestamp_token_bytes).map_err(|e| {
Error::ParseError(format!("failed to decode TimeStampToken: {}", e))
})?;
(token, timestamp_token_bytes.to_vec())
}
};
if content_info.content_type != ID_SIGNED_DATA {
return Err(Error::ParseError(
"ContentInfo content type is not SignedData".to_string(),
));
}
let signed_data_der = content_info
.content
.to_der()
.map_err(|e| Error::ParseError(format!("failed to encode SignedData content: {}", e)))?;
let signed_data = SignedData::from_der(&signed_data_der)
.map_err(|e| Error::ParseError(format!("failed to decode SignedData: {}", e)))?;
if signed_data.encap_content_info.econtent_type != asn1::OID_TST_INFO {
return Err(Error::ParseError(
"encap content type is not TSTInfo".to_string(),
));
}
let tst_info = if let Some(content) = &signed_data.encap_content_info.econtent {
let tst_info_bytes = content.value();
TstInfo::from_der(tst_info_bytes)
.map_err(|e| Error::ParseError(format!("failed to decode TSTInfo: {}", e)))?
} else {
return Err(Error::NoTstInfo);
};
verify_message_imprint(&tst_info, signature_bytes)?;
let system_time = tst_info.gen_time.to_system_time();
let unix_duration = system_time
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| Error::ParseError("timestamp before epoch".to_string()))?;
let seconds = i64::try_from(unix_duration.as_secs())
.map_err(|_| Error::ParseError("invalid timestamp in TSTInfo".to_string()))?;
let timestamp = Timestamp::new(seconds, unix_duration.subsec_nanos() as i32)
.map_err(|_| Error::ParseError("invalid timestamp in TSTInfo".to_string()))?;
tracing::debug!("Extracted timestamp: {}", timestamp);
let tst_info_der = signed_data
.encap_content_info
.econtent
.as_ref()
.ok_or(Error::NoTstInfo)?
.value();
tracing::debug!("Starting CMS signature verification");
let signer_cert = verify_cms_signature(&signed_data, tst_info_der, &opts)?;
tracing::debug!("CMS signature verification completed successfully");
let embedded_certs = extract_certificates(&signed_data);
tracing::debug!("Starting TSA certificate chain validation");
validate_tsa_certificate_chain(&signer_cert, timestamp, &opts, &embedded_certs)?;
tracing::debug!("TSA certificate chain validation completed successfully");
Ok(TimestampResult { time: timestamp })
}
fn verify_message_imprint(tst_info: &TstInfo, signature_bytes: &[u8]) -> Result<()> {
use aws_lc_rs::digest::{digest, SHA256, SHA384, SHA512};
let message_imprint = &tst_info.message_imprint;
let hash_alg_oid = &message_imprint.hash_algorithm.algorithm;
let computed_hash = if hash_alg_oid == &OID_SHA256 {
digest(&SHA256, signature_bytes)
} else if hash_alg_oid == &OID_SHA384 {
digest(&SHA384, signature_bytes)
} else if hash_alg_oid == &OID_SHA512 {
digest(&SHA512, signature_bytes)
} else {
return Err(Error::ParseError(format!(
"unsupported hash algorithm: {}",
hash_alg_oid
)));
};
let expected_hash = message_imprint.hashed_message.as_bytes();
if computed_hash.as_ref() != expected_hash {
return Err(Error::HashMismatch {
expected: hex::encode(expected_hash),
actual: hex::encode(computed_hash),
});
}
Ok(())
}
fn get_signed_attrs_for_verification(attrs: &x509_cert::attr::Attributes) -> Result<Vec<u8>> {
use x509_cert::der::{asn1::SetOfVec, Encode};
let attrs_vec: Vec<x509_cert::attr::Attribute> = attrs.iter().cloned().collect();
let generic_set = SetOfVec::try_from(attrs_vec).map_err(|e| {
Error::SignatureVerificationError(format!("failed to create SetOfVec: {}", e))
})?;
generic_set.to_der().map_err(|e| {
Error::SignatureVerificationError(format!("failed to re-encode attributes: {}", e))
})
}
fn verify_cms_signature(
signed_data: &SignedData,
tst_info_der: &[u8],
opts: &VerifyOpts,
) -> Result<Certificate> {
let signer_info = signed_data
.signer_infos
.0
.get(0)
.ok_or_else(|| Error::SignatureVerificationError("no signer info found".to_string()))?;
let certificates = extract_certificates(signed_data);
let mut all_certs = certificates;
for tsa_cert in &opts.tsa_certificates {
use x509_cert::der::Decode;
if let Ok(cert) = Certificate::from_der(tsa_cert.as_ref()) {
all_certs.push(cert);
}
}
let signer_cert = find_signer_certificate(&signer_info.sid, &all_certs)?;
let signed_attrs = signer_info.signed_attrs.as_ref().ok_or_else(|| {
Error::SignatureVerificationError("no signed attributes found".to_string())
})?;
let digest_alg_oid = &signer_info.digest_alg.oid;
verify_message_digest_attribute(signed_attrs, tst_info_der, digest_alg_oid)?;
let signed_attrs_bytes = get_signed_attrs_for_verification(signed_attrs)?;
let signature_bytes = signer_info.signature.as_bytes();
verify_ecdsa_signature(
signature_bytes,
&signed_attrs_bytes,
&signer_cert,
digest_alg_oid,
)?;
Ok(signer_cert)
}
fn extract_certificates(signed_data: &SignedData) -> Vec<Certificate> {
let mut certificates = Vec::new();
if let Some(cert_set) = &signed_data.certificates {
for cert_choice in cert_set.0.iter() {
match cert_choice {
CertificateChoices::Certificate(cert) => {
certificates.push(cert.clone());
}
CertificateChoices::Other(_) => {
tracing::debug!("Skipping non-standard certificate format");
}
}
}
}
certificates
}
fn find_signer_certificate(
signer_id: &SignerIdentifier,
certificates: &[Certificate],
) -> Result<Certificate> {
match signer_id {
SignerIdentifier::IssuerAndSerialNumber(issuer_serial) => {
for cert in certificates {
if cert.tbs_certificate.issuer == issuer_serial.issuer
&& cert.tbs_certificate.serial_number == issuer_serial.serial_number
{
return Ok(cert.clone());
}
}
Err(Error::SignatureVerificationError(
"no certificate matches issuer and serial number".to_string(),
))
}
SignerIdentifier::SubjectKeyIdentifier(ski) => {
for cert in certificates {
if let Some(extensions) = &cert.tbs_certificate.extensions {
for ext in extensions.iter() {
use x509_cert::der::Decode;
if ext.extn_id.to_string() == "2.5.29.14" {
if let Ok(cert_ski) =
x509_cert::ext::pkix::SubjectKeyIdentifier::from_der(
ext.extn_value.as_bytes(),
)
{
if &cert_ski == ski {
return Ok(cert.clone());
}
}
}
}
}
}
Err(Error::SignatureVerificationError(
"no certificate matches subject key identifier".to_string(),
))
}
}
}
fn verify_message_digest_attribute(
signed_attrs: &x509_cert::attr::Attributes,
tst_info_der: &[u8],
digest_alg_oid: &ObjectIdentifier,
) -> Result<()> {
use aws_lc_rs::digest::{digest, SHA256, SHA384, SHA512};
use x509_cert::der::asn1::OctetStringRef;
use x509_cert::der::{Decode, Encode};
let message_digest_attr = signed_attrs
.iter()
.find(|attr| attr.oid == OID_MESSAGE_DIGEST)
.ok_or_else(|| {
Error::SignatureVerificationError(
"message-digest attribute not found in signed_attrs".to_string(),
)
})?;
if message_digest_attr.values.len() != 1 {
return Err(Error::SignatureVerificationError(
"message-digest attribute should have exactly one value".to_string(),
));
}
let message_digest_any = message_digest_attr.values.get(0).ok_or_else(|| {
Error::SignatureVerificationError(
"failed to get message-digest attribute value".to_string(),
)
})?;
let message_digest_der = message_digest_any.to_der().map_err(|e| {
Error::SignatureVerificationError(format!(
"failed to encode message-digest attribute value: {}",
e
))
})?;
let message_digest_octets = OctetStringRef::from_der(&message_digest_der).map_err(|e| {
Error::SignatureVerificationError(format!(
"failed to decode message-digest as OCTET STRING: {}",
e
))
})?;
let message_digest = message_digest_octets.as_bytes();
let content_hash = if digest_alg_oid == &OID_SHA256 {
digest(&SHA256, tst_info_der)
} else if digest_alg_oid == &OID_SHA384 {
digest(&SHA384, tst_info_der)
} else if digest_alg_oid == &OID_SHA512 {
digest(&SHA512, tst_info_der)
} else {
return Err(Error::ParseError(format!(
"unsupported signer digest algorithm: {}",
digest_alg_oid
)));
};
if content_hash.as_ref() != message_digest {
return Err(Error::HashMismatch {
expected: hex::encode(message_digest),
actual: hex::encode(content_hash),
});
}
Ok(())
}
fn verify_ecdsa_signature(
signature: &[u8],
message: &[u8],
certificate: &Certificate,
digest_alg_oid: &ObjectIdentifier,
) -> Result<()> {
use aws_lc_rs::signature::{
UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA256_ASN1, ECDSA_P384_SHA384_ASN1,
};
let spki = &certificate.tbs_certificate.subject_public_key_info;
let public_key_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| {
Error::SignatureVerificationError("invalid public key encoding".to_string())
})?;
if spki.algorithm.oid != OID_EC_PUBLIC_KEY {
return Err(Error::SignatureVerificationError(format!(
"not an EC key: {}",
spki.algorithm.oid
)));
}
let params = spki.algorithm.parameters.as_ref().ok_or_else(|| {
Error::SignatureVerificationError("missing EC curve parameters".to_string())
})?;
let curve_oid = params.decode_as::<ObjectIdentifier>().map_err(|e| {
Error::SignatureVerificationError(format!("failed to decode curve OID: {}", e))
})?;
let algorithm = match (&curve_oid, digest_alg_oid) {
(&OID_SECP256R1, &OID_SHA256) => &ECDSA_P256_SHA256_ASN1,
(&OID_SECP384R1, &OID_SHA256) => &ECDSA_P384_SHA256_ASN1,
(&OID_SECP384R1, &OID_SHA384) => &ECDSA_P384_SHA384_ASN1,
_ => {
return Err(Error::SignatureVerificationError(format!(
"unsupported curve/digest combination: {} / {}",
curve_oid, digest_alg_oid
)));
}
};
UnparsedPublicKey::new(algorithm, public_key_bytes)
.verify(message, signature)
.map_err(|_| Error::SignatureVerificationError("signature verification failed".to_string()))
}
fn validate_tsa_certificate_chain(
signer_cert: &Certificate,
timestamp: Timestamp,
opts: &VerifyOpts,
embedded_certs: &[Certificate],
) -> Result<()> {
use rustls_pki_types::{CertificateDer, UnixTime};
use x509_cert::der::Encode;
if opts.roots.is_empty() {
tracing::debug!("No trusted roots provided, skipping certificate chain validation");
return Ok(());
}
let signer_cert_der = signer_cert.to_der().map_err(|e| {
Error::CertificateValidationError(format!(
"failed to encode signer certificate to DER: {}",
e
))
})?;
let signer_cert_der = CertificateDer::from(signer_cert_der);
let end_entity_cert = EndEntityCert::try_from(&signer_cert_der).map_err(|e| {
Error::CertificateValidationError(format!("failed to parse end-entity certificate: {}", e))
})?;
let trust_anchors: Vec<_> = opts
.roots
.iter()
.map(|cert| {
anchor_from_trusted_cert(cert)
.map(|anchor| anchor.to_owned())
.map_err(|e| {
Error::CertificateValidationError(format!(
"failed to create trust anchor: {}",
e
))
})
})
.collect::<Result<Vec<_>>>()?;
let mut intermediate_ders: Vec<CertificateDer<'static>> = Vec::new();
for cert in embedded_certs {
if cert == signer_cert {
continue;
}
let cert_der = cert.to_der().map_err(|e| {
Error::CertificateValidationError(format!(
"failed to encode embedded certificate to DER: {}",
e
))
})?;
intermediate_ders.push(CertificateDer::from(cert_der).into_owned());
}
intermediate_ders.extend(opts.intermediates.iter().map(|c| c.clone().into_owned()));
tracing::debug!(
"Using {} embedded intermediate cert(s) + {} provided intermediate cert(s)",
embedded_certs.len().saturating_sub(1),
opts.intermediates.len()
);
let verification_time =
UnixTime::since_unix_epoch(std::time::Duration::from_secs(timestamp.as_second() as u64));
tracing::debug!(
"Verifying certificate chain at timestamp: {} (unix: {})",
timestamp,
timestamp.as_second()
);
end_entity_cert
.verify_for_usage(
ALL_VERIFICATION_ALGS,
&trust_anchors,
&intermediate_ders,
verification_time,
KeyUsage::required(ID_KP_TIME_STAMPING.as_bytes()),
None, None, )
.map_err(|e| {
Error::CertificateValidationError(format!(
"TSA certificate chain validation failed: {}",
e
))
})?;
tracing::debug!("TSA certificate chain validated successfully");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use base64::{engine::general_purpose::STANDARD, Engine as _};
const VALID_BUNDLE: &str = include_str!("../test_data/timestamps/valid_bundle.json");
const VALID_TRUSTED_ROOT: &str =
include_str!("../test_data/timestamps/valid_trusted_root.json");
const PAYLOAD_MISMATCH_BUNDLE: &str =
include_str!("../test_data/timestamps/payload_mismatch_bundle.json");
fn extract_timestamp_token(bundle_json: &str) -> Vec<u8> {
let bundle: serde_json::Value = serde_json::from_str(bundle_json).unwrap();
let timestamps =
&bundle["verificationMaterial"]["timestampVerificationData"]["rfc3161Timestamps"];
let signed_ts = timestamps[0]["signedTimestamp"].as_str().unwrap();
STANDARD.decode(signed_ts).unwrap()
}
fn extract_signature(bundle_json: &str) -> Vec<u8> {
let bundle: serde_json::Value = serde_json::from_str(bundle_json).unwrap();
let sig = bundle["messageSignature"]["signature"].as_str().unwrap();
STANDARD.decode(sig).unwrap()
}
fn extract_tsa_certs(trusted_root_json: &str) -> Vec<CertificateDer<'static>> {
let root: serde_json::Value = serde_json::from_str(trusted_root_json).unwrap();
let tsas = root["timestampAuthorities"].as_array().unwrap();
let mut certs = Vec::new();
for tsa in tsas {
let cert_chain = tsa["certChain"]["certificates"].as_array().unwrap();
for cert in cert_chain {
let raw_bytes = cert["rawBytes"].as_str().unwrap();
let der = STANDARD.decode(raw_bytes).unwrap();
certs.push(CertificateDer::from(der));
}
}
certs
}
#[test]
fn test_verify_valid_timestamp() {
let timestamp_token = extract_timestamp_token(VALID_BUNDLE);
let signature = extract_signature(VALID_BUNDLE);
let tsa_certs = extract_tsa_certs(VALID_TRUSTED_ROOT);
let root = tsa_certs.last().unwrap().clone();
let intermediates: Vec<_> = tsa_certs[..tsa_certs.len() - 1].to_vec();
let opts = VerifyOpts::new()
.with_root(root)
.with_intermediates(intermediates);
let result = verify_timestamp_response(×tamp_token, &signature, opts);
assert!(
result.is_ok(),
"Timestamp verification should succeed: {:?}",
result
);
let timestamp_result = result.unwrap();
assert!(
timestamp_result.time.as_second() > 0,
"Timestamp should be positive"
);
}
#[test]
fn test_verify_timestamp_without_roots_succeeds() {
let timestamp_token = extract_timestamp_token(VALID_BUNDLE);
let signature = extract_signature(VALID_BUNDLE);
let opts = VerifyOpts::new();
let result = verify_timestamp_response(×tamp_token, &signature, opts);
assert!(
result.is_ok(),
"Timestamp verification without roots should succeed: {:?}",
result
);
}
#[test]
fn test_verify_timestamp_payload_mismatch() {
let timestamp_token = extract_timestamp_token(PAYLOAD_MISMATCH_BUNDLE);
let signature = extract_signature(PAYLOAD_MISMATCH_BUNDLE);
let opts = VerifyOpts::new();
let result = verify_timestamp_response(×tamp_token, &signature, opts);
assert!(result.is_err(), "Payload mismatch should be detected");
let err = result.unwrap_err();
match err {
Error::HashMismatch { .. } => (),
other => panic!("Expected HashMismatch error, got: {:?}", other),
}
}
#[test]
fn test_verify_timestamp_invalid_token() {
let invalid_token = b"this is not a valid timestamp token";
let signature = extract_signature(VALID_BUNDLE);
let opts = VerifyOpts::new();
let result = verify_timestamp_response(invalid_token, &signature, opts);
assert!(
result.is_err(),
"Invalid token should fail verification: {:?}",
result
);
let err = result.unwrap_err();
match err {
Error::ParseError(_) => (),
other => panic!("Expected ParseError, got: {:?}", other),
}
}
#[test]
fn test_verify_timestamp_empty_signature() {
let timestamp_token = extract_timestamp_token(VALID_BUNDLE);
let empty_signature: &[u8] = &[];
let opts = VerifyOpts::new();
let result = verify_timestamp_response(×tamp_token, empty_signature, opts);
assert!(
result.is_err(),
"Empty signature should fail verification: {:?}",
result
);
let err = result.unwrap_err();
match err {
Error::HashMismatch { .. } => (),
other => panic!("Expected HashMismatch error, got: {:?}", other),
}
}
#[test]
fn test_verify_timestamp_wrong_signature() {
let timestamp_token = extract_timestamp_token(VALID_BUNDLE);
let wrong_signature = vec![0u8; 64];
let opts = VerifyOpts::new();
let result = verify_timestamp_response(×tamp_token, &wrong_signature, opts);
assert!(
result.is_err(),
"Wrong signature should fail verification: {:?}",
result
);
let err = result.unwrap_err();
match err {
Error::HashMismatch { .. } => (),
other => panic!("Expected HashMismatch error, got: {:?}", other),
}
}
const GITHUB_SHA384_BUNDLE: &str =
include_str!("../test_data/timestamps/github_sha384_bundle.json");
const GITHUB_TRUSTED_ROOT: &str =
include_str!("../test_data/timestamps/github_trusted_root.json");
fn extract_dsse_signature(bundle_json: &str) -> Vec<u8> {
let bundle: serde_json::Value = serde_json::from_str(bundle_json).unwrap();
let sig = bundle["dsseEnvelope"]["signatures"][0]["sig"]
.as_str()
.unwrap();
STANDARD.decode(sig).unwrap()
}
#[test]
fn test_verify_github_sha384_timestamp() {
let timestamp_token = extract_timestamp_token(GITHUB_SHA384_BUNDLE);
let signature = extract_dsse_signature(GITHUB_SHA384_BUNDLE);
let tsa_certs = extract_tsa_certs(GITHUB_TRUSTED_ROOT);
let opts = VerifyOpts::new().with_tsa_certificates(tsa_certs);
let result = verify_timestamp_response(×tamp_token, &signature, opts);
assert!(
result.is_ok(),
"GitHub SHA-384 timestamp should verify: {:?}",
result
);
}
#[test]
fn test_verify_opts_builder() {
let root = CertificateDer::from(vec![1, 2, 3]);
let intermediate = CertificateDer::from(vec![4, 5, 6]);
let tsa_certs = vec![
CertificateDer::from(vec![7, 8, 9]),
CertificateDer::from(vec![10, 11, 12]),
];
let opts = VerifyOpts::new()
.with_root(root.clone())
.with_intermediate(intermediate.clone())
.with_tsa_certificates(tsa_certs);
assert_eq!(opts.roots.len(), 1);
assert_eq!(opts.intermediates.len(), 1);
assert_eq!(opts.tsa_certificates.len(), 2);
}
#[test]
fn test_verify_opts_with_multiple_certs() {
let roots = vec![
CertificateDer::from(vec![1, 2, 3]),
CertificateDer::from(vec![4, 5, 6]),
];
let intermediates = vec![
CertificateDer::from(vec![7, 8, 9]),
CertificateDer::from(vec![10, 11, 12]),
];
let opts = VerifyOpts::new()
.with_roots(roots)
.with_intermediates(intermediates);
assert_eq!(opts.roots.len(), 2);
assert_eq!(opts.intermediates.len(), 2);
}
}