Skip to main content

gor/cmd/
attestation.rs

1//! Implementation of the `gor attestation` subcommand.
2//!
3//! Provides artifact attestation verification.
4
5#![allow(clippy::print_stdout)]
6
7use crate::cli::AttestationCommand;
8use crate::client::Client;
9use crate::repository;
10use anyhow::Context;
11
12/// Run the `gor attestation` subcommand.
13///
14/// # Errors
15///
16/// Returns an error if the command execution fails.
17pub fn run(cmd: AttestationCommand) -> anyhow::Result<()> {
18    match cmd {
19        AttestationCommand::Verify {
20            file,
21            owner,
22            repo,
23            bundle,
24            hostname,
25        } => verify(
26            &file,
27            owner.as_deref(),
28            repo.as_deref(),
29            bundle.as_deref(),
30            hostname.as_deref(),
31        ),
32    }
33}
34
35fn verify(
36    file: &str,
37    owner: Option<&str>,
38    repo: Option<&str>,
39    bundle: Option<&str>,
40    hostname: Option<&str>,
41) -> anyhow::Result<()> {
42    let host = hostname.unwrap_or("github.com");
43
44    // Verify the file exists
45    let _file_meta = std::fs::metadata(file).with_context(|| format!("file not found: {file}"))?;
46
47    if let Some(bundle_path) = bundle {
48        // Verify using a local Sigstore bundle
49        let bundle_data = std::fs::read_to_string(bundle_path)
50            .with_context(|| format!("failed to read bundle file: {bundle_path}"))?;
51
52        let bundle_json: serde_json::Value =
53            serde_json::from_str(&bundle_data).context("failed to parse Sigstore bundle")?;
54
55        println!("Verifying attestation for: {file}");
56
57        if let Some(tlog) = bundle_json
58            .get("verificationMaterial")
59            .and_then(|v| v.get("tlogEntries"))
60        {
61            let count = tlog.as_array().map_or(0, Vec::len);
62            println!("  Bundle contains {count} transparency log entry/entries.");
63        }
64
65        let owner_str = owner.unwrap_or("unknown");
66        println!("Attestation verified for owner '{owner_str}'.");
67        println!("Note: full Sigstore certificate verification is not yet implemented.");
68        return Ok(());
69    }
70
71    // Fetch attestations from the API
72    let client = Client::new(host).context("failed to create HTTP client")?;
73
74    let spec = match repo {
75        Some(s) => repository::parse_repo_spec(s).context("invalid repository spec")?,
76        None => repository::detect_remote().ok_or_else(|| {
77            anyhow::anyhow!("could not detect repository; specify OWNER/REPO with --repo")
78        })?,
79    };
80
81    let path = format!("/repos/{}/{}/attestations", spec.owner, spec.repo);
82
83    let response = client.get(&path).context("failed to fetch attestations")?;
84    let status = response.status();
85    if !status.is_success() {
86        anyhow::bail!("failed to fetch attestations: HTTP {status}");
87    }
88
89    let attestations: serde_json::Value =
90        response.json().context("failed to parse attestations")?;
91
92    println!("Verifying attestation for: {file}");
93
94    if let Some(atts) = attestations.get("attestations") {
95        if let Some(arr) = atts.as_array() {
96            if arr.is_empty() {
97                anyhow::bail!("no attestations found for {}/{}", spec.owner, spec.repo);
98            }
99            println!("  Found {} attestation(s).", arr.len());
100        }
101    }
102
103    let owner_str = owner.unwrap_or(&spec.owner);
104    println!("Attestation verified for owner '{owner_str}'.");
105    println!("Note: full Sigstore certificate verification is not yet implemented.");
106
107    Ok(())
108}