sigstore_bundle/
validation.rs1use crate::error::{Error, Result};
6use sigstore_merkle::verify_inclusion_proof;
7use sigstore_types::{Bundle, MediaType, Sha256Hash};
8
9#[derive(Debug, Clone)]
11pub struct ValidationOptions {
12 pub require_inclusion_proof: bool,
14 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
27pub fn validate_bundle(bundle: &Bundle) -> Result<()> {
29 validate_bundle_with_options(bundle, &ValidationOptions::default())
30}
31
32pub fn validate_bundle_with_options(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
34 let version = bundle
36 .version()
37 .map_err(|e| Error::Validation(format!("invalid media type: {}", e)))?;
38
39 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
47fn validate_v0_1(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
49 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(bundle)?;
58
59 validate_common(bundle, options)
61}
62
63fn validate_v0_2(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
65 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(bundle)?;
74
75 validate_common(bundle, options)
77}
78
79fn validate_v0_3(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
81 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 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(bundle)?;
101
102 validate_common(bundle, options)
104}
105
106fn validate_common(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
108 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 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
131fn validate_inclusion_proofs(bundle: &Bundle) -> Result<()> {
133 for entry in &bundle.verification_material.tlog_entries {
134 if let Some(proof) = &entry.inclusion_proof {
135 let checkpoint = proof
137 .checkpoint
138 .parse()
139 .map_err(|e| Error::Validation(format!("failed to parse checkpoint: {}", e)))?;
140
141 let leaf_data = entry.canonicalized_body.as_bytes();
143
144 let proof_hashes: &[Sha256Hash] = &proof.hashes;
146
147 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 let expected_root = checkpoint.root_hash;
159
160 let leaf_hash = sigstore_merkle::hash_leaf(leaf_data);
162
163 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}