sigstore_verification/sources/
file.rs

1use crate::api::{Attestation, SigstoreBundle, DsseEnvelope, Signature};
2use crate::sources::{ArtifactRef, AttestationSource};
3use crate::{AttestationError, Result};
4use async_trait::async_trait;
5use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
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)
29            .map_err(AttestationError::Json)
30    }
31
32    /// Load a cosign signature from a .sig file
33    pub async fn load_signature(&self) -> Result<Vec<u8>> {
34        fs::read(&self.attestation_path)
35            .await
36            .map_err(AttestationError::Io)
37    }
38}
39
40#[async_trait]
41impl AttestationSource for FileSource {
42    async fn fetch_attestations(&self, _artifact: &ArtifactRef) -> Result<Vec<Attestation>> {
43        let content = fs::read_to_string(&self.attestation_path)
44            .await
45            .map_err(AttestationError::Io)?;
46
47        // Try to parse each line as JSON (JSONL format)
48        let mut attestations = Vec::new();
49
50        for line in content.lines() {
51            if line.trim().is_empty() {
52                continue;
53            }
54
55            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line) {
56                // Check if this is a DSSE envelope (SLSA provenance format)
57                if let (Some(payload_type), Some(payload), Some(signatures)) = (
58                    json_value.get("payloadType"),
59                    json_value.get("payload"),
60                    json_value.get("signatures")
61                ) {
62                    if payload_type.as_str() == Some("application/vnd.in-toto+json") {
63                        // This is a DSSE envelope, parse it into a SigstoreBundle
64                        let mut parsed_signatures = Vec::new();
65                        if let Some(sig_array) = signatures.as_array() {
66                            for sig_obj in sig_array {
67                                let sig_string = sig_obj.get("sig")
68                                    .and_then(|s| s.as_str())
69                                    .unwrap_or("")
70                                    .to_string();
71                                let keyid = sig_obj.get("keyid")
72                                    .and_then(|k| k.as_str())
73                                    .map(|s| s.to_string());
74
75                                parsed_signatures.push(Signature {
76                                    sig: sig_string,
77                                    keyid,
78                                });
79                            }
80                        }
81
82                        let bundle = SigstoreBundle {
83                            media_type: "application/vnd.in-toto+json".to_string(),
84                            dsse_envelope: DsseEnvelope {
85                                payload: payload.as_str().unwrap_or("").to_string(),
86                                payload_type: payload_type.as_str().unwrap_or("").to_string(),
87                                signatures: parsed_signatures,
88                            },
89                            verification_material: None, // SLSA files typically don't have this
90                        };
91
92                        let attestation = Attestation {
93                            bundle: Some(bundle),
94                            bundle_url: None,
95                        };
96                        attestations.push(attestation);
97                        continue;
98                    }
99                }
100
101                // Check if this is a simple in-toto statement (alternative format)
102                if let Some(type_field) = json_value.get("_type") {
103                    let type_str = type_field.as_str().unwrap_or("");
104                    if type_str.starts_with("https://in-toto.io/Statement/v") {
105                        // This is a raw SLSA provenance statement, wrap it in DSSE
106                        let bundle = SigstoreBundle {
107                            media_type: "application/vnd.in-toto+json".to_string(),
108                            dsse_envelope: DsseEnvelope {
109                                payload: BASE64.encode(serde_json::to_string(&json_value)?.as_bytes()),
110                                payload_type: "application/vnd.in-toto+json".to_string(),
111                                signatures: vec![Signature {
112                                    sig: "".to_string(), // Minimal signature for parsing
113                                    keyid: None,
114                                }],
115                            },
116                            verification_material: None,
117                        };
118
119                        let attestation = Attestation {
120                            bundle: Some(bundle),
121                            bundle_url: None,
122                        };
123                        attestations.push(attestation);
124                        continue;
125                    }
126                }
127
128                // Try to parse as an existing attestation format
129                if let Ok(attestation) = serde_json::from_value::<Attestation>(json_value.clone()) {
130                    attestations.push(attestation);
131                    continue;
132                }
133
134                // Try as a raw bundle format - convert JSON to SigstoreBundle
135                if let Ok(bundle) = serde_json::from_value::<SigstoreBundle>(json_value) {
136                    let attestation = Attestation {
137                        bundle: Some(bundle),
138                        bundle_url: None,
139                    };
140                    attestations.push(attestation);
141                }
142            }
143        }
144
145        if attestations.is_empty() {
146            return Err(AttestationError::Verification(
147                "File does not contain valid attestations or SLSA provenance".into()
148            ));
149        }
150
151        Ok(attestations)
152    }
153
154    fn source_type(&self) -> &'static str {
155        "File"
156    }
157}