use crate::error::{Error, Result};
use const_oid::db::rfc5912::ID_KP_CODE_SIGNING;
use rustls_pki_types::{CertificateDer, UnixTime};
use sigstore_crypto::CertificateInfo;
use sigstore_trust_root::{TrustedRoot, TsaAuthority};
use sigstore_types::bundle::VerificationMaterialContent;
use sigstore_types::{Bundle, DerPublicKey, KindVersion, SignatureBytes, SignatureContent};
use webpki::{EndEntityCert, KeyUsage, ALL_VERIFICATION_ALGS};
pub(crate) type FulcioAnchor = (
rustls_pki_types::TrustAnchor<'static>,
Option<sigstore_types::TimeRange>,
);
pub fn extract_signature(content: &SignatureContent) -> SignatureBytes {
match content {
SignatureContent::MessageSignature(msg_sig) => msg_sig.signature.clone(),
SignatureContent::DsseEnvelope(envelope) => envelope.signature.sig.clone(),
}
}
pub fn extract_tsa_timestamps(
bundle: &Bundle,
signature_bytes: &[u8],
trusted_root: &TrustedRoot,
) -> Result<Vec<jiff::Timestamp>> {
let rfc3161_timestamps = &bundle
.verification_material
.timestamp_verification_data
.rfc3161_timestamps;
if rfc3161_timestamps.is_empty() {
return Ok(Vec::new());
}
let authorities = trusted_root.tsa_authorities();
let mut timestamps = Vec::new();
for ts in rfc3161_timestamps {
timestamps.push(verify_timestamp_against_authorities(
ts.signed_timestamp.as_bytes(),
signature_bytes,
&authorities,
)?);
}
Ok(timestamps)
}
fn verify_timestamp_against_authorities(
ts_bytes: &[u8],
signature_bytes: &[u8],
authorities: &[TsaAuthority],
) -> Result<jiff::Timestamp> {
let mut rejected_time = None;
let mut last_error: Option<String> = None;
for authority in authorities {
match sigstore_tsa::verify_timestamp_for_authority(ts_bytes, signature_bytes, authority) {
Ok(time) => return Ok(time),
Err(sigstore_tsa::Error::TimestampOutsideValidity { time }) => {
rejected_time = Some(time);
}
Err(e) => last_error = Some(e.to_string()),
}
}
if let Some(time) = rejected_time {
return Err(Error::Verification(format!(
"TSA timestamp {} is outside the validity period of the timestamp authority that signed it",
time
)));
}
Err(Error::Verification(format!(
"TSA timestamp verification failed: {}",
last_error
.unwrap_or_else(|| "no timestamp authorities configured in trusted root".to_string())
)))
}
pub fn has_v2_tlog_entries(bundle: &Bundle) -> bool {
bundle
.verification_material
.tlog_entries
.iter()
.any(|entry| entry.kind_version == KindVersion::HashedRekordV002)
}
fn extract_v1_integrated_times_with_promise(
bundle: &Bundle,
rekor_keys: &sigstore_crypto::Keyring,
) -> Result<Vec<jiff::Timestamp>> {
let mut times = Vec::new();
for entry in &bundle.verification_material.tlog_entries {
let is_v1 = matches!(
entry.kind_version,
KindVersion::HashedRekordV001 | KindVersion::DsseV001 | KindVersion::IntotoV002
);
if !is_v1 || entry.inclusion_promise.is_none() {
continue;
}
if let Some(time) = entry.integrated_time {
crate::verify_impl::tlog::verify_set(entry, rekor_keys)?;
times.push(time);
}
}
Ok(times)
}
pub fn determine_validation_times(
bundle: &Bundle,
signature: &SignatureBytes,
trusted_root: &TrustedRoot,
rekor_keys: &sigstore_crypto::Keyring,
) -> Result<Vec<jiff::Timestamp>> {
let mut times = extract_tsa_timestamps(bundle, signature.as_bytes(), trusted_root)?;
times.extend(extract_v1_integrated_times_with_promise(
bundle, rekor_keys,
)?);
if !times.is_empty() {
return Ok(times);
}
let is_v2 = has_v2_tlog_entries(bundle);
if is_v2 {
Err(Error::Verification(
"V2 bundle requires RFC3161 timestamp but none could be verified. \
V2 tlog entries have no integrated time by design. \
Ensure TSA certificates are present in the trusted root."
.to_string(),
))
} else {
Err(Error::Verification(
"No verified timestamp found. V1 bundles require either an RFC3161 timestamp \
or a tlog entry with both an integrated time and an inclusion_promise (SET)."
.to_string(),
))
}
}
pub fn validate_certificate_time(
validation_time: jiff::Timestamp,
cert_info: &CertificateInfo,
) -> Result<()> {
if validation_time < cert_info.not_before {
return Err(Error::Verification(format!(
"certificate not yet valid: validation time {} is before not_before {}",
validation_time, cert_info.not_before
)));
}
if validation_time > cert_info.not_after {
return Err(Error::Verification(format!(
"certificate has expired: validation time {} is after not_after {}",
validation_time, cert_info.not_after
)));
}
Ok(())
}
pub fn verify_certificate_chain(
verification_material: &VerificationMaterialContent,
validation_time: jiff::Timestamp,
fulcio_anchors: &[FulcioAnchor],
) -> Result<DerPublicKey> {
let (ee_cert_der, intermediate_ders) = match verification_material {
VerificationMaterialContent::Certificate(cert) => {
(cert.raw_bytes.as_bytes().to_vec(), Vec::new())
}
VerificationMaterialContent::X509CertificateChain { certificates } => {
if certificates.is_empty() {
return Err(Error::Verification("no certificates in chain".to_string()));
}
let ee = certificates[0].raw_bytes.as_bytes().to_vec();
let intermediates: Vec<Vec<u8>> = certificates[1..]
.iter()
.map(|c| c.raw_bytes.as_bytes().to_vec())
.collect();
(ee, intermediates)
}
VerificationMaterialContent::PublicKey { .. } => {
return Err(Error::Verification(
"public key verification not yet supported".to_string(),
));
}
};
let trust_anchors: Vec<_> = fulcio_anchors
.iter()
.filter(|(_, window)| window.is_none_or(|range| range.contains(validation_time)))
.map(|(anchor, _)| anchor.clone())
.collect();
if trust_anchors.is_empty() {
return Err(Error::Verification(format!(
"no Fulcio trust anchor is valid at authenticated signing time {validation_time}"
)));
}
let intermediate_certs: Vec<CertificateDer<'static>> = intermediate_ders
.into_iter()
.map(|der| CertificateDer::from(der).into_owned())
.collect();
let ee_cert_der_ref = CertificateDer::from(ee_cert_der.as_slice());
let end_entity_cert = EndEntityCert::try_from(&ee_cert_der_ref).map_err(|e| {
Error::Verification(format!("failed to parse end-entity certificate: {}", e))
})?;
let verification_time = UnixTime::since_unix_epoch(std::time::Duration::from_secs(
validation_time.as_second() as u64,
));
let path = end_entity_cert
.verify_for_usage(
ALL_VERIFICATION_ALGS,
&trust_anchors,
&intermediate_certs,
verification_time,
KeyUsage::required(ID_KP_CODE_SIGNING.as_bytes()),
None, None, )
.map_err(|e| Error::Verification(format!("certificate chain validation failed: {}", e)))?;
tracing::debug!("Certificate chain validated successfully with CODE_SIGNING EKU");
issuer_spki_from_path(&path)
}
fn issuer_spki_from_path(path: &webpki::VerifiedPath) -> Result<DerPublicKey> {
let der = match path.intermediate_certificates().next() {
Some(issuer) => issuer.subject_public_key_info().as_ref().to_vec(),
None => {
use x509_cert::der::{Any, Encode, Tag};
let spki = path.anchor().subject_public_key_info.as_ref();
Any::new(Tag::Sequence, spki)
.and_then(|any| any.to_der())
.map_err(|e| Error::Verification(format!("failed to encode issuer SPKI: {e}")))?
}
};
Ok(DerPublicKey::new(der))
}
#[cfg(test)]
mod tests {
use super::*;
use sigstore_types::Bundle;
#[test]
fn determine_validation_times_returns_all_verified_sources() {
let trusted_root =
TrustedRoot::from_json(sigstore_trust_root::SIGSTORE_PRODUCTION_TRUSTED_ROOT).unwrap();
let bundle = Bundle::from_json(include_str!(
"../../test_data/bundles/cosign-v3-blob.sigstore.json"
))
.unwrap();
let signature = extract_signature(&bundle.content);
let times = determine_validation_times(
&bundle,
&signature,
&trusted_root,
&trusted_root.rekor_keys().unwrap(),
)
.unwrap();
assert_eq!(
times.len(),
2,
"expected one TSA timestamp and one integratedTime, got {times:?}"
);
let integrated_time = bundle.verification_material.tlog_entries[0]
.integrated_time
.unwrap();
assert!(times.contains(&integrated_time));
}
#[test]
fn sct_verifies_with_multiple_same_named_intermediates() {
let validation_time = jiff::Timestamp::from_second(1_783_488_311).unwrap();
let trusted_root = TrustedRoot::from_json(include_str!(
"../../test_data/sct-multi-intermediate/staging_trusted_root.json"
))
.expect("failed to load staging trusted root");
let bundle = Bundle::from_json(include_str!(
"../../test_data/sct-multi-intermediate/staging_bundle.sigstore.json"
))
.expect("failed to parse staging bundle");
let material = &bundle.verification_material.content;
use x509_cert::der::Decode;
use x509_cert::Certificate;
let same_named_intermediates = trusted_root
.fulcio_certs()
.iter()
.filter_map(|der| Certificate::from_der(der).ok())
.filter(|c| {
c.tbs_certificate
.subject
.to_string()
.contains("sigstore-intermediate")
})
.count();
assert!(
same_named_intermediates >= 2,
"fixture must contain multiple sigstore-intermediate CAs to exercise the bug, found {same_named_intermediates}"
);
let verifier = crate::Verifier::new(&trusted_root).unwrap();
let issuer_spki =
verify_certificate_chain(material, validation_time, &verifier.fulcio_anchors)
.expect("certificate chain should verify against the staging root");
let cert = match material {
VerificationMaterialContent::Certificate(cert) => &cert.raw_bytes,
VerificationMaterialContent::X509CertificateChain { certificates } => {
&certificates[0].raw_bytes
}
_ => panic!("fixture must have a signing certificate"),
};
super::super::sct::verify_sct(cert.as_bytes(), issuer_spki.as_bytes(), &verifier.ct_keys)
.expect("SCT verification should succeed once the correct issuer is selected");
}
mod tsa_authority_binding {
use super::*;
use serde_json::{json, Value};
const TSA_BUNDLE: &str = include_str!("../../test_data/timestamps/valid_bundle.json");
const TSA_TRUSTED_ROOT: &str =
include_str!("../../test_data/timestamps/valid_trusted_root.json");
const GITHUB_TSA_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 verify_bundle_timestamp(root: &TrustedRoot) -> Result<Option<i64>> {
let bundle = Bundle::from_json(TSA_BUNDLE).unwrap();
let signature = extract_signature(&bundle.content);
let timestamps = extract_tsa_timestamps(&bundle, signature.as_bytes(), root)?;
Ok(timestamps.first().map(|t| t.as_second()))
}
fn token_time() -> i64 {
let root = TrustedRoot::from_json(TSA_TRUSTED_ROOT).unwrap();
verify_bundle_timestamp(&root)
.expect("fixture bundle should verify against its own trusted root")
.expect("fixture bundle carries a timestamp")
}
fn iso(seconds: i64) -> String {
jiff::Timestamp::from_second(seconds).unwrap().to_string()
}
fn root_with(valid_for: Option<Value>, extra_authorities: Vec<Value>) -> TrustedRoot {
let mut root: Value = serde_json::from_str(TSA_TRUSTED_ROOT).unwrap();
let tsas = root["timestampAuthorities"].as_array_mut().unwrap();
match valid_for {
Some(window) => tsas[0]["validFor"] = window,
None => {
tsas[0].as_object_mut().unwrap().remove("validFor");
}
}
tsas.extend(extra_authorities);
TrustedRoot::from_json(&root.to_string()).unwrap()
}
fn unrelated_authority(valid_for: Value) -> Value {
let github: Value = serde_json::from_str(GITHUB_TRUSTED_ROOT).unwrap();
let mut tsa = github["timestampAuthorities"][0].clone();
tsa["validFor"] = valid_for;
tsa
}
#[test]
fn accepts_token_within_signing_authoritys_window() {
let time = token_time();
let covering = json!({ "start": iso(time - 3600), "end": iso(time + 3600) });
let root = root_with(Some(covering.clone()), vec![unrelated_authority(covering)]);
assert_eq!(verify_bundle_timestamp(&root).unwrap(), Some(time));
}
#[test]
fn accepts_token_from_authority_without_validity_window() {
let root = root_with(None, vec![]);
assert_eq!(verify_bundle_timestamp(&root).unwrap(), Some(token_time()));
}
#[test]
fn accepts_token_exactly_at_window_boundaries() {
let time = token_time();
let root = root_with(
Some(json!({ "start": iso(time), "end": iso(time) })),
vec![],
);
assert_eq!(verify_bundle_timestamp(&root).unwrap(), Some(time));
}
#[test]
fn rejects_token_just_outside_window_boundary() {
let time = token_time();
let root = root_with(
Some(json!({ "start": iso(time - 3600), "end": iso(time - 1) })),
vec![],
);
let err = verify_bundle_timestamp(&root).unwrap_err();
assert!(
err.to_string().contains("outside the validity period"),
"unexpected error: {err}"
);
}
#[test]
fn rejects_token_outside_its_authoritys_window_despite_unrelated_covering_authority() {
let time = token_time();
let excluding = json!({ "start": iso(time - 7200), "end": iso(time - 3600) });
let covering = json!({ "start": iso(time - 3600), "end": iso(time + 3600) });
let root = root_with(Some(excluding), vec![unrelated_authority(covering)]);
let err = verify_bundle_timestamp(&root).unwrap_err();
assert!(
err.to_string().contains("outside the validity period"),
"unexpected error: {err}"
);
}
#[test]
fn rejects_empty_chain_authority_lending_its_window() {
let time = token_time();
let excluding = json!({ "start": iso(time - 7200), "end": iso(time - 3600) });
let empty_chain_authority = json!({
"certChain": { "certificates": [] },
"validFor": { "start": iso(time - 3600), "end": iso(time + 3600) },
});
let root = root_with(Some(excluding), vec![empty_chain_authority]);
let err = verify_bundle_timestamp(&root).unwrap_err();
assert!(
err.to_string().contains("outside the validity period"),
"unexpected error: {err}"
);
}
#[test]
fn rejects_token_signed_by_no_configured_authority() {
let time = token_time();
let covering = json!({ "start": iso(time - 3600), "end": iso(time + 3600) });
let mut root: Value = serde_json::from_str(TSA_TRUSTED_ROOT).unwrap();
root["timestampAuthorities"] = json!([unrelated_authority(covering)]);
let root = TrustedRoot::from_json(&root.to_string()).unwrap();
let err = verify_bundle_timestamp(&root).unwrap_err();
assert!(
err.to_string()
.contains("TSA timestamp verification failed"),
"unexpected error: {err}"
);
}
#[test]
fn rejects_token_when_no_authorities_configured() {
let mut root: Value = serde_json::from_str(TSA_TRUSTED_ROOT).unwrap();
root["timestampAuthorities"] = json!([]);
let root = TrustedRoot::from_json(&root.to_string()).unwrap();
let err = verify_bundle_timestamp(&root).unwrap_err();
assert!(
err.to_string()
.contains("no timestamp authorities configured"),
"unexpected error: {err}"
);
}
#[test]
fn selects_signing_authority_among_many_real_authorities() {
let bundle = Bundle::from_json(GITHUB_TSA_BUNDLE).unwrap();
let signature = extract_signature(&bundle.content);
let root = TrustedRoot::from_json(GITHUB_TRUSTED_ROOT).unwrap();
let timestamps = extract_tsa_timestamps(&bundle, signature.as_bytes(), &root)
.expect("GitHub bundle timestamp should verify");
let time = timestamps
.first()
.expect("GitHub bundle carries a timestamp");
assert!(time.as_second() > 0);
}
}
}