use crate::asn1::{self, PkiStatus, TimeStampResp, TstInfo};
use crate::error::{Error, Result};
use cms::signed_data::{SignedData, SignerIdentifier};
use const_oid::ObjectIdentifier;
use jiff::Timestamp;
use rustls_pki_types::CertificateDer;
use sigstore_crypto::{verify_signature, KeyAlgorithm};
use sigstore_types::{DerPublicKey, SignatureBytes, TsaAuthority};
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;
#[derive(Debug, Clone)]
pub(crate) 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_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(crate) struct TimestampResult {
pub time: Timestamp,
}
pub fn parse_timestamp_token(timestamp_token_bytes: &[u8]) -> Result<(TstInfo, SignedData)> {
use cms::content_info::ContentInfo;
use x509_cert::der::{Decode, Encode};
let content_info = 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)))?;
ContentInfo::from_der(&bytes).map_err(|e| {
Error::ParseError(format!("failed to decode ContentInfo from token: {}", e))
})?
}
Err(_) => {
ContentInfo::from_der(timestamp_token_bytes)
.map_err(|e| Error::ParseError(format!("failed to decode TimeStampToken: {}", e)))?
}
};
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);
};
Ok((tst_info, signed_data))
}
pub(crate) fn verify_timestamp_response(
timestamp_token_bytes: &[u8],
signature_bytes: &[u8],
opts: VerifyOpts<'_>,
) -> Result<TimestampResult> {
tracing::debug!("Starting RFC 3161 timestamp verification");
let (tst_info, signed_data) = parse_timestamp_token(timestamp_token_bytes)?;
verify_message_imprint(&tst_info, signature_bytes)?;
let timestamp = Timestamp::try_from(tst_info.gen_time.to_system_time())
.map_err(|_| Error::ParseError("invalid timestamp in TSTInfo".to_string()))?;
if timestamp < Timestamp::UNIX_EPOCH {
return Err(Error::ParseError("timestamp before epoch".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");
tracing::debug!("Starting TSA certificate chain validation");
validate_tsa_certificate_chain(&signer_cert, timestamp, &opts)?;
tracing::debug!("TSA certificate chain validation completed successfully");
Ok(TimestampResult { time: timestamp })
}
pub fn verify_timestamp_for_authority(
timestamp_token_bytes: &[u8],
signature_bytes: &[u8],
authority: &TsaAuthority,
) -> Result<Timestamp> {
let opts = VerifyOpts::new()
.with_root(CertificateDer::from(authority.root.as_bytes()))
.with_intermediates(
authority
.intermediates
.iter()
.map(|cert| CertificateDer::from(cert.as_bytes()))
.collect(),
)
.with_tsa_certificates(vec![CertificateDer::from(authority.leaf.as_bytes())]);
let result = verify_timestamp_response(timestamp_token_bytes, signature_bytes, opts)?;
let authorized = authority
.valid_for
.as_ref()
.is_none_or(|window| window.contains(result.time));
if !authorized {
return Err(Error::TimestampOutsideValidity { time: result.time });
}
Ok(result.time)
}
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()))?;
use x509_cert::der::Decode;
let trusted_signers = opts
.tsa_certificates
.iter()
.map(|certificate| {
Certificate::from_der(certificate.as_ref()).map_err(|e| {
Error::SignatureVerificationError(format!(
"failed to parse trusted TSA certificate: {e}"
))
})
})
.collect::<Result<Vec<_>>>()?;
let signer_cert = find_signer_certificate(&signer_info.sid, &trusted_signers)?;
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 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(
"CMS signer does not match any trusted TSA certificate".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(
"CMS signer does not match any trusted TSA certificate".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 x509_cert::der::Encode;
let spki = &certificate.tbs_certificate.subject_public_key_info;
let public_key = DerPublicKey::new(spki.to_der().map_err(|e| {
Error::SignatureVerificationError(format!("failed to encode signer public key: {e}"))
})?);
let algorithm = KeyAlgorithm::from_spki(&public_key)
.map_err(|e| Error::SignatureVerificationError(e.to_string()))?;
if !matches!(algorithm, KeyAlgorithm::EcdsaP256 | KeyAlgorithm::EcdsaP384) {
return Err(Error::SignatureVerificationError("not an EC key".into()));
}
let hash = match *digest_alg_oid {
OID_SHA256 => sigstore_types::HashAlgorithm::Sha2256,
OID_SHA384 => sigstore_types::HashAlgorithm::Sha2384,
OID_SHA512 => sigstore_types::HashAlgorithm::Sha2512,
_ => {
return Err(Error::SignatureVerificationError(format!(
"unsupported digest algorithm: {digest_alg_oid}"
)))
}
};
let scheme = algorithm
.resolve_signing_scheme(hash)
.map_err(|e| Error::SignatureVerificationError(e.to_string()))?;
verify_signature(
&public_key,
message,
&SignatureBytes::from_bytes(signature),
scheme,
)
.map_err(|e| Error::SignatureVerificationError(e.to_string()))
}
fn validate_tsa_certificate_chain(
signer_cert: &Certificate,
timestamp: Timestamp,
opts: &VerifyOpts,
) -> Result<()> {
use rustls_pki_types::{CertificateDer, UnixTime};
use x509_cert::der::Encode;
if opts.roots.is_empty() {
return Err(Error::CertificateValidationError(
"No trusted roots provided for TSA certificate validation".to_string(),
));
}
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 intermediate_ders: Vec<CertificateDer<'static>> = opts
.intermediates
.iter()
.map(|certificate| certificate.clone().into_owned())
.collect();
tracing::debug!(
"Using {} trusted intermediate cert(s)",
intermediate_ders.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 leaf = tsa_certs.first().unwrap().clone();
let root = tsa_certs.last().unwrap().clone();
let intermediates: Vec<_> = tsa_certs[1..tsa_certs.len() - 1].to_vec();
let opts = VerifyOpts::new()
.with_root(root)
.with_intermediates(intermediates)
.with_tsa_certificates(vec![leaf]);
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_fails() {
let timestamp_token = extract_timestamp_token(VALID_BUNDLE);
let signature = extract_signature(VALID_BUNDLE);
let tsa_certs = extract_tsa_certs(VALID_TRUSTED_ROOT);
let leaf = tsa_certs.first().unwrap().clone();
let opts = VerifyOpts::new().with_tsa_certificates(vec![leaf]);
let result = verify_timestamp_response(×tamp_token, &signature, opts);
assert!(
result.is_err(),
"Timestamp verification without roots should fail"
);
match result.unwrap_err() {
Error::CertificateValidationError(msg) => {
assert!(msg.contains("No trusted roots provided"));
}
other => panic!(
"Expected CertificateValidationError error, got: {:?}",
other
),
}
}
#[test]
fn test_verify_timestamp_accepts_pinned_signer_certificate() {
let timestamp_token = extract_timestamp_token(VALID_BUNDLE);
let signature = extract_signature(VALID_BUNDLE);
let tsa_certs = extract_tsa_certs(VALID_TRUSTED_ROOT);
let leaf = tsa_certs.first().unwrap().clone();
let root = tsa_certs.last().unwrap().clone();
let intermediates: Vec<_> = tsa_certs[1..tsa_certs.len() - 1].to_vec();
let opts = VerifyOpts::new()
.with_root(root)
.with_intermediates(intermediates)
.with_tsa_certificates(vec![leaf]);
let result = verify_timestamp_response(×tamp_token, &signature, opts);
assert!(
result.is_ok(),
"Timestamp signed by the pinned certificate should verify: {:?}",
result
);
}
#[test]
fn test_verify_timestamp_rejects_signer_not_in_pinned_certificates() {
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[1..tsa_certs.len() - 1].to_vec();
let opts = VerifyOpts::new()
.with_root(root.clone())
.with_intermediates(intermediates)
.with_tsa_certificates(vec![root]);
let result = verify_timestamp_response(×tamp_token, &signature, opts);
match result.unwrap_err() {
Error::SignatureVerificationError(msg) => {
assert!(
msg.contains("does not match any trusted TSA certificate"),
"unexpected message: {msg}"
);
}
other => panic!(
"Expected SignatureVerificationError error, got: {:?}",
other
),
}
}
fn fixture_authority(valid_for: Option<sigstore_types::TimeRange>) -> TsaAuthority {
use sigstore_types::DerCertificate;
let root: serde_json::Value = serde_json::from_str(VALID_TRUSTED_ROOT).unwrap();
let chain = root["timestampAuthorities"][0]["certChain"]["certificates"]
.as_array()
.unwrap();
let der = |index: usize| {
let raw = chain[index]["rawBytes"].as_str().unwrap();
DerCertificate::new(STANDARD.decode(raw).unwrap())
};
TsaAuthority {
leaf: der(0),
intermediates: (1..chain.len() - 1).map(der).collect(),
root: der(chain.len() - 1),
valid_for,
}
}
fn authority_token_time() -> Timestamp {
verify_timestamp_for_authority(
&extract_timestamp_token(VALID_BUNDLE),
&extract_signature(VALID_BUNDLE),
&fixture_authority(None),
)
.expect("fixture token should verify under an unrestricted authority")
}
fn window_around(
time: Timestamp,
start_offset: i64,
end_offset: i64,
) -> sigstore_types::TimeRange {
let at = |offset: i64| Timestamp::from_second(time.as_second() + offset).unwrap();
sigstore_types::TimeRange::new(at(start_offset), Some(at(end_offset)))
}
#[test]
fn test_authority_verification_accepts_token_within_window() {
let time = authority_token_time();
let authority = fixture_authority(Some(window_around(time, -3600, 3600)));
let result = verify_timestamp_for_authority(
&extract_timestamp_token(VALID_BUNDLE),
&extract_signature(VALID_BUNDLE),
&authority,
);
assert_eq!(result.unwrap(), time);
}
#[test]
fn test_authority_verification_rejects_token_outside_window() {
let time = authority_token_time();
let authority = fixture_authority(Some(window_around(time, -7200, -3600)));
let result = verify_timestamp_for_authority(
&extract_timestamp_token(VALID_BUNDLE),
&extract_signature(VALID_BUNDLE),
&authority,
);
match result.unwrap_err() {
Error::TimestampOutsideValidity { time: rejected } => assert_eq!(rejected, time),
other => panic!("Expected TimestampOutsideValidity, got: {:?}", other),
}
}
#[test]
fn test_authority_verification_rejects_token_signed_by_other_leaf() {
let time = authority_token_time();
let mut authority = fixture_authority(Some(window_around(time, -3600, 3600)));
authority.leaf = authority.root.clone();
let result = verify_timestamp_for_authority(
&extract_timestamp_token(VALID_BUNDLE),
&extract_signature(VALID_BUNDLE),
&authority,
);
match result.unwrap_err() {
Error::SignatureVerificationError(msg) => {
assert!(
msg.contains("does not match any trusted TSA certificate"),
"unexpected message: {msg}"
);
}
other => panic!("Expected SignatureVerificationError, got: {:?}", other),
}
}
#[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 root = tsa_certs.last().unwrap().clone();
let intermediates = tsa_certs[..tsa_certs.len() - 1].to_vec();
let opts = VerifyOpts::new()
.with_root(root)
.with_intermediates(intermediates)
.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_parse_timestamp_token_extracts_nonce() {
let timestamp_token = extract_timestamp_token(VALID_BUNDLE);
let (tst_info, _) = parse_timestamp_token(×tamp_token).unwrap();
assert!(tst_info.nonce.is_some());
}
}