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