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    // Common validation
57    validate_common(bundle, options)
58}
59
60/// Validate a v0.2 bundle
61fn validate_v0_2(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
62    // v0.2 requires inclusion proof with checkpoint
63    if options.require_inclusion_proof && !bundle.has_inclusion_proof() {
64        return Err(Error::Validation(
65            "v0.2 bundle must have inclusion proof".to_string(),
66        ));
67    }
68
69    // Validate inclusion proofs
70    validate_inclusion_proofs(bundle)?;
71
72    // Common validation
73    validate_common(bundle, options)
74}
75
76/// Validate a v0.3 bundle
77fn validate_v0_3(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
78    // v0.3 must have single certificate (not chain) or public key
79    match &bundle.verification_material.content {
80        sigstore_types::bundle::VerificationMaterialContent::Certificate(_) => {}
81        sigstore_types::bundle::VerificationMaterialContent::X509CertificateChain { .. } => {
82            return Err(Error::Validation(
83                "v0.3 bundle must use single certificate, not chain".to_string(),
84            ));
85        }
86        sigstore_types::bundle::VerificationMaterialContent::PublicKey { .. } => {}
87    }
88
89    // v0.3 requires inclusion proof
90    if options.require_inclusion_proof && !bundle.has_inclusion_proof() {
91        return Err(Error::Validation(
92            "v0.3 bundle must have inclusion proof".to_string(),
93        ));
94    }
95
96    // Validate inclusion proofs
97    validate_inclusion_proofs(bundle)?;
98
99    // Common validation
100    validate_common(bundle, options)
101}
102
103/// Common validation for all bundle versions
104fn validate_common(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
105    // Must have at least one tlog entry
106    if bundle.verification_material.tlog_entries.is_empty() {
107        return Err(Error::Validation(
108            "bundle must have at least one tlog entry".to_string(),
109        ));
110    }
111
112    // Check timestamp if required
113    if options.require_timestamp
114        && bundle
115            .verification_material
116            .timestamp_verification_data
117            .rfc3161_timestamps
118            .is_empty()
119    {
120        return Err(Error::Validation(
121            "bundle must have timestamp verification data".to_string(),
122        ));
123    }
124
125    Ok(())
126}
127
128/// Validate inclusion proofs in the bundle
129fn validate_inclusion_proofs(bundle: &Bundle) -> Result<()> {
130    for entry in &bundle.verification_material.tlog_entries {
131        if let Some(proof) = &entry.inclusion_proof {
132            // Parse the checkpoint to get the expected root
133            let checkpoint = proof
134                .checkpoint
135                .parse()
136                .map_err(|e| Error::Validation(format!("failed to parse checkpoint: {}", e)))?;
137
138            // Get the leaf (canonicalized body) bytes
139            let leaf_data = entry.canonicalized_body.as_bytes();
140
141            // Get proof hashes (already decoded as Vec<Sha256Hash>)
142            let proof_hashes: &[Sha256Hash] = &proof.hashes;
143
144            // Parse indices
145            let leaf_index: u64 = proof
146                .log_index
147                .as_u64()
148                .map_err(|_| Error::Validation("invalid log_index in proof".to_string()))?;
149            let tree_size: u64 = proof
150                .tree_size
151                .parse()
152                .map_err(|_| Error::Validation("invalid tree_size in proof".to_string()))?;
153
154            // Get expected root from checkpoint (already a Sha256Hash)
155            let expected_root = checkpoint.root_hash;
156
157            // Hash the leaf
158            let leaf_hash = sigstore_merkle::hash_leaf(leaf_data);
159
160            // Verify the inclusion proof
161            verify_inclusion_proof(
162                &leaf_hash,
163                leaf_index,
164                tree_size,
165                proof_hashes,
166                &expected_root,
167            )
168            .map_err(|e| {
169                Error::Validation(format!("inclusion proof verification failed: {}", e))
170            })?;
171        }
172    }
173
174    Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_validation_options_default() {
183        let opts = ValidationOptions::default();
184        assert!(opts.require_inclusion_proof);
185        assert!(!opts.require_timestamp);
186    }
187}