sigstore_verification/
api.rs

1use crate::{AttestationError, Result};
2use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue, USER_AGENT};
3use serde::{Deserialize, Serialize};
4
5const GITHUB_API_URL: &str = "https://api.github.com";
6const USER_AGENT_VALUE: &str = "mise-attestation/0.1.0";
7
8#[derive(Debug, Clone)]
9pub struct AttestationClient {
10    client: reqwest::Client,
11    base_url: String,
12}
13
14#[derive(Debug, Serialize)]
15pub struct FetchParams {
16    pub owner: String,
17    pub repo: Option<String>,
18    pub digest: String,
19    pub limit: usize,
20    pub predicate_type: Option<String>,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct AttestationsResponse {
25    pub attestations: Vec<Attestation>,
26}
27
28#[derive(Debug, Deserialize, Clone)]
29pub struct Attestation {
30    pub bundle: Option<SigstoreBundle>,
31    pub bundle_url: Option<String>,
32}
33
34#[derive(Debug, Deserialize, Clone)]
35pub struct SigstoreBundle {
36    #[serde(rename = "mediaType")]
37    pub media_type: String,
38    #[serde(rename = "dsseEnvelope")]
39    pub dsse_envelope: Option<DsseEnvelope>,
40    #[serde(rename = "verificationMaterial")]
41    pub verification_material: Option<serde_json::Value>,
42    /// Message signature for direct blob signing (cosign v3 format)
43    #[serde(rename = "messageSignature")]
44    pub message_signature: Option<MessageSignature>,
45}
46
47#[derive(Debug, Deserialize, Clone)]
48pub struct MessageSignature {
49    #[serde(rename = "messageDigest")]
50    pub message_digest: MessageDigest,
51    pub signature: String,
52}
53
54#[derive(Debug, Deserialize, Clone)]
55pub struct MessageDigest {
56    pub algorithm: String,
57    pub digest: String,
58}
59
60#[derive(Debug, Deserialize, Clone)]
61pub struct DsseEnvelope {
62    pub payload: String,
63    #[serde(rename = "payloadType")]
64    pub payload_type: String,
65    pub signatures: Vec<Signature>,
66}
67
68#[derive(Debug, Deserialize, Clone)]
69pub struct Signature {
70    pub sig: String,
71    pub keyid: Option<String>,
72}
73
74impl AttestationClient {
75    pub fn new(token: Option<&str>) -> Result<Self> {
76        let mut headers = HeaderMap::new();
77        headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
78
79        if let Some(token) = token {
80            let auth_value = format!("Bearer {}", token);
81            headers.insert(
82                AUTHORIZATION,
83                HeaderValue::from_str(&auth_value)
84                    .map_err(|e| AttestationError::Api(e.to_string()))?,
85            );
86        }
87
88        let client = reqwest::Client::builder()
89            .default_headers(headers)
90            .build()?;
91
92        Ok(Self {
93            client,
94            base_url: GITHUB_API_URL.to_string(),
95        })
96    }
97
98    pub async fn fetch_attestations(&self, params: FetchParams) -> Result<Vec<Attestation>> {
99        let url = if let Some(repo) = &params.repo {
100            format!(
101                "{}/repos/{}/attestations/{}",
102                self.base_url, repo, params.digest
103            )
104        } else {
105            format!(
106                "{}/orgs/{}/attestations/{}",
107                self.base_url, params.owner, params.digest
108            )
109        };
110
111        let mut query_params = vec![("per_page", params.limit.to_string())];
112        if let Some(predicate_type) = &params.predicate_type {
113            query_params.push(("predicate_type", predicate_type.clone()));
114        }
115
116        let response = self.client.get(&url).query(&query_params).send().await?;
117
118        if !response.status().is_success() {
119            let status = response.status();
120
121            // 404 means no attestations exist for this artifact
122            if status == reqwest::StatusCode::NOT_FOUND {
123                return Ok(Vec::new());
124            }
125
126            let body = response
127                .text()
128                .await
129                .unwrap_or_else(|_| "Unknown error".to_string());
130            return Err(AttestationError::Api(format!(
131                "GitHub API returned {}: {}",
132                status, body
133            )));
134        }
135
136        let attestations_response: AttestationsResponse = response.json().await?;
137
138        // Download bundles if only URLs are provided
139        let mut attestations = Vec::new();
140        for att in attestations_response.attestations {
141            if att.bundle.is_some() {
142                attestations.push(att);
143            } else if let Some(bundle_url) = &att.bundle_url {
144                // Download the bundle
145                let bundle_response = self.client.get(bundle_url).send().await?;
146                if bundle_response.status().is_success() {
147                    let bundle: SigstoreBundle = bundle_response.json().await?;
148                    attestations.push(Attestation {
149                        bundle: Some(bundle),
150                        bundle_url: att.bundle_url.clone(),
151                    });
152                }
153            }
154        }
155
156        Ok(attestations)
157    }
158}