sigstore_verification/
api.rs1use crate::{AttestationError, Result};
2use reqwest::Url;
3use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue, USER_AGENT};
4use serde::{Deserialize, Serialize};
5
6const GITHUB_API_URL: &str = "https://api.github.com";
7const USER_AGENT_VALUE: &str = "mise-attestation/0.1.0";
8
9#[derive(Debug, Clone)]
10pub struct AttestationClient {
11 client: reqwest::Client,
12 base_url: String,
13 github_token: Option<String>,
14}
15
16#[derive(Debug, Serialize)]
17pub struct FetchParams {
18 pub owner: String,
19 pub repo: Option<String>,
20 pub digest: String,
21 pub limit: usize,
22 pub predicate_type: Option<String>,
23}
24
25#[derive(Debug, Deserialize)]
26pub struct AttestationsResponse {
27 pub attestations: Vec<Attestation>,
28}
29
30#[derive(Debug, Deserialize, Clone)]
31pub struct Attestation {
32 pub bundle: Option<SigstoreBundle>,
33 pub bundle_url: Option<String>,
34}
35
36#[derive(Debug, Deserialize, Clone)]
37pub struct SigstoreBundle {
38 #[serde(rename = "mediaType")]
39 pub media_type: String,
40 #[serde(rename = "dsseEnvelope")]
41 pub dsse_envelope: Option<DsseEnvelope>,
42 #[serde(rename = "verificationMaterial")]
43 pub verification_material: Option<serde_json::Value>,
44 #[serde(rename = "messageSignature")]
46 pub message_signature: Option<MessageSignature>,
47}
48
49#[derive(Debug, Deserialize, Clone)]
50pub struct MessageSignature {
51 #[serde(rename = "messageDigest")]
52 pub message_digest: MessageDigest,
53 pub signature: String,
54}
55
56#[derive(Debug, Deserialize, Clone)]
57pub struct MessageDigest {
58 pub algorithm: String,
59 pub digest: String,
60}
61
62#[derive(Debug, Deserialize, Clone)]
63pub struct DsseEnvelope {
64 pub payload: String,
65 #[serde(rename = "payloadType")]
66 pub payload_type: String,
67 pub signatures: Vec<Signature>,
68}
69
70#[derive(Debug, Deserialize, Clone)]
71pub struct Signature {
72 pub sig: String,
73 pub keyid: Option<String>,
74}
75
76impl AttestationClient {
77 pub fn new(github_token: Option<&str>) -> Result<Self> {
78 let mut headers = HeaderMap::new();
79 headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
80
81 let client = reqwest::Client::builder()
82 .default_headers(headers)
83 .build()?;
84
85 Ok(Self {
86 client,
87 base_url: GITHUB_API_URL.to_string(),
88 github_token: github_token.map(|s| s.to_string()),
89 })
90 }
91
92 fn github_headers(&self, url: &str) -> Result<HeaderMap> {
93 let mut headers = HeaderMap::new();
94 if let Ok(url) = Url::parse(url) {
95 if url.host_str() == Some("api.github.com") {
96 if let Some(token) = &self.github_token {
97 headers.insert(
98 AUTHORIZATION,
99 HeaderValue::from_str(&format!("Bearer {}", token))
100 .map_err(|e| AttestationError::Api(e.to_string()))?,
101 );
102 }
103 headers.insert(
104 "x-github-api-version",
105 HeaderValue::from_static("2022-11-28"),
106 );
107 }
108 }
109 Ok(headers)
110 }
111
112 pub async fn fetch_attestations(&self, params: FetchParams) -> Result<Vec<Attestation>> {
113 let url = if let Some(repo) = ¶ms.repo {
114 format!(
115 "{}/repos/{}/attestations/{}",
116 self.base_url, repo, params.digest
117 )
118 } else {
119 format!(
120 "{}/orgs/{}/attestations/{}",
121 self.base_url, params.owner, params.digest
122 )
123 };
124
125 let mut query_params = vec![("per_page", params.limit.to_string())];
126 if let Some(predicate_type) = ¶ms.predicate_type {
127 query_params.push(("predicate_type", predicate_type.clone()));
128 }
129
130 let response = self
131 .client
132 .get(&url)
133 .headers(self.github_headers(&url)?)
134 .query(&query_params)
135 .send()
136 .await?;
137
138 if !response.status().is_success() {
139 let status = response.status();
140
141 if status == reqwest::StatusCode::NOT_FOUND {
143 return Ok(Vec::new());
144 }
145
146 let body = response
147 .text()
148 .await
149 .unwrap_or_else(|_| "Unknown error".to_string());
150 return Err(AttestationError::Api(format!(
151 "GitHub API returned {}: {}",
152 status, body
153 )));
154 }
155
156 let attestations_response: AttestationsResponse = response.json().await?;
157
158 let mut attestations = Vec::new();
160 for att in attestations_response.attestations {
161 if att.bundle.is_some() {
162 attestations.push(att);
163 } else if let Some(bundle_url) = &att.bundle_url {
164 let bundle_response = self
166 .client
167 .get(bundle_url)
168 .headers(self.github_headers(bundle_url)?)
169 .send()
170 .await?;
171 if bundle_response.status().is_success() {
172 let bundle: SigstoreBundle = if bundle_response
173 .headers()
174 .get(reqwest::header::CONTENT_TYPE)
175 .and_then(|v| v.to_str().ok())
176 == Some("application/x-snappy")
177 {
178 let bytes = bundle_response.bytes().await?;
179 let decompressed = decompress_snappy(&bytes)?;
180 serde_json::from_slice(&decompressed)?
181 } else {
182 bundle_response.json().await?
183 };
184
185 attestations.push(Attestation {
186 bundle: Some(bundle),
187 bundle_url: att.bundle_url.clone(),
188 });
189 }
190 }
191 }
192
193 Ok(attestations)
194 }
195}
196
197fn decompress_snappy(bytes: &[u8]) -> Result<Vec<u8>> {
198 let mut decoder = snap::raw::Decoder::new();
199 decoder
200 .decompress_vec(bytes)
201 .map_err(|e| AttestationError::Api(format!("Snappy decompression failed: {}", e)))
202}