use std::path::Path;
use sigstore_verification::sources::github::GitHubSource;
use sigstore_verification::{ArtifactRef, AttestationSource};
pub use sigstore_verification::AttestationError;
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)
}
pub async fn verify_attestation(
artifact_path: &Path,
owner: &str,
repo: &str,
expected_workflow: Option<&str>,
api_url: Option<&str>,
) -> AttestationResult<bool> {
let token = resolve_token_for_wrapper(api_url);
match api_url {
Some(base_url) => {
sigstore_verification::verify_github_attestation_with_base_url(
artifact_path,
owner,
repo,
token.as_deref(),
expected_workflow,
base_url,
)
.await
}
None => {
sigstore_verification::verify_github_attestation(
artifact_path,
owner,
repo,
token.as_deref(),
expected_workflow,
)
.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> {
let token = resolve_token_for_wrapper(Some(api_url));
let source = GitHubSource::with_base_url(owner, repo, token.as_deref(), api_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())
}
pub async fn verify_slsa_provenance(
artifact_path: &Path,
provenance_path: &Path,
min_level: u8,
) -> AttestationResult<bool> {
sigstore_verification::verify_slsa_provenance(artifact_path, provenance_path, min_level).await
}
pub async fn verify_cosign_signature(
artifact_path: &Path,
sig_or_bundle_path: &Path,
) -> AttestationResult<bool> {
sigstore_verification::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> {
sigstore_verification::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;
const TOKEN_ENV_VARS: &[&str] = &[
"MISE_GITHUB_TOKEN",
"GITHUB_API_TOKEN",
"GITHUB_TOKEN",
"MISE_GITHUB_ENTERPRISE_TOKEN",
];
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"
);
}
}