use std::path::Path;
use mise_sigstore::sources::github::GitHubSource;
use mise_sigstore::{ArtifactRef, AttestationSource};
pub use mise_sigstore::{AttestationError, SlsaArtifact};
type AttestationResult<T> = std::result::Result<T, AttestationError>;
fn resolve_token_for_wrapper(api_url: Option<&str>) -> Option<String> {
let url = api_url.unwrap_or(crate::github::API_URL);
crate::github::resolve_token_for_api_url(url)
}
fn routed_api_url(api_url: &str) -> String {
let Ok(mut url) = url::Url::parse(api_url) else {
debug!("invalid GitHub attestation API URL, skipping url_replacements: {api_url}");
return api_url.to_string();
};
let original = url.clone();
crate::http::apply_url_replacements(&mut url);
if url == original {
api_url.to_string()
} else {
url.to_string()
}
}
pub async fn verify_attestation(
artifact_path: &Path,
owner: &str,
repo: &str,
expected_workflow: Option<&str>,
api_url: Option<&str>,
) -> AttestationResult<bool> {
let mut digest = None;
if use_versions_host_for_attestations(api_url) {
let artifact_digest = mise_sigstore::calculate_file_digest(artifact_path).await?;
match crate::versions_host::github_attestations(
&format!("{owner}/{repo}"),
&format!("sha256:{artifact_digest}"),
)
.await
{
Ok(Some(attestations)) => {
trace!(
"got {} GitHub attestations for {owner}/{repo}@sha256:{artifact_digest} from mise-versions",
attestations.len()
);
if attestations.is_empty() {
return Err(AttestationError::NoAttestations);
} else if attestations.iter().any(|a| !a.has_inline_bundle()) {
debug!(
"mise-versions returned GitHub attestations without inline bundles; falling back to GitHub API"
);
} else {
return mise_sigstore::verify_github_attestation_with_attestations(
artifact_path,
&attestations,
expected_workflow,
)
.await;
}
}
Ok(None) => {}
Err(err) => debug!("mise-versions GitHub attestations lookup failed: {err:#}"),
}
digest = Some(artifact_digest);
}
let token = resolve_token_for_wrapper(api_url);
let base_url = routed_api_url(api_url.unwrap_or(crate::github::API_URL));
if let Some(digest) = digest {
mise_sigstore::verify_github_attestation_with_base_url_and_digest(
artifact_path,
owner,
repo,
token.as_deref(),
expected_workflow,
&base_url,
&digest,
)
.await
} else {
mise_sigstore::verify_github_attestation_with_base_url(
artifact_path,
owner,
repo,
token.as_deref(),
expected_workflow,
&base_url,
)
.await
}
}
#[derive(Debug)]
pub enum DetectError {
SourceCreation(AttestationError),
Fetch(AttestationError),
}
impl std::fmt::Display for DetectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DetectError::SourceCreation(e) => write!(f, "{e}"),
DetectError::Fetch(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for DetectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DetectError::SourceCreation(e) => Some(e),
DetectError::Fetch(e) => Some(e),
}
}
}
pub async fn detect_attestations(
owner: &str,
repo: &str,
api_url: &str,
digest: &str,
) -> Result<bool, DetectError> {
if use_versions_host_for_attestations(Some(api_url)) {
match crate::versions_host::github_attestations(&format!("{owner}/{repo}"), digest).await {
Ok(Some(attestations)) => {
trace!(
"got {} GitHub attestation probes for {owner}/{repo}@{digest} from mise-versions",
attestations.len()
);
return Ok(!attestations.is_empty());
}
Ok(None) => {}
Err(err) => debug!("mise-versions GitHub attestation probe failed: {err:#}"),
}
}
let token = resolve_token_for_wrapper(Some(api_url));
let base_url = routed_api_url(api_url);
let source = GitHubSource::with_base_url(owner, repo, token.as_deref(), &base_url)
.map_err(DetectError::SourceCreation)?;
let artifact_ref = ArtifactRef::from_digest(digest);
let attestations = source
.fetch_attestations(&artifact_ref)
.await
.map_err(DetectError::Fetch)?;
Ok(!attestations.is_empty())
}
fn use_versions_host_for_attestations(api_url: Option<&str>) -> bool {
let settings = crate::config::Settings::get();
if settings.prefer_offline() || !settings.use_versions_host {
return false;
}
api_url
.unwrap_or(crate::github::API_URL)
.trim_end_matches('/')
== crate::github::API_URL
}
pub async fn verify_slsa_provenance(
artifact_path: &Path,
provenance_path: &Path,
min_level: u8,
) -> AttestationResult<bool> {
mise_sigstore::verify_slsa_provenance(artifact_path, provenance_path, min_level).await
}
pub async fn verify_slsa_provenance_artifacts(
provenance_path: &Path,
artifacts: &[SlsaArtifact],
min_level: u8,
) -> AttestationResult<bool> {
mise_sigstore::verify_slsa_provenance_artifacts(provenance_path, artifacts, min_level).await
}
pub fn is_slsa_subject_mismatch(error: &AttestationError) -> bool {
mise_sigstore::is_slsa_subject_mismatch(error)
}
pub fn is_api_failure(error: &AttestationError) -> bool {
matches!(error, AttestationError::Api(_) | AttestationError::Http(_))
}
pub async fn verify_cosign_signature(
artifact_path: &Path,
sig_or_bundle_path: &Path,
) -> AttestationResult<bool> {
mise_sigstore::verify_cosign_signature(artifact_path, sig_or_bundle_path).await
}
pub async fn verify_cosign_signature_with_key(
artifact_path: &Path,
sig_or_bundle_path: &Path,
public_key_path: &Path,
) -> AttestationResult<bool> {
mise_sigstore::verify_cosign_signature_with_key(
artifact_path,
sig_or_bundle_path,
public_key_path,
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::env as mise_env;
use confique::Layer;
use std::sync::Mutex;
const TOKEN_ENV_VARS: &[&str] = &[
"MISE_GITHUB_TOKEN",
"GITHUB_API_TOKEN",
"GITHUB_TOKEN",
"MISE_GITHUB_ENTERPRISE_TOKEN",
];
static TEST_SETTINGS_LOCK: Mutex<()> = Mutex::new(());
struct SettingsGuard {
_lock: std::sync::MutexGuard<'static, ()>,
}
impl SettingsGuard {
fn new(replacements: Option<indexmap::IndexMap<String, String>>) -> Self {
Self::with_versions_host(replacements, None)
}
fn with_versions_host(
replacements: Option<indexmap::IndexMap<String, String>>,
use_versions_host: Option<bool>,
) -> Self {
let lock = TEST_SETTINGS_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let mut settings = crate::config::settings::SettingsPartial::empty();
settings.url_replacements = replacements;
settings.use_versions_host = use_versions_host;
crate::config::Settings::reset(Some(settings));
Self { _lock: lock }
}
}
impl Drop for SettingsGuard {
fn drop(&mut self) {
crate::config::Settings::reset(None);
}
}
struct TokenEnvGuard {
saved: Vec<(&'static str, Option<String>)>,
}
impl TokenEnvGuard {
fn new() -> Self {
let saved: Vec<_> = TOKEN_ENV_VARS
.iter()
.map(|name| (*name, std::env::var(name).ok()))
.collect();
for name in TOKEN_ENV_VARS {
mise_env::remove_var(name);
}
Self { saved }
}
}
impl Drop for TokenEnvGuard {
fn drop(&mut self) {
for (name, value) in std::mem::take(&mut self.saved) {
match value {
Some(v) => mise_env::set_var(name, v),
None => mise_env::remove_var(name),
}
}
}
}
#[test]
fn test_resolve_token_wrapper_uses_env_var_with_default_url() {
let _lock = crate::github::TEST_ENV_LOCK.lock().unwrap();
let _env = TokenEnvGuard::new();
mise_env::set_var("GITHUB_TOKEN", "ghp_wrapper_default");
let resolved = resolve_token_for_wrapper(None);
assert_eq!(
resolved.as_deref(),
Some("ghp_wrapper_default"),
"env var should flow through the wrapper with the default API URL"
);
}
#[test]
fn test_resolve_token_wrapper_uses_env_var_with_explicit_api_url() {
let _lock = crate::github::TEST_ENV_LOCK.lock().unwrap();
let _env = TokenEnvGuard::new();
mise_env::set_var("MISE_GITHUB_TOKEN", "ghp_explicit_api");
let resolved = resolve_token_for_wrapper(Some(crate::github::API_URL));
assert_eq!(
resolved.as_deref(),
Some("ghp_explicit_api"),
"explicit api.github.com URL should resolve identically to the default"
);
}
#[test]
fn test_resolve_token_wrapper_respects_enterprise_api_url() {
let _lock = crate::github::TEST_ENV_LOCK.lock().unwrap();
let _env = TokenEnvGuard::new();
mise_env::set_var("GITHUB_TOKEN", "ghp_public_only");
mise_env::set_var("MISE_GITHUB_ENTERPRISE_TOKEN", "ghp_enterprise_only");
let resolved =
resolve_token_for_wrapper(Some("https://github.enterprise.example.com/api/v3"));
assert_eq!(
resolved.as_deref(),
Some("ghp_enterprise_only"),
"enterprise api_url should resolve the enterprise token, not the public one"
);
let resolved_default = resolve_token_for_wrapper(None);
assert_eq!(
resolved_default.as_deref(),
Some("ghp_public_only"),
"default api_url should still resolve the public token"
);
}
struct TokensFileOverrideGuard;
impl TokensFileOverrideGuard {
fn set(host: &str, token: &str) -> Self {
let mut map = std::collections::HashMap::new();
map.insert(host.to_string(), token.to_string());
*crate::github::test_support::TOKENS_FILE_OVERRIDE
.write()
.unwrap() = Some(map);
Self
}
}
impl Drop for TokensFileOverrideGuard {
fn drop(&mut self) {
*crate::github::test_support::TOKENS_FILE_OVERRIDE
.write()
.unwrap() = None;
}
}
#[test]
fn test_resolve_token_wrapper_uses_github_tokens_toml_source() {
let _lock = crate::github::TEST_ENV_LOCK.lock().unwrap();
let _env = TokenEnvGuard::new();
let _tokens_file = TokensFileOverrideGuard::set("github.com", "ghp_from_tokens_file");
let resolved = resolve_token_for_wrapper(None);
assert_eq!(
resolved.as_deref(),
Some("ghp_from_tokens_file"),
"wrapper should resolve tokens from github_tokens.toml when env vars are empty"
);
}
#[test]
fn test_is_api_failure_excludes_malformed_payloads() {
assert!(is_api_failure(&AttestationError::Api(
"rate limited".into()
)));
assert!(!is_api_failure(&AttestationError::Json(
serde_json::from_str::<serde_json::Value>("{").unwrap_err()
)));
}
#[test]
fn test_routed_api_url_applies_simple_url_replacement() {
let _settings = SettingsGuard::new(Some(indexmap::indexmap! {
"https://api.github.com".to_string() => "https://github-proxy.example.com".to_string(),
}));
let routed = routed_api_url(crate::github::API_URL);
assert_eq!(routed, "https://github-proxy.example.com/");
}
#[test]
fn test_routed_api_url_applies_regex_url_replacement() {
let _settings = SettingsGuard::new(Some(indexmap::indexmap! {
"regex:^https://api\\.github\\.com".to_string() => "https://github-proxy.example.com/api".to_string(),
}));
let routed = routed_api_url(crate::github::API_URL);
assert_eq!(routed, "https://github-proxy.example.com/api/");
}
#[test]
fn test_routed_api_url_keeps_original_url_without_replacement() {
let _settings = SettingsGuard::new(None);
let routed = routed_api_url(crate::github::API_URL);
assert_eq!(routed, crate::github::API_URL);
}
#[test]
fn test_use_versions_host_for_attestations_respects_setting() {
let _settings = SettingsGuard::with_versions_host(None, Some(false));
assert!(!use_versions_host_for_attestations(Some(
crate::github::API_URL
)));
}
}