1use sha2::{Digest, Sha256};
2
3use crate::bundle::Bundle;
4use crate::error::{AhuError, Result};
5use crate::index::validate_sort_order;
6
7#[derive(Debug)]
9pub struct VerifyResult {
10 pub header_ok: bool,
11 pub manifest_ok: bool,
12 pub index_digest_ok: bool,
13 pub data_digest_ok: bool,
14 pub sort_order_ok: bool,
15 pub entry_bounds_ok: bool,
16 pub seal_present: bool,
17 pub entry_count_matches: bool,
18 pub warnings: Vec<String>,
19}
20
21pub fn verify_structure(bundle: &Bundle) -> Result<VerifyResult> {
27 let mut result = VerifyResult {
28 header_ok: true,
29 manifest_ok: true,
30 index_digest_ok: false,
31 data_digest_ok: false,
32 sort_order_ok: false,
33 entry_bounds_ok: true,
34 seal_present: !bundle.seal_bytes.is_empty(),
35 entry_count_matches: false,
36 warnings: Vec::new(),
37 };
38
39 let mut index_bytes = Vec::new();
41 for record in &bundle.index {
42 record.write_to(&mut index_bytes)?;
43 }
44 let computed_index_digest: [u8; 32] = Sha256::digest(&index_bytes).into();
45 result.index_digest_ok = computed_index_digest == bundle.manifest.integrity.index_digest;
46 if !result.index_digest_ok {
47 return Err(AhuError::IndexDigestMismatch {
48 expected: hex::encode(bundle.manifest.integrity.index_digest),
49 actual: hex::encode(computed_index_digest),
50 });
51 }
52
53 let computed_data_digest: [u8; 32] = Sha256::digest(&bundle.data).into();
55 result.data_digest_ok = computed_data_digest == bundle.manifest.integrity.data_digest;
56 if !result.data_digest_ok {
57 return Err(AhuError::DataDigestMismatch {
58 expected: hex::encode(bundle.manifest.integrity.data_digest),
59 actual: hex::encode(computed_data_digest),
60 });
61 }
62
63 validate_sort_order(&bundle.index)?;
65 result.sort_order_ok = true;
66
67 result.entry_count_matches = bundle.index.len() as u64 == bundle.manifest.entry_count;
69 if !result.entry_count_matches {
70 result.warnings.push(format!(
71 "entry_count in manifest ({}) does not match index record count ({})",
72 bundle.manifest.entry_count,
73 bundle.index.len()
74 ));
75 }
76
77 for record in &bundle.index {
79 if record.is_tombstone() {
80 continue;
81 }
82 if bundle.entry_bytes(record).is_none() {
83 result.entry_bounds_ok = false;
84 return Err(AhuError::EntryOutOfBounds {
85 offset: record.data_offset,
86 length: record.data_length,
87 });
88 }
89 }
90
91 if bundle.manifest.bundle_type == crate::manifest::BundleType::Delta
93 && bundle.manifest.continuity.base_manifest_digest.is_none()
94 {
95 return Err(AhuError::DeltaMissingBase);
96 }
97
98 if !result.seal_present {
100 result.warnings.push("seal section is empty".into());
101 }
102
103 Ok(result)
104}
105
106pub fn manifest_digest(manifest_bytes: &[u8]) -> [u8; 32] {
108 Sha256::digest(manifest_bytes).into()
109}
110
111pub fn check_epochs(
113 bundle: &Bundle,
114 high_water_marks: &std::collections::HashMap<(String, Vec<u8>), u64>,
115) -> Result<()> {
116 for scope in &bundle.manifest.ca_scopes {
117 let key = (
118 bundle.manifest.producer_id.clone(),
119 scope.issuer_key_hash.clone(),
120 );
121 if let Some(&hw) = high_water_marks.get(&key) {
122 if scope.epoch <= hw {
123 return Err(AhuError::EpochRollback {
124 scope: hex::encode(&scope.issuer_key_hash),
125 epoch: scope.epoch,
126 high_water: hw,
127 });
128 }
129 }
130 }
131 Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::bundle::BundleBuilder;
138 use crate::manifest::*;
139 use sha2::Sha256;
140 use uuid::Uuid;
141
142 fn test_manifest() -> Manifest {
143 Manifest {
144 format_version: 1,
145 bundle_id: Uuid::nil(),
146 producer_id: "test".into(),
147 created_at: 1700000000,
148 bundle_type: BundleType::Full,
149 ca_scopes: vec![CaScope {
150 hash_algorithm: vec![0x01],
151 issuer_name_hash: vec![0xAA; 32],
152 issuer_key_hash: vec![0xBB; 32],
153 epoch: 5,
154 responder_id: ResponderId {
155 id_type: ResponderIdType::ByKey,
156 value: vec![0xCC; 20],
157 },
158 responder_chain: None,
159 signature_algorithm: vec![0x02],
160 completeness: Completeness::AuthoritativeComplete,
161 }],
162 window: Window {
163 produced_at: 1700000000,
164 this_update_min: 1700000000,
165 next_update_min: 1700086400,
166 next_update_max: 1700093600,
167 },
168 integrity: Integrity {
169 index_digest: [0; 32],
170 data_digest: [0; 32],
171 },
172 entry_count: 0,
173 continuity: Continuity {
174 prev_manifest_digest: None,
175 base_manifest_digest: None,
176 chain_length: 0,
177 },
178 shard: None,
179 compression: None,
180 extensions: None,
181 }
182 }
183
184 #[test]
185 fn verify_valid_bundle() {
186 let mut builder = BundleBuilder::new(test_manifest());
187 builder.add_entry([0xAA; 32], b"response".to_vec());
188
189 let bytes = builder.build(|m| Ok(Sha256::digest(m).to_vec())).unwrap();
190
191 let bundle = Bundle::from_bytes(&bytes).unwrap();
192 let result = verify_structure(&bundle).unwrap();
193 assert!(result.index_digest_ok);
194 assert!(result.data_digest_ok);
195 assert!(result.sort_order_ok);
196 assert!(result.entry_bounds_ok);
197 assert!(result.entry_count_matches);
198 }
199
200 #[test]
201 fn epoch_rollback_rejected() {
202 let mut builder = BundleBuilder::new(test_manifest());
203 builder.add_entry([0xAA; 32], b"response".to_vec());
204
205 let bytes = builder.build(|m| Ok(Sha256::digest(m).to_vec())).unwrap();
206
207 let bundle = Bundle::from_bytes(&bytes).unwrap();
208
209 let mut hw = std::collections::HashMap::new();
210 hw.insert(("test".to_string(), vec![0xBB; 32]), 5);
211
212 let err = check_epochs(&bundle, &hw).unwrap_err();
213 assert!(matches!(err, AhuError::EpochRollback { .. }));
214 }
215}