1use csaf_models::settings::Settings;
17use sha2::{Digest as Sha2Digest, Sha256, Sha512};
18use sha3::Sha3_512;
19
20use crate::fs::DataDir;
21
22#[doc(hidden)]
27pub use generic_array::typenum::Unsigned as _GenericArrayUnsigned;
28
29use crate::error::Result;
30
31#[must_use]
33pub fn sha256_hex(data: &[u8]) -> String {
34 let mut hasher = Sha256::new();
35 hasher.update(data);
36 let result = hasher.finalize();
37 hex_encode(&result)
38}
39
40#[must_use]
42pub fn sha512_hex(data: &[u8]) -> String {
43 let mut hasher = Sha512::new();
44 hasher.update(data);
45 let result = hasher.finalize();
46 hex_encode(&result)
47}
48
49#[must_use]
51pub fn sha3_512_hex(data: &[u8]) -> String {
52 let mut hasher = Sha3_512::new();
53 hasher.update(data);
54 let result = hasher.finalize();
55 hex_encode(&result)
56}
57
58#[must_use]
63pub fn generate_hashes(data: &[u8]) -> (String, String) {
64 (sha256_hex(data), sha3_512_hex(data))
65}
66
67#[must_use]
69pub fn generate_all_hashes(data: &[u8]) -> (String, String, String) {
70 (sha256_hex(data), sha512_hex(data), sha3_512_hex(data))
71}
72
73#[derive(Debug, Clone, Copy)]
79pub struct SidecarHashes {
80 pub sha256: bool,
82 pub sha512: bool,
84 pub sha3_512: bool,
86}
87
88impl SidecarHashes {
89 #[must_use]
91 pub const fn from_settings(settings: &Settings) -> Self {
92 Self {
93 sha256: settings.sidecar_sha256,
94 sha512: settings.sidecar_sha512,
95 sha3_512: settings.sidecar_sha3_512,
96 }
97 }
98}
99
100pub fn write_sidecar_files(
112 dir: &DataDir,
113 rel: &str,
114 data: &[u8],
115 hashes: SidecarHashes,
116) -> Result<()> {
117 let filename = rel.rsplit('/').next().unwrap_or(rel);
118
119 if hashes.sha256 {
120 dir.write(
121 &format!("{rel}.sha-256"),
122 format!("{} {filename}\n", sha256_hex(data)).as_bytes(),
123 )?;
124 }
125
126 if hashes.sha512 {
127 dir.write(
128 &format!("{rel}.sha-512"),
129 format!("{} {filename}\n", sha512_hex(data)).as_bytes(),
130 )?;
131 }
132
133 if hashes.sha3_512 {
134 dir.write(
135 &format!("{rel}.sha3-512"),
136 format!("{} {filename}\n", sha3_512_hex(data)).as_bytes(),
137 )?;
138 }
139
140 Ok(())
141}
142
143pub fn write_sidecar_files_for(
155 dir: &DataDir,
156 rel: &str,
157 data: &[u8],
158 hashes: SidecarHashes,
159) -> Result<Vec<String>> {
160 let filename = rel.rsplit('/').next().unwrap_or(rel);
161 let mut written = Vec::new();
162
163 if hashes.sha256 {
164 let sidecar = format!("{rel}.sha-256");
165 dir.write(
166 &sidecar,
167 format!("{} {filename}\n", sha256_hex(data)).as_bytes(),
168 )?;
169 written.push(sidecar);
170 }
171
172 if hashes.sha512 {
173 let sidecar = format!("{rel}.sha-512");
174 dir.write(
175 &sidecar,
176 format!("{} {filename}\n", sha512_hex(data)).as_bytes(),
177 )?;
178 written.push(sidecar);
179 }
180
181 if hashes.sha3_512 {
182 let sidecar = format!("{rel}.sha3-512");
183 dir.write(
184 &sidecar,
185 format!("{} {filename}\n", sha3_512_hex(data)).as_bytes(),
186 )?;
187 written.push(sidecar);
188 }
189
190 Ok(written)
191}
192
193fn hex_encode(bytes: &[u8]) -> String {
195 use std::fmt::Write as _;
196 let mut hex = String::with_capacity(bytes.len() * 2);
197 for b in bytes {
198 let _ = write!(hex, "{b:02x}");
199 }
200 hex
201}
202
203#[cfg(test)]
204#[allow(clippy::case_sensitive_file_extension_comparisons)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_sha256_known_value() {
212 let hash = sha256_hex(b"");
214 assert_eq!(
215 hash,
216 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
217 );
218 }
219
220 #[test]
221 fn test_sha256_hello() {
222 let hash = sha256_hex(b"hello");
223 assert_eq!(
224 hash,
225 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
226 );
227 }
228
229 #[test]
230 fn test_sha512_known_value() {
231 let hash = sha512_hex(b"");
233 assert_eq!(
234 hash,
235 "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\
236 47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
237 );
238 }
239
240 #[test]
241 fn test_sha512_hello() {
242 let hash = sha512_hex(b"hello");
243 assert_eq!(
244 hash,
245 "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca7\
246 2323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
247 );
248 }
249
250 #[test]
251 fn test_sha3_512_known_value() {
252 let hash = sha3_512_hex(b"");
254 assert_eq!(
255 hash,
256 "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615\
257 b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
258 );
259 }
260
261 #[test]
262 fn test_generate_hashes_legacy() {
263 let (sha256, sha3) = generate_hashes(b"test data");
264 assert_eq!(sha256.len(), 64); assert_eq!(sha3.len(), 128); }
267
268 #[test]
269 fn test_generate_all_hashes_triplet() {
270 let (sha256, sha512, sha3) = generate_all_hashes(b"test data");
271 assert_eq!(sha256.len(), 64);
272 assert_eq!(sha512.len(), 128);
273 assert_eq!(sha3.len(), 128);
274 assert_ne!(sha256, sha512);
276 assert_ne!(sha512, sha3);
277 assert_ne!(sha256, sha3);
278 }
279
280 #[test]
281 fn test_deterministic() {
282 let data = b"CSAF document content";
283 let (h1_256, h1_512, h1_3) = generate_all_hashes(data);
284 let (h2_256, h2_512, h2_3) = generate_all_hashes(data);
285 assert_eq!(h1_256, h2_256);
286 assert_eq!(h1_512, h2_512);
287 assert_eq!(h1_3, h2_3);
288 }
289
290 #[test]
291 fn test_write_sidecar_files() {
292 let dir = tempfile::tempdir().expect("tmpdir failed");
293 let data = b"{\"test\": true}";
294 let dd = DataDir::open(dir.path()).expect("open base");
295 dd.write("test.json", data).expect("write json");
296
297 write_sidecar_files(
298 &dd,
299 "test.json",
300 data,
301 SidecarHashes {
302 sha256: true,
303 sha512: true,
304 sha3_512: true,
305 },
306 )
307 .expect("sidecar write failed");
308
309 let sha256_path = dir.path().join("test.json.sha-256");
310 let sha512_path = dir.path().join("test.json.sha-512");
311 let sha3_path = dir.path().join("test.json.sha3-512");
312
313 assert!(sha256_path.exists());
314 assert!(sha512_path.exists());
315 assert!(sha3_path.exists());
316
317 let sha256_content = std::fs::read_to_string(&sha256_path).expect("read failed");
318 assert!(sha256_content.contains("test.json"));
319 assert!(sha256_content.contains(" ")); assert!(!dir.path().join("test.json.sha256").exists());
323 assert!(!dir.path().join("test.json.sha512").exists());
324 }
325
326 #[test]
327 fn test_write_only_sha256() {
328 let dir = tempfile::tempdir().expect("tmpdir failed");
329 let data = b"{}";
330 let dd = DataDir::open(dir.path()).expect("open base");
331 dd.write("test.json", data).expect("write json");
332
333 write_sidecar_files(
334 &dd,
335 "test.json",
336 data,
337 SidecarHashes {
338 sha256: true,
339 sha512: false,
340 sha3_512: false,
341 },
342 )
343 .expect("sidecar write failed");
344
345 assert!(dir.path().join("test.json.sha-256").exists());
346 assert!(!dir.path().join("test.json.sha-512").exists());
347 assert!(!dir.path().join("test.json.sha3-512").exists());
348 }
349
350 #[test]
351 fn test_write_only_sha512() {
352 let dir = tempfile::tempdir().expect("tmpdir failed");
353 let data = b"{}";
354 let dd = DataDir::open(dir.path()).expect("open base");
355 dd.write("test.json", data).expect("write json");
356
357 write_sidecar_files(
358 &dd,
359 "test.json",
360 data,
361 SidecarHashes {
362 sha256: false,
363 sha512: true,
364 sha3_512: false,
365 },
366 )
367 .expect("sidecar write failed");
368
369 assert!(!dir.path().join("test.json.sha-256").exists());
370 assert!(dir.path().join("test.json.sha-512").exists());
371 assert!(!dir.path().join("test.json.sha3-512").exists());
372 }
373
374 #[test]
375 fn test_write_sidecar_files_for_redb() {
376 let dir = tempfile::tempdir().expect("tmpdir failed");
377 let data = b"dummy-redb-bytes";
378 let dd = DataDir::open(dir.path()).expect("open base");
379 dd.write("csaf.redb", data).expect("write redb");
380
381 let written = write_sidecar_files_for(
382 &dd,
383 "csaf.redb",
384 data,
385 SidecarHashes {
386 sha256: true,
387 sha512: true,
388 sha3_512: true,
389 },
390 )
391 .expect("sidecar write failed");
392
393 assert_eq!(
394 written,
395 vec![
396 "csaf.redb.sha-256".to_string(),
397 "csaf.redb.sha-512".to_string(),
398 "csaf.redb.sha3-512".to_string(),
399 ]
400 );
401 assert!(dir.path().join("csaf.redb.sha-256").exists());
402 assert!(dir.path().join("csaf.redb.sha-512").exists());
403 assert!(dir.path().join("csaf.redb.sha3-512").exists());
404
405 let sha_content =
406 std::fs::read_to_string(dir.path().join("csaf.redb.sha-256")).expect("read");
407 assert!(sha_content.contains("csaf.redb"));
408 assert!(sha_content.contains(" "));
409 assert!(sha_content.contains(&sha256_hex(data)));
410
411 let sha512_content =
412 std::fs::read_to_string(dir.path().join("csaf.redb.sha-512")).expect("read");
413 assert!(sha512_content.contains(&sha512_hex(data)));
414 }
415
416 #[test]
417 fn test_write_sidecar_files_for_skip_sha3() {
418 let dir = tempfile::tempdir().expect("tmpdir failed");
419 let data = b"dummy-sqlite";
420 let dd = DataDir::open(dir.path()).expect("open base");
421 dd.write("csaf.sqlite", data).expect("write sqlite");
422
423 let written = write_sidecar_files_for(
424 &dd,
425 "csaf.sqlite",
426 data,
427 SidecarHashes {
428 sha256: true,
429 sha512: true,
430 sha3_512: false,
431 },
432 )
433 .expect("sidecar write");
434 assert_eq!(
435 written,
436 vec![
437 "csaf.sqlite.sha-256".to_string(),
438 "csaf.sqlite.sha-512".to_string(),
439 ]
440 );
441 assert!(!dir.path().join("csaf.sqlite.sha3-512").exists());
442 }
443
444 #[test]
445 fn test_write_sidecar_files_for_skip_all_but_sha3() {
446 let dir = tempfile::tempdir().expect("tmpdir failed");
447 let data = b"x";
448 let dd = DataDir::open(dir.path()).expect("open base");
449 dd.write("evidence.bin", data).expect("write evidence");
450
451 let written = write_sidecar_files_for(
452 &dd,
453 "evidence.bin",
454 data,
455 SidecarHashes {
456 sha256: false,
457 sha512: false,
458 sha3_512: true,
459 },
460 )
461 .expect("sidecar write");
462 assert_eq!(written, vec!["evidence.bin.sha3-512".to_string()]);
463 assert!(dir.path().join("evidence.bin.sha3-512").exists());
464 }
465
466 #[test]
467 fn test_extension_uses_hyphenated_form_only() {
468 let dir = tempfile::tempdir().expect("tmpdir failed");
471 let dd = DataDir::open(dir.path()).expect("open base");
472 dd.write("payload.json", b"payload").expect("write json");
473
474 write_sidecar_files(
475 &dd,
476 "payload.json",
477 b"payload",
478 SidecarHashes {
479 sha256: true,
480 sha512: true,
481 sha3_512: true,
482 },
483 )
484 .expect("sidecar write");
485
486 for entry in std::fs::read_dir(dir.path()).expect("readdir") {
487 let name = entry.unwrap().file_name().to_string_lossy().to_string();
488 assert!(
490 name.ends_with(".json")
491 || name.ends_with(".json.sha-256")
492 || name.ends_with(".json.sha-512")
493 || name.ends_with(".json.sha3-512"),
494 "unexpected sidecar extension: {name}"
495 );
496 assert!(
497 !(name.ends_with(".sha256") || name.ends_with(".sha512")),
498 "legacy unhyphenated form leaked: {name}"
499 );
500 }
501 }
502
503 #[test]
504 fn test_sidecar_matches_csaf_file() {
505 let json = include_str!("../../../test/csaf/2026/003/ndaal-sa-2026-003.json");
506 let (sha256, sha512, sha3) = generate_all_hashes(json.as_bytes());
507
508 assert_eq!(sha256.len(), 64);
510 assert_eq!(sha512.len(), 128);
511 assert_eq!(sha3.len(), 128);
512
513 let (sha256_again, sha512_again, sha3_again) = generate_all_hashes(json.as_bytes());
515 assert_eq!(sha256, sha256_again);
516 assert_eq!(sha512, sha512_again);
517 assert_eq!(sha3, sha3_again);
518 }
519}