sigstore_verification/
bundle.rs

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