sigstore_verification/sources/
file.rs

1use 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
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
70                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                        // Parse the nested DSSE envelope
76                        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                // Check if this is a DSSE envelope (SLSA provenance format)
129                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                        // This is a DSSE envelope, parse it into a SigstoreBundle
136                        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, // SLSA files typically don't have this
164                        };
165
166                        let attestation = Attestation {
167                            bundle: Some(bundle),
168                            bundle_url: None,
169                        };
170                        attestations.push(attestation);
171                        continue;
172                    }
173                }
174
175                // Check if this is a simple in-toto statement (alternative format)
176                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                        // This is a raw SLSA provenance statement, wrap it in DSSE
180                        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(), // Minimal signature for parsing
188                                    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                // Check if this is a traditional Cosign bundle format
204                // This check must come before parsing as Attestation since traditional Cosign
205                // JSON can be parsed as Attestation but with bundle=None
206                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                    // This is a traditional Cosign bundle, create a minimal DSSE envelope for compatibility
213                    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(), // Empty payload for traditional Cosign bundles
218                            payload_type: "application/vnd.dev.sigstore.cosign".to_string(),
219                            signatures: vec![Signature {
220                                sig: "".to_string(), // Signature is in the rekor bundle
221                                keyid: None,
222                            }],
223                        },
224                        verification_material: Some(json_value.clone()), // Store the entire Cosign bundle as verification material
225                    };
226
227                    let attestation = Attestation {
228                        bundle: Some(bundle),
229                        bundle_url: None,
230                    };
231                    attestations.push(attestation);
232                    continue;
233                }
234
235                // Try to parse as an existing attestation format
236                if let Ok(attestation) = serde_json::from_value::<Attestation>(json_value.clone()) {
237                    attestations.push(attestation);
238                    continue;
239                }
240
241                // Try as a raw bundle format - convert JSON to SigstoreBundle
242                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}