diff --git a/crates/mise-sigstore/src/lib.rs b/crates/mise-sigstore/src/lib.rs index 2740b99a92..bc5601375a 100644 --- a/crates/mise-sigstore/src/lib.rs +++ b/crates/mise-sigstore/src/lib.rs @@ -7,7 +7,8 @@ use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue, USER_AGENT}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use sigstore_verify::VerificationPolicy; -use sigstore_verify::trust_root::{SigstoreInstance, TrustedRoot}; +pub use sigstore_verify::trust_root::DEFAULT_TUF_URL; +use sigstore_verify::trust_root::{PRODUCTION_TUF_ROOT, SigstoreInstance, TrustedRoot, TufConfig}; use sigstore_verify::types::bundle::VerificationMaterialContent; use sigstore_verify::types::{ Artifact, Bundle, DerCertificate, DerPublicKey, HashAlgorithm, Sha256Hash, SignatureBytes, @@ -1385,8 +1386,47 @@ fn extract_spki_der(cert_der: &[u8]) -> Result> { }) } +/// Process-global override for the Sigstore public-good TUF repository URL. +/// +/// Set by the embedding crate from `settings.url_replacements` so the TUF root +/// fetch follows the same mirror/proxy as the rest of mise's HTTP traffic. +/// `None` means "use the crate default" (unchanged behavior). +static TUF_URL_OVERRIDE: std::sync::RwLock> = std::sync::RwLock::new(None); + +/// Override the Sigstore public-good TUF URL (e.g. a mirror derived from mise's +/// `settings.url_replacements`). Passing a mirror URL still bootstraps from the +/// embedded production root ([`PRODUCTION_TUF_ROOT`]), so a mirror cannot forge +/// the chain of trust — TUF verifies all fetched metadata against that pinned +/// root. Passing `None` restores the default behavior. +pub fn set_tuf_url(url: Option) { + // Recover from a poisoned lock rather than silently dropping the override: + // the guarded data is just a String, so a poisoned lock still holds a valid + // value and we must still apply the (mirror) URL. + let mut guard = TUF_URL_OVERRIDE.write().unwrap_or_else(|e| e.into_inner()); + *guard = url; +} + +/// Build the [`TufConfig`] for the Sigstore public-good root, honoring an +/// optional URL override. +fn select_tuf_config(override_url: Option) -> TufConfig { + match override_url { + // SECURITY: pin the embedded production root even when fetching from a + // mirror. A custom URL has no embedded-root fallback, and the mirror + // serves identical TUF content; bootstrapping with PRODUCTION_TUF_ROOT + // means every metadata file is verified against the canonical root. + Some(url) => TufConfig::custom(url, PRODUCTION_TUF_ROOT), + // Equivalent to `TrustedRoot::production()` (which is itself + // `from_tuf(TufConfig::production())`) — the default path is unchanged. + None => TufConfig::production(), + } +} + async fn production_trusted_root() -> Result { - Ok(TrustedRoot::production().await?) + let override_url = TUF_URL_OVERRIDE + .read() + .unwrap_or_else(|e| e.into_inner()) + .clone(); + Ok(TrustedRoot::from_tuf(select_tuf_config(override_url)).await?) } fn github_trusted_root() -> Result { @@ -1542,6 +1582,21 @@ pub async fn calculate_file_digest(path: &Path) -> Result { mod tests { use super::*; + #[test] + fn select_tuf_config_default_uses_production_url() { + // No override → canonical Sigstore public-good TUF URL (default behavior). + assert_eq!(select_tuf_config(None).url, DEFAULT_TUF_URL); + } + + #[test] + fn select_tuf_config_override_uses_mirror_url() { + // Override → the mirror URL, while still pinning PRODUCTION_TUF_ROOT + // (the latter is enforced by TufConfig::custom, covered by the + // sigstore-trust-root crate's own tests). + let mirror = "https://tuf-mirror.example.com/".to_string(); + assert_eq!(select_tuf_config(Some(mirror.clone())).url, mirror); + } + #[test] fn attestations_url_includes_predicate_type() { let client = AttestationClient::builder() diff --git a/src/github/sigstore.rs b/src/github/sigstore.rs index c3ec3a02aa..c2e06e5abb 100644 --- a/src/github/sigstore.rs +++ b/src/github/sigstore.rs @@ -56,6 +56,25 @@ fn routed_api_url(api_url: &str) -> String { } } +/// Apply mise's `url_replacements` to the Sigstore public-good TUF URL. +/// +/// Returns `Some(replaced)` only when a replacement actually changed the URL, +/// otherwise `None` (meaning: keep the sigstore crate's default behavior). The +/// result is pushed into `mise-sigstore` via [`mise_sigstore::set_tuf_url`] so +/// the TUF root fetch follows the same mirror as the rest of mise's traffic. +fn routed_tuf_url() -> Option { + let Ok(mut url) = url::Url::parse(mise_sigstore::DEFAULT_TUF_URL) else { + debug!( + "invalid Sigstore TUF URL, skipping url_replacements: {}", + mise_sigstore::DEFAULT_TUF_URL + ); + return None; + }; + let original = url.clone(); + crate::http::apply_url_replacements(&mut url); + (url != original).then(|| url.to_string()) +} + /// Build a [`RetryConfig`] from mise's HTTP settings so attestation requests /// retry and time out exactly like the rest of mise's HTTP traffic rather than /// using a policy hardcoded in the `mise-sigstore` crate. @@ -92,6 +111,7 @@ pub async fn verify_attestation( api_url: Option<&str>, use_versions_host: bool, ) -> AttestationResult { + mise_sigstore::set_tuf_url(routed_tuf_url()); let mut digest = None; if use_versions_host_for_attestations(api_url, use_versions_host) { let artifact_digest = mise_sigstore::calculate_file_digest(artifact_path).await?; @@ -168,6 +188,7 @@ pub async fn verify_attestation_with_predicate_type( api_url: Option<&str>, use_versions_host: bool, ) -> AttestationResult { + mise_sigstore::set_tuf_url(routed_tuf_url()); let Some(predicate_type) = predicate_type else { return verify_attestation( artifact_path, @@ -322,6 +343,7 @@ pub async fn verify_slsa_provenance( provenance_path: &Path, min_level: u8, ) -> AttestationResult { + mise_sigstore::set_tuf_url(routed_tuf_url()); mise_sigstore::verify_slsa_provenance(artifact_path, provenance_path, min_level).await } @@ -330,6 +352,7 @@ pub async fn verify_slsa_provenance_artifacts( artifacts: &[SlsaArtifact], min_level: u8, ) -> AttestationResult { + mise_sigstore::set_tuf_url(routed_tuf_url()); mise_sigstore::verify_slsa_provenance_artifacts(provenance_path, artifacts, min_level).await } @@ -346,6 +369,7 @@ pub async fn verify_cosign_signature( artifact_path: &Path, sig_or_bundle_path: &Path, ) -> AttestationResult { + mise_sigstore::set_tuf_url(routed_tuf_url()); mise_sigstore::verify_cosign_signature(artifact_path, sig_or_bundle_path).await } @@ -355,6 +379,7 @@ pub async fn verify_cosign_signature_with_key( sig_or_bundle_path: &Path, public_key_path: &Path, ) -> AttestationResult { + mise_sigstore::set_tuf_url(routed_tuf_url()); mise_sigstore::verify_cosign_signature_with_key( artifact_path, sig_or_bundle_path, @@ -571,6 +596,25 @@ mod tests { assert_eq!(routed, crate::github::API_URL); } + #[test] + fn test_routed_tuf_url_applies_url_replacement() { + let _settings = SettingsGuard::new(Some(indexmap::indexmap! { + "https://tuf-repo-cdn.sigstore.dev".to_string() + => "https://tuf-mirror.example.com".to_string(), + })); + + let routed = routed_tuf_url(); + + assert_eq!(routed.as_deref(), Some("https://tuf-mirror.example.com/")); + } + + #[test] + fn test_routed_tuf_url_none_without_replacement() { + let _settings = SettingsGuard::new(None); + + assert_eq!(routed_tuf_url(), None); + } + #[test] fn test_use_versions_host_for_attestations_respects_setting() { let _settings = SettingsGuard::with_versions_host(None, Some(false));