sigstore_verification/sources/
file.rs1use crate::api::{Attestation, DsseEnvelope, MessageDigest, MessageSignature, Signature, SigstoreBundle};
2use crate::sources::{ArtifactRef, AttestationSource};
3use crate::{AttestationError, Result};
4use async_trait::async_trait;
5use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
6use std::path::{Path, PathBuf};
7use tokio::fs;
8
9pub struct FileSource {
11 attestation_path: PathBuf,
13}
14
15impl FileSource {
16 pub fn new(path: impl AsRef<Path>) -> Self {
17 Self {
18 attestation_path: path.as_ref().to_path_buf(),
19 }
20 }
21
22 pub async fn load_bundle(&self) -> Result<serde_json::Value> {
24 let content = fs::read_to_string(&self.attestation_path)
25 .await
26 .map_err(AttestationError::Io)?;
27
28 serde_json::from_str(&content).map_err(AttestationError::Json)
29 }
30
31 pub async fn load_signature(&self) -> Result<Vec<u8>> {
33 fs::read(&self.attestation_path)
34 .await
35 .map_err(AttestationError::Io)
36 }
37}
38
39#[async_trait]
40impl AttestationSource for FileSource {
41 async fn fetch_attestations(&self, _artifact: &ArtifactRef) -> Result<Vec<Attestation>> {
42 let content = fs::read_to_string(&self.attestation_path)
43 .await
44 .map_err(AttestationError::Io)?;
45
46 let mut attestations = Vec::new();
48
49 let lines: Vec<&str> = content.lines().collect();
51 let lines = if lines.is_empty() && !content.trim().is_empty() {
52 vec![content.trim()]
54 } else {
55 lines
56 };
57
58 for line in lines {
59 if line.trim().is_empty() {
60 continue;
61 }
62
63 log::trace!("Parsing line of length: {}", line.len());
64 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line) {
65 log::trace!(
66 "Successfully parsed JSON with keys: {:?}",
67 json_value.as_object().map(|o| o.keys().collect::<Vec<_>>())
68 );
69 if let (Some(media_type), Some(message_signature)) =
71 (json_value.get("mediaType"), json_value.get("messageSignature"))
72 {
73 let media_type_str = media_type.as_str().unwrap_or("");
74 if media_type_str.contains("sigstore.bundle") {
75 if let (Some(message_digest), Some(signature)) = (
77 message_signature.get("messageDigest"),
78 message_signature.get("signature"),
79 ) {
80 if let (Some(algorithm), Some(digest)) = (
81 message_digest.get("algorithm"),
82 message_digest.get("digest"),
83 ) {
84 log::debug!("Found Sigstore Bundle v0.3 with messageSignature (cosign v3 format)");
85 let bundle = SigstoreBundle {
86 media_type: media_type_str.to_string(),
87 dsse_envelope: None,
88 verification_material: json_value
89 .get("verificationMaterial")
90 .cloned(),
91 message_signature: Some(MessageSignature {
92 message_digest: MessageDigest {
93 algorithm: algorithm.as_str().unwrap_or("SHA2_256").to_string(),
94 digest: digest.as_str().unwrap_or("").to_string(),
95 },
96 signature: signature.as_str().unwrap_or("").to_string(),
97 }),
98 };
99
100 let attestation = Attestation {
101 bundle: Some(bundle),
102 bundle_url: None,
103 };
104 attestations.push(attestation);
105 continue;
106 }
107 }
108 }
109 }
110
111 if let (Some(media_type), Some(dsse_envelope)) =
113 (json_value.get("mediaType"), json_value.get("dsseEnvelope"))
114 {
115 if media_type.as_str() == Some("application/vnd.dev.sigstore.bundle.v0.3+json")
116 {
117 if let (Some(payload_type), Some(payload), Some(signatures)) = (
119 dsse_envelope.get("payloadType"),
120 dsse_envelope.get("payload"),
121 dsse_envelope.get("signatures"),
122 ) {
123 if payload_type.as_str() == Some("application/vnd.in-toto+json") {
124 let mut parsed_signatures = Vec::new();
125 if let Some(sig_array) = signatures.as_array() {
126 for sig_obj in sig_array {
127 let sig_string = sig_obj
128 .get("sig")
129 .and_then(|s| s.as_str())
130 .unwrap_or("")
131 .to_string();
132 let keyid = sig_obj
133 .get("keyid")
134 .and_then(|k| k.as_str())
135 .map(|s| s.to_string());
136
137 parsed_signatures.push(Signature {
138 sig: sig_string,
139 keyid,
140 });
141 }
142 }
143
144 let bundle = SigstoreBundle {
145 media_type: media_type.as_str().unwrap_or("").to_string(),
146 dsse_envelope: Some(DsseEnvelope {
147 payload: payload.as_str().unwrap_or("").to_string(),
148 payload_type: payload_type
149 .as_str()
150 .unwrap_or("")
151 .to_string(),
152 signatures: parsed_signatures,
153 }),
154 verification_material: json_value
155 .get("verificationMaterial")
156 .cloned(),
157 message_signature: None,
158 };
159
160 let attestation = Attestation {
161 bundle: Some(bundle),
162 bundle_url: None,
163 };
164 attestations.push(attestation);
165 continue;
166 }
167 }
168 }
169 }
170
171 if let (Some(payload_type), Some(payload), Some(signatures)) = (
173 json_value.get("payloadType"),
174 json_value.get("payload"),
175 json_value.get("signatures"),
176 ) {
177 if payload_type.as_str() == Some("application/vnd.in-toto+json") {
178 let mut parsed_signatures = Vec::new();
180 if let Some(sig_array) = signatures.as_array() {
181 for sig_obj in sig_array {
182 let sig_string = sig_obj
183 .get("sig")
184 .and_then(|s| s.as_str())
185 .unwrap_or("")
186 .to_string();
187 let keyid = sig_obj
188 .get("keyid")
189 .and_then(|k| k.as_str())
190 .map(|s| s.to_string());
191
192 parsed_signatures.push(Signature {
193 sig: sig_string,
194 keyid,
195 });
196 }
197 }
198
199 let bundle = SigstoreBundle {
200 media_type: "application/vnd.in-toto+json".to_string(),
201 dsse_envelope: Some(DsseEnvelope {
202 payload: payload.as_str().unwrap_or("").to_string(),
203 payload_type: payload_type.as_str().unwrap_or("").to_string(),
204 signatures: parsed_signatures,
205 }),
206 verification_material: None, message_signature: None,
208 };
209
210 let attestation = Attestation {
211 bundle: Some(bundle),
212 bundle_url: None,
213 };
214 attestations.push(attestation);
215 continue;
216 }
217 }
218
219 if let Some(type_field) = json_value.get("_type") {
221 let type_str = type_field.as_str().unwrap_or("");
222 if type_str.starts_with("https://in-toto.io/Statement/v") {
223 let bundle = SigstoreBundle {
225 media_type: "application/vnd.in-toto+json".to_string(),
226 dsse_envelope: Some(DsseEnvelope {
227 payload: BASE64
228 .encode(serde_json::to_string(&json_value)?.as_bytes()),
229 payload_type: "application/vnd.in-toto+json".to_string(),
230 signatures: vec![Signature {
231 sig: "".to_string(), keyid: None,
233 }],
234 }),
235 verification_material: None,
236 message_signature: None,
237 };
238
239 let attestation = Attestation {
240 bundle: Some(bundle),
241 bundle_url: None,
242 };
243 attestations.push(attestation);
244 continue;
245 }
246 }
247
248 if let (Some(_base64_sig), Some(_cert), Some(_rekor_bundle)) = (
252 json_value.get("base64Signature"),
253 json_value.get("cert"),
254 json_value.get("rekorBundle"),
255 ) {
256 log::debug!("Found traditional Cosign bundle format");
257 let bundle = SigstoreBundle {
259 media_type: "application/vnd.dev.sigstore.bundle+json;version=0.1"
260 .to_string(),
261 dsse_envelope: Some(DsseEnvelope {
262 payload: "".to_string(), payload_type: "application/vnd.dev.sigstore.cosign".to_string(),
264 signatures: vec![Signature {
265 sig: "".to_string(), keyid: None,
267 }],
268 }),
269 verification_material: Some(json_value.clone()), message_signature: None,
271 };
272
273 let attestation = Attestation {
274 bundle: Some(bundle),
275 bundle_url: None,
276 };
277 attestations.push(attestation);
278 continue;
279 }
280
281 if let Ok(attestation) = serde_json::from_value::<Attestation>(json_value.clone()) {
283 attestations.push(attestation);
284 continue;
285 }
286
287 if let Ok(bundle) = serde_json::from_value::<SigstoreBundle>(json_value) {
289 let attestation = Attestation {
290 bundle: Some(bundle),
291 bundle_url: None,
292 };
293 attestations.push(attestation);
294 }
295 }
296 }
297
298 if attestations.is_empty() {
299 return Err(AttestationError::Verification(
300 "File does not contain valid attestations or SLSA provenance".into(),
301 ));
302 }
303
304 Ok(attestations)
305 }
306
307 fn source_type(&self) -> &'static str {
308 "File"
309 }
310}