Skip to main content

sigstore_bundle/
validation.rs

1//! Bundle validation
2//!
3//! Validates Sigstore bundles according to version-specific rules.
4
5use crate::error::{Error, Result};
6use sigstore_merkle::verify_inclusion_proof;
7use sigstore_types::{Bundle, MediaType, Sha256Hash};
8
9/// Validation options
10#[derive(Debug, Clone)]
11pub struct ValidationOptions {
12    /// Require inclusion proof (not just promise)
13    pub require_inclusion_proof: bool,
14    /// Require timestamp verification data
15    pub require_timestamp: bool,
16}
17
18impl Default for ValidationOptions {
19    fn default() -> Self {
20        Self {
21            require_inclusion_proof: true,
22            require_timestamp: false,
23        }
24    }
25}
26
27/// Validate a Sigstore bundle
28pub fn validate_bundle(bundle: &Bundle) -> Result<()> {
29    validate_bundle_with_options(bundle, &ValidationOptions::default())
30}
31
32/// Validate a Sigstore bundle with custom options
33pub fn validate_bundle_with_options(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
34    // Check media type is valid
35    let version = bundle
36        .version()
37        .map_err(|e| Error::Validation(format!("invalid media type: {}", e)))?;
38
39    // Version-specific validation
40    match version {
41        MediaType::Bundle0_1 => validate_v0_1(bundle, options),
42        MediaType::Bundle0_2 => validate_v0_2(bundle, options),
43        MediaType::Bundle0_3 => validate_v0_3(bundle, options),
44    }
45}
46
47/// Validate a v0.1 bundle
48fn validate_v0_1(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
49    // v0.1 requires inclusion promise (SET)
50    if !bundle.has_inclusion_promise() {
51        return Err(Error::Validation(
52            "v0.1 bundle must have inclusion promise".to_string(),
53        ));
54    }
55
56    // Validate inclusion proofs if present (v0.1 may have both promise and proof)
57    validate_inclusion_proofs(bundle)?;
58
59    // Common validation
60    validate_common(bundle, options)
61}
62
63/// Validate a v0.2 bundle
64fn validate_v0_2(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
65    // v0.2 requires inclusion proof with checkpoint
66    if options.require_inclusion_proof && !bundle.has_inclusion_proof() {
67        return Err(Error::Validation(
68            "v0.2 bundle must have inclusion proof".to_string(),
69        ));
70    }
71
72    // Validate inclusion proofs
73    validate_inclusion_proofs(bundle)?;
74
75    // Common validation
76    validate_common(bundle, options)
77}
78
79/// Validate a v0.3 bundle
80fn validate_v0_3(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
81    // v0.3 must have single certificate (not chain) or public key
82    match &bundle.verification_material.content {
83        sigstore_types::bundle::VerificationMaterialContent::Certificate(_) => {}
84        sigstore_types::bundle::VerificationMaterialContent::X509CertificateChain { .. } => {
85            return Err(Error::Validation(
86                "v0.3 bundle must use single certificate, not chain".to_string(),
87            ));
88        }
89        sigstore_types::bundle::VerificationMaterialContent::PublicKey { .. } => {}
90    }
91
92    // v0.3 requires inclusion proof
93    if options.require_inclusion_proof && !bundle.has_inclusion_proof() {
94        return Err(Error::Validation(
95            "v0.3 bundle must have inclusion proof".to_string(),
96        ));
97    }
98
99    // Validate inclusion proofs
100    validate_inclusion_proofs(bundle)?;
101
102    // Common validation
103    validate_common(bundle, options)
104}
105
106/// Common validation for all bundle versions
107fn validate_common(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
108    // Must have at least one tlog entry
109    if bundle.verification_material.tlog_entries.is_empty() {
110        return Err(Error::Validation(
111            "bundle must have at least one tlog entry".to_string(),
112        ));
113    }
114
115    // Check timestamp if required
116    if options.require_timestamp
117        && bundle
118            .verification_material
119            .timestamp_verification_data
120            .rfc3161_timestamps
121            .is_empty()
122    {
123        return Err(Error::Validation(
124            "bundle must have timestamp verification data".to_string(),
125        ));
126    }
127
128    Ok(())
129}
130
131/// Validate inclusion proofs in the bundle
132fn validate_inclusion_proofs(bundle: &Bundle) -> Result<()> {
133    for entry in &bundle.verification_material.tlog_entries {
134        if let Some(proof) = &entry.inclusion_proof {
135            // Parse the checkpoint to get the expected root
136            let checkpoint = proof
137                .checkpoint
138                .parse()
139                .map_err(|e| Error::Validation(format!("failed to parse checkpoint: {}", e)))?;
140
141            // Get the leaf (canonicalized body) bytes
142            let leaf_data = entry.canonicalized_body.as_bytes();
143
144            // Get proof hashes (already decoded as Vec<Sha256Hash>)
145            let proof_hashes: &[Sha256Hash] = &proof.hashes;
146
147            // Get indices (now i64 internally)
148            let leaf_index: u64 = proof
149                .log_index
150                .as_u64()
151                .ok_or_else(|| Error::Validation("invalid log_index in proof".to_string()))?;
152            let tree_size: u64 = proof
153                .tree_size
154                .try_into()
155                .map_err(|_| Error::Validation("invalid tree_size in proof".to_string()))?;
156
157            // Get expected root from checkpoint (already a Sha256Hash)
158            let expected_root = checkpoint.root_hash;
159
160            // Hash the leaf
161            let leaf_hash = sigstore_merkle::hash_leaf(leaf_data);
162
163            // Verify the inclusion proof
164            verify_inclusion_proof(
165                &leaf_hash,
166                leaf_index,
167                tree_size,
168                proof_hashes,
169                &expected_root,
170            )
171            .map_err(|e| {
172                Error::Validation(format!("inclusion proof verification failed: {}", e))
173            })?;
174        }
175    }
176
177    Ok(())
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_validation_options_default() {
186        let opts = ValidationOptions::default();
187        assert!(opts.require_inclusion_proof);
188        assert!(!opts.require_timestamp);
189    }
190}