use std::path::Path;
use thiserror::Error;
pub mod api;
pub mod bundle;
pub mod sources;
pub mod verifiers;
pub mod verify;
pub use api::{
Attestation, AttestationClient, AttestationClientBuilder, FetchParams, MessageDigest,
MessageSignature,
};
pub use bundle::{ParsedBundle, SlsaProvenance};
pub use sources::{ArtifactRef, AttestationSource};
pub use verifiers::{Policy, VerificationResult, Verifier};
pub use verify::verify_attestations;
#[derive(Debug, Error)]
pub enum AttestationError {
#[error("API error: {0}")]
Api(String),
#[error("Verification failed: {0}")]
Verification(String),
#[error("No attestations found")]
NoAttestations,
#[error("Invalid digest format: {0}")]
InvalidDigest(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Sigstore error: {0}")]
Sigstore(String),
}
pub type Result<T> = std::result::Result<T, AttestationError>;
pub async fn verify_artifact(
artifact_path: &Path,
source: &dyn AttestationSource,
verifier: &dyn Verifier,
policy: Option<&Policy>,
) -> Result<VerificationResult> {
let artifact_ref = ArtifactRef::from_path(artifact_path)?;
let attestations = source.fetch_attestations(&artifact_ref).await?;
if attestations.is_empty() {
return Err(AttestationError::NoAttestations);
}
let bundle = bundle::parse_bundle(&attestations[0])?;
let default_policy = Policy::default();
let policy = policy.unwrap_or(&default_policy);
verifier.verify(&bundle, artifact_path, policy).await
}
pub async fn verify_cosign_signature(
artifact_path: &Path,
sig_or_bundle_path: &Path,
) -> Result<bool> {
let source = sources::file::FileSource::new(sig_or_bundle_path);
let verifier = verifiers::cosign::CosignVerifier::new_keyless();
let result = verify_artifact(artifact_path, &source, &verifier, None).await?;
Ok(result.success)
}
pub async fn verify_cosign_signature_with_key(
artifact_path: &Path,
sig_or_bundle_path: &Path,
public_key_path: &Path,
) -> Result<bool> {
let source = sources::file::FileSource::new(sig_or_bundle_path);
let verifier = verifiers::cosign::CosignVerifier::new_with_key_file(public_key_path).await?;
let result = verify_artifact(artifact_path, &source, &verifier, None).await?;
Ok(result.success)
}
pub async fn verify_slsa_provenance(
artifact_path: &Path,
provenance_path: &Path,
min_level: u8,
) -> Result<bool> {
let source = sources::file::FileSource::new(provenance_path);
let verifier = verifiers::slsa::SlsaVerifier::new(min_level);
let policy = Policy {
slsa_level: Some(min_level),
..Default::default()
};
let result = verify_artifact(artifact_path, &source, &verifier, Some(&policy)).await?;
Ok(result.success)
}
pub async fn verify_github_attestation(
artifact_path: &Path,
owner: &str,
repo: &str,
token: Option<&str>,
signer_workflow: Option<&str>,
) -> Result<bool> {
let digest = calculate_file_digest(artifact_path)?;
let client = AttestationClient::new(token)?;
let params = FetchParams {
owner: owner.to_string(),
repo: Some(format!("{}/{}", owner, repo)),
digest: format!("sha256:{}", digest),
limit: 30,
predicate_type: None,
};
let attestations = client.fetch_attestations(params).await?;
if attestations.is_empty() {
return Err(AttestationError::NoAttestations);
}
verify::verify_attestations(&attestations, artifact_path, signer_workflow).await?;
Ok(true)
}
pub fn calculate_file_digest(path: &Path) -> Result<String> {
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Read;
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0; 8192];
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(hex::encode(hasher.finalize()))
}