sigstore_verification/sources/
file.rs1use crate::api::{Attestation, DsseEnvelope, 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(dsse_envelope)) =
71 (json_value.get("mediaType"), json_value.get("dsseEnvelope"))
72 {
73 if media_type.as_str() == Some("application/vnd.dev.sigstore.bundle.v0.3+json")
74 {
75 if let (Some(payload_type), Some(payload), Some(signatures)) = (
77 dsse_envelope.get("payloadType"),
78 dsse_envelope.get("payload"),
79 dsse_envelope.get("signatures"),
80 ) {
81 if payload_type.as_str() == Some("application/vnd.in-toto+json") {
82 let mut parsed_signatures = Vec::new();
83 if let Some(sig_array) = signatures.as_array() {
84 for sig_obj in sig_array {
85 let sig_string = sig_obj
86 .get("sig")
87 .and_then(|s| s.as_str())
88 .unwrap_or("")
89 .to_string();
90 let keyid = sig_obj
91 .get("keyid")
92 .and_then(|k| k.as_str())
93 .map(|s| s.to_string());
94
95 parsed_signatures.push(Signature {
96 sig: sig_string,
97 keyid,
98 });
99 }
100 }
101
102 let bundle = SigstoreBundle {
103 media_type: media_type.as_str().unwrap_or("").to_string(),
104 dsse_envelope: DsseEnvelope {
105 payload: payload.as_str().unwrap_or("").to_string(),
106 payload_type: payload_type
107 .as_str()
108 .unwrap_or("")
109 .to_string(),
110 signatures: parsed_signatures,
111 },
112 verification_material: json_value
113 .get("verificationMaterial")
114 .cloned(),
115 };
116
117 let attestation = Attestation {
118 bundle: Some(bundle),
119 bundle_url: None,
120 };
121 attestations.push(attestation);
122 continue;
123 }
124 }
125 }
126 }
127
128 if let (Some(payload_type), Some(payload), Some(signatures)) = (
130 json_value.get("payloadType"),
131 json_value.get("payload"),
132 json_value.get("signatures"),
133 ) {
134 if payload_type.as_str() == Some("application/vnd.in-toto+json") {
135 let mut parsed_signatures = Vec::new();
137 if let Some(sig_array) = signatures.as_array() {
138 for sig_obj in sig_array {
139 let sig_string = sig_obj
140 .get("sig")
141 .and_then(|s| s.as_str())
142 .unwrap_or("")
143 .to_string();
144 let keyid = sig_obj
145 .get("keyid")
146 .and_then(|k| k.as_str())
147 .map(|s| s.to_string());
148
149 parsed_signatures.push(Signature {
150 sig: sig_string,
151 keyid,
152 });
153 }
154 }
155
156 let bundle = SigstoreBundle {
157 media_type: "application/vnd.in-toto+json".to_string(),
158 dsse_envelope: DsseEnvelope {
159 payload: payload.as_str().unwrap_or("").to_string(),
160 payload_type: payload_type.as_str().unwrap_or("").to_string(),
161 signatures: parsed_signatures,
162 },
163 verification_material: None, };
165
166 let attestation = Attestation {
167 bundle: Some(bundle),
168 bundle_url: None,
169 };
170 attestations.push(attestation);
171 continue;
172 }
173 }
174
175 if let Some(type_field) = json_value.get("_type") {
177 let type_str = type_field.as_str().unwrap_or("");
178 if type_str.starts_with("https://in-toto.io/Statement/v") {
179 let bundle = SigstoreBundle {
181 media_type: "application/vnd.in-toto+json".to_string(),
182 dsse_envelope: DsseEnvelope {
183 payload: BASE64
184 .encode(serde_json::to_string(&json_value)?.as_bytes()),
185 payload_type: "application/vnd.in-toto+json".to_string(),
186 signatures: vec![Signature {
187 sig: "".to_string(), keyid: None,
189 }],
190 },
191 verification_material: None,
192 };
193
194 let attestation = Attestation {
195 bundle: Some(bundle),
196 bundle_url: None,
197 };
198 attestations.push(attestation);
199 continue;
200 }
201 }
202
203 if let (Some(_base64_sig), Some(_cert), Some(_rekor_bundle)) = (
207 json_value.get("base64Signature"),
208 json_value.get("cert"),
209 json_value.get("rekorBundle"),
210 ) {
211 log::debug!("Found traditional Cosign bundle format");
212 let bundle = SigstoreBundle {
214 media_type: "application/vnd.dev.sigstore.bundle+json;version=0.1"
215 .to_string(),
216 dsse_envelope: DsseEnvelope {
217 payload: "".to_string(), payload_type: "application/vnd.dev.sigstore.cosign".to_string(),
219 signatures: vec![Signature {
220 sig: "".to_string(), keyid: None,
222 }],
223 },
224 verification_material: Some(json_value.clone()), };
226
227 let attestation = Attestation {
228 bundle: Some(bundle),
229 bundle_url: None,
230 };
231 attestations.push(attestation);
232 continue;
233 }
234
235 if let Ok(attestation) = serde_json::from_value::<Attestation>(json_value.clone()) {
237 attestations.push(attestation);
238 continue;
239 }
240
241 if let Ok(bundle) = serde_json::from_value::<SigstoreBundle>(json_value) {
243 let attestation = Attestation {
244 bundle: Some(bundle),
245 bundle_url: None,
246 };
247 attestations.push(attestation);
248 }
249 }
250 }
251
252 if attestations.is_empty() {
253 return Err(AttestationError::Verification(
254 "File does not contain valid attestations or SLSA provenance".into(),
255 ));
256 }
257
258 Ok(attestations)
259 }
260
261 fn source_type(&self) -> &'static str {
262 "File"
263 }
264}