sigstore_verification/
bundle.rs

1use crate::api::Attestation;
2pub use crate::api::DsseEnvelope;
3use crate::{AttestationError, Result};
4use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
5use serde_json::Value;
6
7/// Parse and extract information from a Sigstore bundle
8pub fn parse_bundle(attestation: &Attestation) -> Result<ParsedBundle> {
9    let bundle = attestation
10        .bundle
11        .as_ref()
12        .ok_or_else(|| AttestationError::Verification("No bundle found in attestation".into()))?;
13
14    // Check if this is a traditional Cosign bundle (empty payload in DSSE envelope, verification material contains the bundle)
15    if bundle.dsse_envelope.payload.is_empty()
16        && bundle.dsse_envelope.payload_type == "application/vnd.dev.sigstore.cosign"
17        && bundle.verification_material.is_some()
18    {
19        // Traditional Cosign bundle - extract what we can
20        let certificate = extract_certificate_from_bundle(attestation)?;
21        let tlog_entries = extract_tlog_entries_from_bundle(attestation)?;
22
23        return Ok(ParsedBundle {
24            payload: Vec::new(), // No SLSA payload for traditional Cosign bundles
25            dsse_envelope: Some(bundle.dsse_envelope.clone()),
26            certificate,
27            media_type: bundle.media_type.clone(),
28            tlog_entries,
29        });
30    }
31
32    // Standard DSSE envelope processing
33    let payload = decode_payload(&bundle.dsse_envelope.payload)?;
34
35    // Extract certificate if present
36    let certificate = extract_certificate_from_bundle(attestation)?;
37
38    // Extract tlog entries if present
39    let tlog_entries = extract_tlog_entries_from_bundle(attestation)?;
40
41    Ok(ParsedBundle {
42        payload,
43        dsse_envelope: Some(bundle.dsse_envelope.clone()),
44        certificate,
45        media_type: bundle.media_type.clone(),
46        tlog_entries,
47    })
48}
49
50/// Decode base64-encoded payload
51fn decode_payload(payload: &str) -> Result<Vec<u8>> {
52    BASE64
53        .decode(payload)
54        .map_err(|e| AttestationError::Verification(format!("Failed to decode payload: {}", e)))
55}
56
57/// Extract certificate from verification material
58fn extract_certificate_from_bundle(attestation: &Attestation) -> Result<Option<String>> {
59    let bundle = attestation
60        .bundle
61        .as_ref()
62        .ok_or_else(|| AttestationError::Verification("No bundle found in attestation".into()))?;
63
64    if let Some(verification_material) = &bundle.verification_material {
65        if let Some(cert) = verification_material.get("certificate") {
66            if let Some(raw_bytes) = cert.get("rawBytes") {
67                if let Some(cert_str) = raw_bytes.as_str() {
68                    return Ok(Some(cert_str.to_string()));
69                }
70            }
71        }
72    }
73
74    Ok(None)
75}
76
77/// Extract tlog entries from verification material
78fn extract_tlog_entries_from_bundle(
79    attestation: &Attestation,
80) -> Result<Option<Vec<serde_json::Value>>> {
81    let bundle = attestation
82        .bundle
83        .as_ref()
84        .ok_or_else(|| AttestationError::Verification("No bundle found in attestation".into()))?;
85
86    if let Some(verification_material) = &bundle.verification_material {
87        // Handle traditional Cosign bundle format
88        if let Some(rekor_bundle) = verification_material.get("rekorBundle") {
89            return Ok(Some(vec![rekor_bundle.clone()]));
90        }
91
92        // Handle modern Sigstore bundle format
93        if let Some(tlog_entries) = verification_material.get("tlogEntries") {
94            if let Some(tlog_array) = tlog_entries.as_array() {
95                return Ok(Some(tlog_array.clone()));
96            }
97        }
98    }
99
100    Ok(None)
101}
102
103/// Parse the payload to extract SLSA provenance information
104pub fn parse_slsa_provenance(payload: &[u8]) -> Result<SlsaProvenance> {
105    let statement: Value = serde_json::from_slice(payload)
106        .map_err(|e| AttestationError::Verification(format!("Failed to parse payload: {}", e)))?;
107
108    // Check if it's an in-toto statement (accept both v0.1 and v1)
109    if let Some(type_field) = statement.get("_type") {
110        let type_str = type_field.as_str().unwrap_or("");
111        if !type_str.starts_with("https://in-toto.io/Statement/v") {
112            return Err(AttestationError::Verification(format!(
113                "Not an in-toto statement: {}",
114                type_str
115            )));
116        }
117    }
118
119    // Extract predicate type
120    let predicate_type = statement
121        .get("predicateType")
122        .and_then(|v| v.as_str())
123        .ok_or_else(|| AttestationError::Verification("Missing predicateType".into()))?;
124
125    if !predicate_type.starts_with("https://slsa.dev/provenance/") {
126        return Err(AttestationError::Verification(format!(
127            "Not a SLSA provenance statement: {}",
128            predicate_type
129        )));
130    }
131
132    // Extract workflow information from predicate
133    let predicate = statement
134        .get("predicate")
135        .ok_or_else(|| AttestationError::Verification("Missing predicate".into()))?;
136
137    let workflow_ref = extract_workflow_ref(predicate)?;
138
139    Ok(SlsaProvenance {
140        predicate_type: predicate_type.to_string(),
141        workflow_ref,
142    })
143}
144
145/// Extract workflow reference from SLSA predicate
146fn extract_workflow_ref(predicate: &Value) -> Result<Option<String>> {
147    // Try v1 format
148    if let Some(build_def) = predicate.get("buildDefinition") {
149        if let Some(ext_params) = build_def.get("externalParameters") {
150            if let Some(workflow) = ext_params.get("workflow") {
151                if let Some(path) = workflow.get("path") {
152                    if let Some(path_str) = path.as_str() {
153                        return Ok(Some(path_str.to_string()));
154                    }
155                }
156            }
157        }
158    }
159
160    // Try v0.2 format
161    if let Some(invocation) = predicate.get("invocation") {
162        if let Some(config_source) = invocation.get("configSource") {
163            if let Some(path) = config_source.get("entryPoint") {
164                if let Some(path_str) = path.as_str() {
165                    return Ok(Some(path_str.to_string()));
166                }
167            }
168        }
169    }
170
171    Ok(None)
172}
173
174#[derive(Debug)]
175pub struct ParsedBundle {
176    pub payload: Vec<u8>,
177    pub dsse_envelope: Option<DsseEnvelope>,
178    pub certificate: Option<String>,
179    pub media_type: String,
180    pub tlog_entries: Option<Vec<serde_json::Value>>,
181}
182
183#[derive(Debug, Clone)]
184pub struct SlsaProvenance {
185    pub predicate_type: String,
186    pub workflow_ref: Option<String>,
187}