sigstore_verification/sources/
file.rs

1use 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
9/// File-based attestation source for loading attestations from local files
10pub struct FileSource {
11    /// Path to the attestation file or bundle
12    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    /// Load a Sigstore bundle from a file
23    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    /// Load a cosign signature from a .sig file
32    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        // Try to parse each line as JSON (JSONL format)
47        let mut attestations = Vec::new();
48
49        // Handle both JSONL format (multiple lines) and single JSON object
50        let lines: Vec<&str> = content.lines().collect();
51        let lines = if lines.is_empty() && !content.trim().is_empty() {
52            // Single JSON object without newline
53            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                // Check if this is a Sigstore Bundle v0.3 format with messageSignature (cosign v3)
70                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                        // Parse messageSignature for direct blob signing
76                        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                // Check if this is a Sigstore Bundle v0.3 format with dsseEnvelope
112                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                        // Parse the nested DSSE envelope
118                        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                // Check if this is a DSSE envelope (SLSA provenance format)
172                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                        // This is a DSSE envelope, parse it into a SigstoreBundle
179                        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, // SLSA files typically don't have this
207                            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                // Check if this is a simple in-toto statement (alternative format)
220                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                        // This is a raw SLSA provenance statement, wrap it in DSSE
224                        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(), // Minimal signature for parsing
232                                    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                // Check if this is a traditional Cosign bundle format
249                // This check must come before parsing as Attestation since traditional Cosign
250                // JSON can be parsed as Attestation but with bundle=None
251                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                    // This is a traditional Cosign bundle, create a minimal DSSE envelope for compatibility
258                    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(), // Empty payload for traditional Cosign bundles
263                            payload_type: "application/vnd.dev.sigstore.cosign".to_string(),
264                            signatures: vec![Signature {
265                                sig: "".to_string(), // Signature is in the rekor bundle
266                                keyid: None,
267                            }],
268                        }),
269                        verification_material: Some(json_value.clone()), // Store the entire Cosign bundle as verification material
270                        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                // Try to parse as an existing attestation format
282                if let Ok(attestation) = serde_json::from_value::<Attestation>(json_value.clone()) {
283                    attestations.push(attestation);
284                    continue;
285                }
286
287                // Try as a raw bundle format - convert JSON to SigstoreBundle
288                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}