sigstore_verification/
api.rs1use 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: DsseEnvelope,
40 #[serde(rename = "verificationMaterial")]
41 pub verification_material: Option<serde_json::Value>,
42}
43
44#[derive(Debug, Deserialize, Clone)]
45pub struct DsseEnvelope {
46 pub payload: String,
47 #[serde(rename = "payloadType")]
48 pub payload_type: String,
49 pub signatures: Vec<Signature>,
50}
51
52#[derive(Debug, Deserialize, Clone)]
53pub struct Signature {
54 pub sig: String,
55 pub keyid: Option<String>,
56}
57
58impl AttestationClient {
59 pub fn new(token: Option<&str>) -> Result<Self> {
60 let mut headers = HeaderMap::new();
61 headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
62
63 if let Some(token) = token {
64 let auth_value = format!("Bearer {}", token);
65 headers.insert(
66 AUTHORIZATION,
67 HeaderValue::from_str(&auth_value)
68 .map_err(|e| AttestationError::Api(e.to_string()))?,
69 );
70 }
71
72 let client = reqwest::Client::builder()
73 .default_headers(headers)
74 .build()?;
75
76 Ok(Self {
77 client,
78 base_url: GITHUB_API_URL.to_string(),
79 })
80 }
81
82 pub async fn fetch_attestations(&self, params: FetchParams) -> Result<Vec<Attestation>> {
83 let url = if let Some(repo) = ¶ms.repo {
84 format!(
85 "{}/repos/{}/attestations/{}",
86 self.base_url, repo, params.digest
87 )
88 } else {
89 format!(
90 "{}/orgs/{}/attestations/{}",
91 self.base_url, params.owner, params.digest
92 )
93 };
94
95 let mut query_params = vec![("per_page", params.limit.to_string())];
96 if let Some(predicate_type) = ¶ms.predicate_type {
97 query_params.push(("predicate_type", predicate_type.clone()));
98 }
99
100 let response = self.client.get(&url).query(&query_params).send().await?;
101
102 if !response.status().is_success() {
103 let status = response.status();
104
105 if status == reqwest::StatusCode::NOT_FOUND {
107 return Ok(Vec::new());
108 }
109
110 let body = response
111 .text()
112 .await
113 .unwrap_or_else(|_| "Unknown error".to_string());
114 return Err(AttestationError::Api(format!(
115 "GitHub API returned {}: {}",
116 status, body
117 )));
118 }
119
120 let attestations_response: AttestationsResponse = response.json().await?;
121
122 let mut attestations = Vec::new();
124 for att in attestations_response.attestations {
125 if att.bundle.is_some() {
126 attestations.push(att);
127 } else if let Some(bundle_url) = &att.bundle_url {
128 let bundle_response = self.client.get(bundle_url).send().await?;
130 if bundle_response.status().is_success() {
131 let bundle: SigstoreBundle = bundle_response.json().await?;
132 attestations.push(Attestation {
133 bundle: Some(bundle),
134 bundle_url: att.bundle_url.clone(),
135 });
136 }
137 }
138 }
139
140 Ok(attestations)
141 }
142}