mithril_cardano_node_internal_database/entities/
ancillary_files_manifest.rs1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use anyhow::Context;
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use thiserror::Error;
8use tokio::io::AsyncReadExt;
9
10use mithril_common::crypto_helper::ManifestSignature;
11use mithril_common::entities::SignableManifest;
12use mithril_common::{StdError, StdResult};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct AncillaryFilesManifest {
17 #[serde(flatten)]
19 pub signable_manifest: SignableManifest<PathBuf, String>,
20}
21
22#[derive(Error, Debug)]
24pub enum AncillaryFilesManifestVerifyError {
25 #[error(
27 "File `{file_path}` hash does not match expected hash, expected: '{expected_hash}', actual: '{actual_hash}'"
28 )]
29 FileHashMismatch {
30 file_path: PathBuf,
32 expected_hash: String,
34 actual_hash: String,
36 },
37 #[error("Failed to compute hash for file `{file_path}`")]
39 HashCompute {
40 file_path: PathBuf,
42 source: StdError,
44 },
45}
46
47impl AncillaryFilesManifest {
48 pub const ANCILLARY_MANIFEST_FILE_NAME: &str = "ancillary_manifest.json";
50
51 pub fn new(data: BTreeMap<PathBuf, String>, signature: ManifestSignature) -> Self {
53 Self {
54 signable_manifest: SignableManifest {
55 data,
56 signature: Some(signature),
57 },
58 }
59 }
60
61 pub fn new_without_signature(data: BTreeMap<PathBuf, String>) -> Self {
63 Self {
64 signable_manifest: SignableManifest {
65 data,
66 signature: None,
67 },
68 }
69 }
70
71 pub fn files(&self) -> Vec<PathBuf> {
73 self.signable_manifest.data.keys().cloned().collect()
74 }
75
76 pub fn signature(&self) -> Option<ManifestSignature> {
78 self.signable_manifest.signature
79 }
80
81 pub fn set_signature(&mut self, signature: ManifestSignature) {
83 self.signable_manifest.signature = Some(signature);
84 }
85
86 pub async fn from_paths(
90 base_directory: &std::path::Path,
91 paths: Vec<PathBuf>,
92 ) -> StdResult<Self> {
93 let mut data = BTreeMap::new();
94
95 for path in paths {
96 let file_path = base_directory.join(&path);
97 let hash = Self::compute_file_hash(&file_path).await.with_context(|| {
98 format!("Failed to compute hash for file `{}`", file_path.display())
99 })?;
100 data.insert(path, hash);
101 }
102
103 Ok(Self::new_without_signature(data))
104 }
105
106 pub async fn verify_data(
110 &self,
111 base_directory: &std::path::Path,
112 ) -> Result<(), AncillaryFilesManifestVerifyError> {
113 for (file_path, expected_hash) in &self.signable_manifest.data {
114 let file_path = base_directory.join(file_path);
115 let actual_hash = Self::compute_file_hash(&file_path).await.map_err(|source| {
116 AncillaryFilesManifestVerifyError::HashCompute {
117 file_path: file_path.clone(),
118 source,
119 }
120 })?;
121
122 if actual_hash != *expected_hash {
123 return Err(AncillaryFilesManifestVerifyError::FileHashMismatch {
124 file_path,
125 expected_hash: expected_hash.clone(),
126 actual_hash,
127 });
128 }
129 }
130
131 Ok(())
132 }
133
134 async fn compute_file_hash(file_path: &std::path::Path) -> StdResult<String> {
135 let mut file = tokio::fs::File::open(&file_path).await?;
136 let mut hasher = Sha256::new();
137
138 let mut data = vec![0; 64 * 1024];
139 loop {
140 let len = file.read(&mut data).await?;
141 if len == 0 {
143 break;
144 }
145
146 hasher.update(&data[..len]);
147 }
148
149 Ok(hex::encode(hasher.finalize()))
150 }
151
152 pub fn compute_hash(&self) -> Vec<u8> {
154 let mut hasher = Sha256::new();
155 for (key, value) in &self.signable_manifest.data {
156 hasher.update(key.to_string_lossy().as_bytes());
157 hasher.update(value.as_bytes());
158 }
159 hasher.finalize().to_vec()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use std::{fs::File, io::Write};
166
167 use mithril_common::temp_dir_create;
168
169 use super::*;
170
171 fn compute_sha256_hash(data: impl AsRef<[u8]>) -> String {
172 hex::encode(Sha256::digest(data))
173 }
174
175 mod ancillary_files_manifest {
176 use super::*;
177
178 const TEST_SIGNATURE: &str = "b5690fe641ee240248d1335092392fefe2399fb11a4bfaddffc790676f4d48a9c34ec648699a3e3b0ba0de8c8bcde5855f16b88eb644d12a9ba1044b5ba91b07";
179
180 #[test]
181 fn can_deserialize_signed_manifest_into_ancillary_files_manifest() {
182 let data = BTreeMap::from([
183 (PathBuf::from("file1"), "hash1".to_string()),
184 (PathBuf::from("file2"), "hash2".to_string()),
185 ]);
186 let signable_manifest = SignableManifest {
187 data: data.clone(),
188 signature: Some(TEST_SIGNATURE.try_into().unwrap()),
189 };
190 let expected_ancillary_manifest =
191 AncillaryFilesManifest::new(data, TEST_SIGNATURE.try_into().unwrap());
192
193 let signable_manifest_json = serde_json::to_string(&signable_manifest).unwrap();
194 let deserialized_ancillary_manifest: AncillaryFilesManifest =
195 serde_json::from_str(&signable_manifest_json).unwrap();
196 assert_eq!(expected_ancillary_manifest, deserialized_ancillary_manifest);
197 }
198
199 #[test]
200 fn compute_hash_when_data_is_empty() {
201 let manifest = AncillaryFilesManifest::new_without_signature(BTreeMap::new());
202 let hash_of_empty_data = Sha256::digest(Vec::<u8>::new()).to_vec();
203
204 assert_eq!(hash_of_empty_data, manifest.compute_hash());
205 }
206
207 #[test]
208 fn compute_hash() {
209 let expected: Vec<u8> = vec![
210 123, 150, 146, 219, 108, 23, 117, 210, 8, 3, 126, 211, 68, 93, 169, 200, 177, 115,
211 169, 219, 87, 2, 238, 52, 209, 37, 214, 207, 21, 188, 246, 127,
212 ];
213
214 let manifest = AncillaryFilesManifest::new_without_signature(BTreeMap::from([
215 (PathBuf::from("file1"), "hash1".to_string()),
216 (PathBuf::from("file2"), "hash2".to_string()),
217 ]));
218
219 assert_eq!(expected, manifest.compute_hash());
220 assert_eq!(
222 expected,
223 AncillaryFilesManifest::new_without_signature(BTreeMap::from([
224 (PathBuf::from("file2"), "hash2".to_string()),
225 (PathBuf::from("file1"), "hash1".to_string()),
226 ]))
227 .compute_hash()
228 );
229 assert_ne!(
230 expected,
231 AncillaryFilesManifest::new_without_signature(BTreeMap::from([
232 (PathBuf::from("file1"), "hash1".to_string()),
233 (PathBuf::from("file3"), "hash3".to_string()),
234 ]))
235 .compute_hash()
236 );
237 assert_ne!(
238 expected,
239 AncillaryFilesManifest::new_without_signature(BTreeMap::from([(
240 PathBuf::from("file1"),
241 "hash1".to_string()
242 ),]),)
243 .compute_hash()
244 );
245 }
246
247 #[test]
248 fn signature_is_not_included_in_compute_hash() {
249 let data = BTreeMap::from([
250 (PathBuf::from("file1"), "hash1".to_string()),
251 (PathBuf::from("file2"), "hash2".to_string()),
252 ]);
253
254 assert_eq!(
255 AncillaryFilesManifest::new(data.clone(), TEST_SIGNATURE.try_into().unwrap())
256 .compute_hash(),
257 AncillaryFilesManifest::new_without_signature(data).compute_hash(),
258 );
259 }
260
261 #[tokio::test]
262 async fn from_paths() {
263 let test_dir = temp_dir_create!();
264 std::fs::create_dir(test_dir.join("sub_folder")).unwrap();
265
266 let file1_path = PathBuf::from("file1.txt");
267 let file2_path = PathBuf::from("sub_folder/file1.txt");
268
269 let mut file1 = File::create(test_dir.join(&file1_path)).unwrap();
270 write!(&mut file1, "file1 content").unwrap();
271
272 let mut file2 = File::create(test_dir.join(&file2_path)).unwrap();
273 write!(&mut file2, "file2 content").unwrap();
274
275 let manifest = AncillaryFilesManifest::from_paths(
276 &test_dir,
277 vec![file1_path.clone(), file2_path.clone()],
278 )
279 .await
280 .expect("Manifest creation should succeed");
281
282 assert_eq!(
283 AncillaryFilesManifest::new_without_signature(BTreeMap::from([
284 (file1_path, compute_sha256_hash("file1 content".as_bytes())),
285 (file2_path, compute_sha256_hash("file2 content".as_bytes())),
286 ]),),
287 manifest
288 );
289 }
290
291 mod verify_data {
292 use super::*;
293
294 #[tokio::test]
295 async fn verify_data_succeed_when_files_hashes_in_target_directory_match() {
296 let test_dir = temp_dir_create!();
297 std::fs::create_dir(test_dir.join("sub_folder")).unwrap();
298
299 let file1_path = PathBuf::from("file1.txt");
300 let file2_path = PathBuf::from("sub_folder/file1.txt");
301
302 File::create(test_dir.join("random_not_included_file.txt")).unwrap();
304
305 let mut file1 = File::create(test_dir.join(&file1_path)).unwrap();
306 write!(&mut file1, "file1 content").unwrap();
307
308 let mut file2 = File::create(test_dir.join(&file2_path)).unwrap();
309 write!(&mut file2, "file2 content").unwrap();
310
311 let manifest = AncillaryFilesManifest::new_without_signature(BTreeMap::from([
312 (file1_path, compute_sha256_hash("file1 content".as_bytes())),
313 (file2_path, compute_sha256_hash("file2 content".as_bytes())),
314 ]));
315
316 manifest
317 .verify_data(&test_dir)
318 .await
319 .expect("Verification should succeed when files exists and hashes match");
320 }
321
322 #[tokio::test]
323 async fn verify_data_fail_when_a_file_in_missing_in_target_directory() {
324 let test_dir = temp_dir_create!();
325 let file_path = PathBuf::from("file1.txt");
326
327 let manifest = AncillaryFilesManifest::new_without_signature(BTreeMap::from([(
328 file_path,
329 compute_sha256_hash("non existent file content".as_bytes()),
330 )]));
331
332 let result = manifest.verify_data(&test_dir).await;
333 assert!(
334 matches!(
335 result,
336 Err(AncillaryFilesManifestVerifyError::HashCompute { .. }),
337 ),
338 "Expected HashCompute error, got: {result:?}",
339 );
340 }
341
342 #[tokio::test]
343 async fn verify_data_fail_when_a_file_hash_does_not_match_in_target_directory() {
344 let test_dir = temp_dir_create!();
345
346 let file_path = PathBuf::from("file1.txt");
347
348 let mut file = File::create(test_dir.join(&file_path)).unwrap();
349 write!(&mut file, "file content").unwrap();
350
351 let manifest = AncillaryFilesManifest::new_without_signature(BTreeMap::from([(
352 file_path,
353 "This is not the file content hash".to_string(),
354 )]));
355
356 let result = manifest.verify_data(&test_dir).await;
357 assert!(
358 matches!(
359 result,
360 Err(AncillaryFilesManifestVerifyError::FileHashMismatch { .. }),
361 ),
362 "Expected FileHashMismatch error, got: {result:?}",
363 );
364 }
365 }
366 }
367}