Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions crates/mise-sigstore/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1385,8 +1386,47 @@ fn extract_spki_der(cert_der: &[u8]) -> Result<Vec<u8>> {
})
}

/// 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<Option<String>> = 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<String>) {
// 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<String>) -> 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<TrustedRoot> {
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<TrustedRoot> {
Expand Down Expand Up @@ -1542,6 +1582,21 @@ pub async fn calculate_file_digest(path: &Path) -> Result<String> {
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()
Expand Down
44 changes: 44 additions & 0 deletions src/github/sigstore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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.
Expand Down Expand Up @@ -92,6 +111,7 @@ pub async fn verify_attestation(
api_url: Option<&str>,
use_versions_host: bool,
) -> AttestationResult<bool> {
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?;
Expand Down Expand Up @@ -168,6 +188,7 @@ pub async fn verify_attestation_with_predicate_type(
api_url: Option<&str>,
use_versions_host: bool,
) -> AttestationResult<bool> {
mise_sigstore::set_tuf_url(routed_tuf_url());
let Some(predicate_type) = predicate_type else {
return verify_attestation(
artifact_path,
Expand Down Expand Up @@ -322,6 +343,7 @@ pub async fn verify_slsa_provenance(
provenance_path: &Path,
min_level: u8,
) -> AttestationResult<bool> {
mise_sigstore::set_tuf_url(routed_tuf_url());
mise_sigstore::verify_slsa_provenance(artifact_path, provenance_path, min_level).await
}

Expand All @@ -330,6 +352,7 @@ pub async fn verify_slsa_provenance_artifacts(
artifacts: &[SlsaArtifact],
min_level: u8,
) -> AttestationResult<bool> {
mise_sigstore::set_tuf_url(routed_tuf_url());
mise_sigstore::verify_slsa_provenance_artifacts(provenance_path, artifacts, min_level).await
}

Expand All @@ -346,6 +369,7 @@ pub async fn verify_cosign_signature(
artifact_path: &Path,
sig_or_bundle_path: &Path,
) -> AttestationResult<bool> {
mise_sigstore::set_tuf_url(routed_tuf_url());
mise_sigstore::verify_cosign_signature(artifact_path, sig_or_bundle_path).await
}

Expand All @@ -355,6 +379,7 @@ pub async fn verify_cosign_signature_with_key(
sig_or_bundle_path: &Path,
public_key_path: &Path,
) -> AttestationResult<bool> {
mise_sigstore::set_tuf_url(routed_tuf_url());
mise_sigstore::verify_cosign_signature_with_key(
artifact_path,
sig_or_bundle_path,
Expand Down Expand Up @@ -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));
Expand Down
Loading