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_common(bundle, options)
58}
59
60fn validate_v0_2(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
62 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(bundle)?;
71
72 validate_common(bundle, options)
74}
75
76fn validate_v0_3(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
78 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 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(bundle)?;
98
99 validate_common(bundle, options)
101}
102
103fn validate_common(bundle: &Bundle, options: &ValidationOptions) -> Result<()> {
105 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 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
128fn validate_inclusion_proofs(bundle: &Bundle) -> Result<()> {
130 for entry in &bundle.verification_material.tlog_entries {
131 if let Some(proof) = &entry.inclusion_proof {
132 let checkpoint = proof
134 .checkpoint
135 .parse()
136 .map_err(|e| Error::Validation(format!("failed to parse checkpoint: {}", e)))?;
137
138 let leaf_data = entry.canonicalized_body.as_bytes();
140
141 let proof_hashes: &[Sha256Hash] = &proof.hashes;
143
144 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 let expected_root = checkpoint.root_hash;
156
157 let leaf_hash = sigstore_merkle::hash_leaf(leaf_data);
159
160 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}