From d143e3720d0ad42e9f12183a5e7f15e6aaf854a6 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Sat, 5 Sep 2026 19:03:40 +0000 Subject: [PATCH] feat(verify)!: validate and prepare static trust in a fallible constructor Signed-off-by: Wolf Vollprecht --- README.md | 2 +- crates/sigstore-verify/README.md | 10 +- .../sigstore-verify/examples/verify_bundle.rs | 5 +- crates/sigstore-verify/src/error.rs | 8 ++ crates/sigstore-verify/src/verify.rs | 110 ++++++++++++++++-- .../src/verify_impl/helpers.rs | 56 ++++----- crates/sigstore-verify/src/verify_impl/sct.rs | 12 +- .../sigstore-verify/src/verify_impl/tlog.rs | 29 ++--- .../tests/verification_tests.rs | 41 ++++++- 9 files changed, 204 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index a397f2c7..bfa5d90b 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ use sigstore_trust_root::TrustedRoot; // Load the trusted root via TUF (recommended - ensures up-to-date trust material) let root = TrustedRoot::production().await?; -let verifier = Verifier::new(&root); +let verifier = Verifier::new(&root)?; // Parse the bundle (contains signature, certificate, transparency log entry) let bundle: sigstore_types::Bundle = serde_json::from_str(&bundle_json)?; diff --git a/crates/sigstore-verify/README.md b/crates/sigstore-verify/README.md index 4acdf7d6..c3e7d9f6 100644 --- a/crates/sigstore-verify/README.md +++ b/crates/sigstore-verify/README.md @@ -23,6 +23,14 @@ This crate provides high-level APIs for verifying Sigstore signatures. It handle 5. Verify timestamps if present 6. Check identity against policy (optional) +## Verifier construction + +`Verifier::new(&root)?` prepares Rekor/CT keyrings and Fulcio trust anchors before +reading artifacts. Invalid certificates, keys, duplicate log IDs and reversed +validity windows are errors at construction. Unsupported configured keys must be +removed explicitly rather than silently ignored. Key/authority activation times +are still checked when verifying, so long-lived verifiers do not freeze time. + ## Verification results `VerificationResult` is created only by successful verification and exposes @@ -61,7 +69,7 @@ let digest = Sha256Hash::from_hex("b94d27b9...")?; let result = verify(digest, &bundle, &policy, &root)?; // Or use a Verifier directly; it also offers the same inputs -let verifier = Verifier::new(&root); +let verifier = Verifier::new(&root)?; let result = verifier.verify(artifact_bytes.as_slice(), &bundle, &policy)?; // Stream a large artifact in constant memory diff --git a/crates/sigstore-verify/examples/verify_bundle.rs b/crates/sigstore-verify/examples/verify_bundle.rs index 9d80aad3..7b69712f 100644 --- a/crates/sigstore-verify/examples/verify_bundle.rs +++ b/crates/sigstore-verify/examples/verify_bundle.rs @@ -261,7 +261,10 @@ async fn main() { } // Verify - let verifier = Verifier::new(&trusted_root); + let verifier = Verifier::new(&trusted_root).unwrap_or_else(|e| { + eprintln!("Error preparing trusted root: {e}"); + process::exit(2); + }); let result = if is_digest { // Parse digest (sha256:hex...) let hex_digest = artifact_or_digest.strip_prefix("sha256:").unwrap(); diff --git a/crates/sigstore-verify/src/error.rs b/crates/sigstore-verify/src/error.rs index d0ff230f..73245b48 100644 --- a/crates/sigstore-verify/src/error.rs +++ b/crates/sigstore-verify/src/error.rs @@ -21,6 +21,14 @@ pub enum Error { #[error("Bundle error: {0}")] Bundle(#[from] sigstore_bundle::Error), + /// Invalid trust-root configuration. + #[error("Trust root error: {0}")] + TrustRoot(#[from] sigstore_trust_root::Error), + + /// A configured authority certificate could not be parsed. + #[error("invalid trusted certificate: {0}")] + TrustedCertificate(#[source] webpki::Error), + /// Failed to read artifact input. #[error("failed to read artifact: {0}")] ArtifactRead(#[source] std::io::Error), diff --git a/crates/sigstore-verify/src/verify.rs b/crates/sigstore-verify/src/verify.rs index 854c7c5d..599236f0 100644 --- a/crates/sigstore-verify/src/verify.rs +++ b/crates/sigstore-verify/src/verify.rs @@ -267,6 +267,9 @@ impl VerificationResult { pub struct Verifier { /// Trusted root containing verification material trusted_root: TrustedRoot, + rekor_keys: sigstore_crypto::Keyring, + pub(crate) ct_keys: Vec<(SigningScheme, sigstore_crypto::Keyring)>, + pub(crate) fulcio_anchors: Vec, } impl Verifier { @@ -274,10 +277,85 @@ impl Verifier { /// /// The trusted root is required and contains all cryptographic material /// needed for verification (Fulcio CA certs, Rekor keys, TSA certs, etc.) - pub fn new(trusted_root: &TrustedRoot) -> Self { - Self { - trusted_root: trusted_root.clone(), + /// + /// Prepares configured keys and certificates, rejecting malformed, + /// unsupported, duplicate or invalid-window trust material before artifact + /// I/O. Empty authority lists are allowed for managed-key or relaxed policies. + pub fn new(trusted_root: &TrustedRoot) -> Result { + use sigstore_crypto::{Keyring, VerificationKey}; + use sigstore_types::Sha256Hash; + let mut ids = std::collections::HashSet::new(); + let logs = trusted_root + .tlogs + .iter() + .map(|log| (true, &log.log_id, &log.public_key)) + .chain( + trusted_root + .ctlogs + .iter() + .map(|log| (false, &log.log_id, &log.public_key)), + ); + for (is_rekor, id, public_key) in logs { + validate_trust_window(public_key.valid_for)?; + let id = Sha256Hash::try_from_slice(&id.key_id.decode()?)?; + if !ids.insert((is_rekor, id)) { + return Err(Error::Verification(format!( + "duplicate trusted log ID: {}", + id.to_hex() + ))); + } + if is_rekor { + VerificationKey::from_spki(&public_key.raw_bytes)?; + } else { + VerificationKey::from_spki(&public_key.raw_bytes).or_else(|_| { + VerificationKey::from_der(&public_key.raw_bytes, SigningScheme::RsaPkcs1Sha256) + })?; + } + } + let rekor_keys = trusted_root.rekor_keys()?; + let ct_keys: Vec<(SigningScheme, Keyring)> = [ + SigningScheme::EcdsaP256Sha256, + SigningScheme::EcdsaP384Sha384, + SigningScheme::RsaPkcs1Sha256, + SigningScheme::RsaPkcs1Sha384, + SigningScheme::RsaPkcs1Sha512, + ] + .into_iter() + .map(|scheme| Ok((scheme, trusted_root.ctfe_keys(scheme)?))) + .collect::>()?; + let mut fulcio_anchors = Vec::new(); + let authorities = trusted_root + .certificate_authorities + .iter() + .map(|ca| (true, &ca.cert_chain, ca.valid_for)) + .chain( + trusted_root + .timestamp_authorities + .iter() + .map(|tsa| (false, &tsa.cert_chain, tsa.valid_for)), + ); + for (is_fulcio, chain, window) in authorities { + validate_trust_window(window)?; + if chain.certificates.is_empty() { + return Err(Error::Verification( + "trusted authority has an empty certificate chain".into(), + )); + } + for cert in &chain.certificates { + let der = rustls_pki_types::CertificateDer::from(cert.raw_bytes.as_bytes()); + let anchor = + webpki::anchor_from_trusted_cert(&der).map_err(Error::TrustedCertificate)?; + if is_fulcio { + fulcio_anchors.push((anchor.to_owned(), window)); + } + } } + Ok(Self { + trusted_root: trusted_root.clone(), + rekor_keys, + ct_keys, + fulcio_anchors, + }) } /// Verify an artifact against a bundle @@ -295,7 +373,7 @@ impl Verifier { /// /// # async fn example() -> Result<(), Box> { /// let trusted_root = TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT)?; - /// let verifier = Verifier::new(&trusted_root); + /// let verifier = Verifier::new(&trusted_root)?; /// let bundle: Bundle = todo!(); /// let policy = VerificationPolicy::any_identity(); /// @@ -419,6 +497,7 @@ impl Verifier { bundle, &signature, &self.trusted_root, + &self.rekor_keys, )?; // (1): Verify that the signing certificate chains to the root of trust, @@ -439,7 +518,7 @@ impl Verifier { issuer_spki = Some(crate::verify_impl::helpers::verify_certificate_chain( &bundle.verification_material.content, validation_time, - &self.trusted_root, + &self.fulcio_anchors, )?); // Also verify the certificate is within its validity period @@ -458,7 +537,7 @@ impl Verifier { crate::verify_impl::sct::verify_sct( cert.as_bytes(), issuer_spki.as_bytes(), - &self.trusted_root, + &self.ct_keys, )?; result.sct_verified = true; } @@ -514,7 +593,7 @@ impl Verifier { if policy.verify_tlog { let integrated_time = crate::verify_impl::tlog::verify_tlog_entries( bundle, - &self.trusted_root, + &self.rekor_keys, cert_info.not_before, cert_info.not_after, )?; @@ -584,7 +663,7 @@ impl Verifier { /// /// # fn example() -> Result<(), Box> { /// let trusted_root = TrustedRoot::from_file("trusted_root.json")?; - /// let verifier = Verifier::new(&trusted_root); + /// let verifier = Verifier::new(&trusted_root)?; /// let bundle = Bundle::from_json(&std::fs::read_to_string("artifact.sigstore.json")?)?; /// let public_key = DerPublicKey::from_pem(&std::fs::read_to_string("key.pub")?)?; /// let artifact = std::fs::read("artifact.txt")?; @@ -678,7 +757,7 @@ impl Verifier { // SETs) without certificate time validation. if policy.verify_tlog { for entry in &bundle.verification_material.tlog_entries { - crate::verify_impl::tlog::verify_entry_inclusion(entry, &self.trusted_root)?; + crate::verify_impl::tlog::verify_entry_inclusion(entry, &self.rekor_keys)?; let is_rekor_v1 = matches!( entry.kind_version, @@ -733,6 +812,15 @@ impl Verifier { } } +fn validate_trust_window(window: Option) -> Result<()> { + if window.is_some_and(|range| range.end.is_some_and(|end| end < range.start)) { + return Err(Error::Verification( + "trusted validity window ends before it starts".into(), + )); + } + Ok(()) +} + /// Check that a `MessageSignature`'s declared `messageDigest`, if any, matches /// the artifact. fn verify_message_digest_binding( @@ -904,7 +992,7 @@ pub fn verify<'a>( policy: &VerificationPolicy, trusted_root: &TrustedRoot, ) -> Result { - Verifier::new(trusted_root).verify(artifact, bundle, policy) + Verifier::new(trusted_root)?.verify(artifact, bundle, policy) } /// Convenience function to verify a managed-key bundle with a caller-supplied @@ -947,7 +1035,7 @@ pub fn verify_with_key<'a>( policy: &PublicKeyVerificationPolicy, trusted_root: &TrustedRoot, ) -> Result { - Verifier::new(trusted_root).verify_with_key(artifact, bundle, public_key, policy) + Verifier::new(trusted_root)?.verify_with_key(artifact, bundle, public_key, policy) } #[cfg(test)] diff --git a/crates/sigstore-verify/src/verify_impl/helpers.rs b/crates/sigstore-verify/src/verify_impl/helpers.rs index 52f302ec..8dd77e36 100644 --- a/crates/sigstore-verify/src/verify_impl/helpers.rs +++ b/crates/sigstore-verify/src/verify_impl/helpers.rs @@ -10,7 +10,12 @@ 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::{anchor_from_trusted_cert, EndEntityCert, KeyUsage, ALL_VERIFICATION_ALGS}; +use webpki::{EndEntityCert, KeyUsage, ALL_VERIFICATION_ALGS}; + +pub(crate) type FulcioAnchor = ( + rustls_pki_types::TrustAnchor<'static>, + Option, +); /// Extract signature from bundle content (needed for TSA verification). /// @@ -131,7 +136,7 @@ pub fn has_v2_tlog_entries(bundle: &Bundle) -> bool { /// Returns every authenticated integrated time. fn extract_v1_integrated_times_with_promise( bundle: &Bundle, - trusted_root: &TrustedRoot, + rekor_keys: &sigstore_crypto::Keyring, ) -> Result> { let mut times = Vec::new(); @@ -147,7 +152,7 @@ fn extract_v1_integrated_times_with_promise( } if let Some(time) = entry.integrated_time { - crate::verify_impl::tlog::verify_set(entry, trusted_root)?; + crate::verify_impl::tlog::verify_set(entry, rekor_keys)?; times.push(time); } } @@ -186,11 +191,11 @@ pub fn determine_validation_times( bundle: &Bundle, signature: &SignatureBytes, trusted_root: &TrustedRoot, + rekor_keys: &sigstore_crypto::Keyring, ) -> Result> { let mut times = extract_tsa_timestamps(bundle, signature.as_bytes(), trusted_root)?; times.extend(extract_v1_integrated_times_with_promise( - bundle, - trusted_root, + bundle, rekor_keys, )?); if !times.is_empty() { @@ -252,7 +257,7 @@ pub fn validate_certificate_time( pub fn verify_certificate_chain( verification_material: &VerificationMaterialContent, validation_time: jiff::Timestamp, - trusted_root: &TrustedRoot, + fulcio_anchors: &[FulcioAnchor], ) -> Result { // Extract the end-entity certificate and any intermediates from the bundle let (ee_cert_der, intermediate_ders) = match verification_material { @@ -277,24 +282,13 @@ pub fn verify_certificate_chain( } }; - // Get Fulcio certificates from trusted root to use as trust anchors - let fulcio_certs = trusted_root.fulcio_certs(); - - if fulcio_certs.is_empty() { - return Err(Error::Verification( - "no Fulcio certificates in trusted root".to_string(), - )); - } - - // Build trust anchors from Fulcio root certificates - let trust_anchors: Vec<_> = fulcio_certs + // Keep window selection dynamic: a long-lived verifier may cross the + // activation time of a future authority after construction. + let now = jiff::Timestamp::now(); + let trust_anchors: Vec<_> = fulcio_anchors .iter() - .filter_map(|cert_der| { - let cert = CertificateDer::from(&cert_der[..]); - anchor_from_trusted_cert(&cert) - .map(|anchor| anchor.to_owned()) - .ok() - }) + .filter(|(_, window)| window.is_none_or(|range| range.has_started_by(now))) + .map(|(anchor, _)| anchor.clone()) .collect(); if trust_anchors.is_empty() { @@ -385,7 +379,13 @@ mod tests { .unwrap(); let signature = extract_signature(&bundle.content); - let times = determine_validation_times(&bundle, &signature, &trusted_root).unwrap(); + let times = determine_validation_times( + &bundle, + &signature, + &trusted_root, + &trusted_root.rekor_keys().unwrap(), + ) + .unwrap(); assert_eq!( times.len(), @@ -450,8 +450,10 @@ mod tests { // The canonical flow: the issuer comes from the verified chain, then SCT // verification uses it. Before the fix, SCT verification returned // Err("SCT signature verification failed: ... signature invalid"). - let issuer_spki = verify_certificate_chain(material, validation_time, &trusted_root) - .expect("certificate chain should verify against the staging root"); + 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 } => { @@ -459,7 +461,7 @@ mod tests { } _ => panic!("fixture must have a signing certificate"), }; - super::super::sct::verify_sct(cert.as_bytes(), issuer_spki.as_bytes(), &trusted_root) + 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"); } diff --git a/crates/sigstore-verify/src/verify_impl/sct.rs b/crates/sigstore-verify/src/verify_impl/sct.rs index 6664543b..3657cbbf 100644 --- a/crates/sigstore-verify/src/verify_impl/sct.rs +++ b/crates/sigstore-verify/src/verify_impl/sct.rs @@ -6,8 +6,8 @@ use crate::error::{Error, Result}; use const_oid::db::rfc6962::CT_PRECERT_SCTS; +use sigstore_crypto::Keyring; use sigstore_crypto::SigningScheme; -use sigstore_trust_root::TrustedRoot; use sigstore_types::{Sha256Hash, SignatureBytes}; use tls_codec::{SerializeBytes, TlsByteVecU16, TlsByteVecU24, TlsSerializeBytes, TlsSize}; use x509_cert::{ @@ -191,7 +191,7 @@ pub fn extract_sct( pub fn verify_sct( cert_der: &[u8], issuer_spki_der: &[u8], - trusted_root: &TrustedRoot, + ct_keys: &[(SigningScheme, Keyring)], ) -> Result<()> { // Parse the certificate let cert = Certificate::from_der(cert_der) @@ -212,9 +212,11 @@ pub fn verify_sct( // TrustedRoot constructs the keyring and preserves each CT log's declared // key ID and validity window. RFC 6962 timestamps are milliseconds since // the Unix epoch, so select the key that was valid when the SCT was issued. - let keyring = trusted_root - .ctfe_keys(scheme) - .map_err(|e| Error::Verification(format!("failed to build CT keyring: {e}")))?; + let keyring = &ct_keys + .iter() + .find(|(candidate, _)| *candidate == scheme) + .ok_or_else(|| Error::Verification("unsupported SCT signing scheme".into()))? + .1; if keyring.is_empty() { return Err(Error::Verification( "no CT log keys in trusted root".to_string(), diff --git a/crates/sigstore-verify/src/verify_impl/tlog.rs b/crates/sigstore-verify/src/verify_impl/tlog.rs index 5ff2919d..8e95b006 100644 --- a/crates/sigstore-verify/src/verify_impl/tlog.rs +++ b/crates/sigstore-verify/src/verify_impl/tlog.rs @@ -7,7 +7,7 @@ use crate::error::{Error, Result}; use base64::Engine; use serde::Serialize; use sigstore_crypto::Checkpoint; -use sigstore_trust_root::TrustedRoot; +use sigstore_crypto::Keyring; use sigstore_types::bundle::InclusionProof; use sigstore_types::{Bundle, KindVersion, Sha256Hash, SignatureBytes, TransparencyLogEntry}; @@ -26,12 +26,12 @@ use sigstore_types::{Bundle, KindVersion, Sha256Hash, SignatureBytes, Transparen /// /// # Arguments /// * `bundle` - The bundle containing transparency log entries -/// * `trusted_root` - Trusted root for cryptographic verification +/// * `rekor_keys` - Trusted root for cryptographic verification /// * `not_before` - Certificate validity start time (Unix timestamp) /// * `not_after` - Certificate validity end time (Unix timestamp) pub fn verify_tlog_entries( bundle: &Bundle, - trusted_root: &TrustedRoot, + rekor_keys: &Keyring, not_before: jiff::Timestamp, not_after: jiff::Timestamp, ) -> Result> { @@ -39,7 +39,7 @@ pub fn verify_tlog_entries( for entry in &bundle.verification_material.tlog_entries { // Verify Merkle inclusion proof, checkpoint signature and SET - verify_entry_inclusion(entry, trusted_root)?; + verify_entry_inclusion(entry, rekor_keys)?; // Only a Rekor v1 SET authenticates integratedTime. An inclusion proof // authenticates the body, not this separate timestamp field. @@ -110,22 +110,19 @@ pub(crate) fn validate_integrated_time_not_in_future( /// /// Time-related checks (integrated time vs. certificate validity) are not /// performed here; see [`verify_tlog_entries`]. -pub fn verify_entry_inclusion( - entry: &TransparencyLogEntry, - trusted_root: &TrustedRoot, -) -> Result<()> { +pub fn verify_entry_inclusion(entry: &TransparencyLogEntry, rekor_keys: &Keyring) -> Result<()> { if let Some(ref inclusion_proof) = entry.inclusion_proof { verify_merkle_inclusion(entry, inclusion_proof)?; verify_checkpoint( inclusion_proof.checkpoint.envelope(), inclusion_proof, is_rekor_v2(entry), - trusted_root, + rekor_keys, )?; } if entry.inclusion_promise.is_some() { - verify_set(entry, trusted_root)?; + verify_set(entry, rekor_keys)?; } Ok(()) @@ -168,7 +165,7 @@ pub fn verify_checkpoint( checkpoint_envelope: &str, inclusion_proof: &InclusionProof, is_v2: bool, - trusted_root: &TrustedRoot, + rekor_keys: &Keyring, ) -> Result<()> { // Parse the checkpoint (signed note) let checkpoint = Checkpoint::from_text(checkpoint_envelope) @@ -191,9 +188,7 @@ pub fn verify_checkpoint( // validity metadata alongside each parsed verification key. Four-byte // checkpoint hints may collide, so try every matching key and do not let // an invalid matching signature suppress a later valid log signature. - let rekor_keys = trusted_root - .rekor_keys() - .map_err(|e| Error::Verification(format!("failed to build Rekor keyring: {e}")))?; + let message = checkpoint.signed_data(); let now = jiff::Timestamp::now(); let mut found_matching_key = false; @@ -230,7 +225,7 @@ struct RekorPayload { } /// Verify SET (Signed Entry Timestamp) -pub fn verify_set(entry: &TransparencyLogEntry, trusted_root: &TrustedRoot) -> Result<()> { +pub fn verify_set(entry: &TransparencyLogEntry, rekor_keys: &Keyring) -> Result<()> { let promise = entry .inclusion_promise .as_ref() @@ -246,9 +241,7 @@ pub fn verify_set(entry: &TransparencyLogEntry, trusted_root: &TrustedRoot) -> R .map_err(|e| Error::Verification(format!("invalid Rekor log ID: {e}")))?; let key_id = Sha256Hash::try_from_slice(&decoded_key_id) .map_err(|e| Error::Verification(format!("invalid Rekor log ID: {e}")))?; - let keyring = trusted_root - .rekor_keys() - .map_err(|e| Error::Verification(format!("failed to build Rekor keyring: {e}")))?; + let keyring = rekor_keys; let log_key = if let Some(integrated_ts) = entry.integrated_time { keyring.get_key_at(&key_id, integrated_ts).ok_or_else(|| { Error::Verification(format!( diff --git a/crates/sigstore-verify/tests/verification_tests.rs b/crates/sigstore-verify/tests/verification_tests.rs index b9601ade..43cf68a2 100644 --- a/crates/sigstore-verify/tests/verification_tests.rs +++ b/crates/sigstore-verify/tests/verification_tests.rs @@ -200,7 +200,7 @@ fn test_verifier_creation() { // V03_BUNDLE is from sigstore-python tests, so it verifies against the // staging root (whose expired Rekor key authenticates the SET / signed time). let root = staging_root(); - let verifier = Verifier::new(&root); + let verifier = Verifier::new(&root).unwrap(); let bundle = Bundle::from_json(V03_BUNDLE).unwrap(); // Extract expected digest from the bundle @@ -970,6 +970,7 @@ fn test_verify_conda_package_attestation_from_sync_reader() { let bundle = Bundle::from_json(CONDA_ATTESTATION_BUNDLE).unwrap(); let verification = Verifier::new(&production_root()) + .unwrap() .verify_reader( std::io::Cursor::new(CONDA_PACKAGE), &bundle, @@ -984,6 +985,7 @@ async fn test_verify_conda_package_attestation_from_async_reader() { let bundle = Bundle::from_json(CONDA_ATTESTATION_BUNDLE).unwrap(); Verifier::new(&production_root()) + .unwrap() .verify_async_reader( futures::io::Cursor::new(CONDA_PACKAGE), &bundle, @@ -1000,6 +1002,7 @@ fn test_verify_conda_package_tampered_from_reader() { let bundle = Bundle::from_json(CONDA_ATTESTATION_BUNDLE).unwrap(); let err = Verifier::new(&production_root()) + .unwrap() .verify_reader( std::io::Cursor::new(b"this is not the original package content"), &bundle, @@ -1118,7 +1121,7 @@ async fn invalid_certificate_does_not_consume_readers() { raw_bytes: sigstore_types::DerCertificate::new(vec![0]), }, ); - let verifier = Verifier::new(&production_root()); + let verifier = Verifier::new(&production_root()).unwrap(); let mut reader = std::io::Cursor::new(b"do not consume"); let error = verifier .verify_reader(&mut reader, &bundle, &VerificationPolicy::any_identity()) @@ -1153,11 +1156,35 @@ fn inclusion_proof_alone_does_not_authenticate_integrated_time() { assert!(result.tlog_verified()); } +#[test] +fn verifier_constructor_rejects_invalid_static_trust() { + let root = production_root(); + assert!(Verifier::new(&root).is_ok()); + let mut bad_key = root.clone(); + bad_key.tlogs[0].public_key.raw_bytes = sigstore_types::DerPublicKey::new(vec![0]); + let mut bad_ca = root.clone(); + bad_ca.certificate_authorities[0].cert_chain.certificates[0].raw_bytes = + sigstore_types::DerCertificate::new(vec![0]); + let mut bad_tsa = root.clone(); + bad_tsa.timestamp_authorities[0].cert_chain.certificates[0].raw_bytes = + sigstore_types::DerCertificate::new(vec![0]); + let mut duplicate_ct = root.clone(); + duplicate_ct.ctlogs.push(duplicate_ct.ctlogs[0].clone()); + let mut reversed_window = root; + reversed_window.tlogs[0].public_key.valid_for = Some(sigstore_types::TimeRange::new( + jiff::Timestamp::MAX, + Some(jiff::Timestamp::MIN), + )); + for invalid in [bad_key, bad_ca, bad_tsa, duplicate_ct, reversed_window] { + assert!(Verifier::new(&invalid).is_err()); + } +} + #[test] fn verification_results_report_only_checked_evidence() { let bundle = Bundle::from_json(COSIGN_V3_BLOB_BUNDLE).unwrap(); let bytes = include_bytes!("../test_data/bundles/cosign-v3-blob.txt"); - let verifier = Verifier::new(&production_root()); + let verifier = Verifier::new(&production_root()).unwrap(); for (policy, chain, sct, tlog) in [ (VerificationPolicy::any_identity(), true, true, true), ( @@ -1206,6 +1233,7 @@ fn test_verify_cosign_bundle_from_sync_reader() { VerificationPolicy::any_identity().require_issuer("https://github.com/login/oauth"); Verifier::new(&production_root()) + .unwrap() .verify_reader(std::io::Cursor::new(artifact), &bundle, &policy) .unwrap(); } @@ -1218,6 +1246,7 @@ async fn test_verify_cosign_bundle_from_async_reader() { VerificationPolicy::any_identity().require_issuer("https://github.com/login/oauth"); Verifier::new(&production_root()) + .unwrap() .verify_async_reader(futures::io::Cursor::new(artifact), &bundle, &policy) .await .unwrap(); @@ -1580,7 +1609,7 @@ fn managed_dsse_verifier_is_bound_even_when_tlog_verification_is_skipped() { inclusion_proof: None, canonicalized_body: CanonicalizedBody::new(serde_json::to_vec(&body).unwrap()), }]; - let result = Verifier::new(&production_root()).verify_with_key( + let result = Verifier::new(&production_root()).unwrap().verify_with_key( b"artifact", &bundle, &public_key, @@ -1624,7 +1653,7 @@ fn test_verify_with_key_treats_public_key_hint_as_opaque() { fn test_verifier_with_key_accepts_digest_and_reports_integrated_time() { let bundle = Bundle::from_json(MANAGED_KEY_BUNDLE).unwrap(); let expected_time = bundle.verification_material.tlog_entries[0].integrated_time; - let verifier = Verifier::new(&production_root()); + let verifier = Verifier::new(&production_root()).unwrap(); let result = verifier .verify_with_key( @@ -1648,6 +1677,7 @@ fn test_verifier_with_key_from_sync_reader() { let bundle = Bundle::from_json(MANAGED_KEY_BUNDLE).unwrap(); Verifier::new(&production_root()) + .unwrap() .verify_with_key_reader( std::io::Cursor::new(MANAGED_KEY_ARTIFACT), &bundle, @@ -1662,6 +1692,7 @@ async fn test_verifier_with_key_from_async_reader() { let bundle = Bundle::from_json(MANAGED_KEY_BUNDLE).unwrap(); Verifier::new(&production_root()) + .unwrap() .verify_with_key_async_reader( futures::io::Cursor::new(MANAGED_KEY_ARTIFACT), &bundle,