sbom_tools/verification/
hash.rs1use std::fmt;
6use std::fs;
7use std::path::Path;
8
9use sha2::{Digest, Sha256, Sha512};
10
11#[derive(Debug, thiserror::Error)]
13pub enum HashError {
14 #[error(transparent)]
16 Io(#[from] std::io::Error),
17 #[error(
19 "unrecognized hash format (length {length}), expected sha256:<hex> or sha512:<hex>, \
20 or a 64-char (SHA-256) or 128-char (SHA-512) hex string"
21 )]
22 UnrecognizedFormat {
23 length: usize,
25 },
26}
27
28#[derive(Debug, Clone)]
30pub struct HashVerifyResult {
31 pub verified: bool,
33 pub algorithm: String,
35 pub expected: String,
37 pub actual: String,
39}
40
41impl fmt::Display for HashVerifyResult {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 if self.verified {
44 write!(f, "OK: {} hash verified", self.algorithm)
45 } else {
46 write!(
47 f,
48 "MISMATCH: {} hash\n expected: {}\n actual: {}",
49 self.algorithm, self.expected, self.actual
50 )
51 }
52 }
53}
54
55pub fn compute_file_sha256(path: &Path) -> Result<String, HashError> {
64 use std::io::Read;
65
66 let mut file = fs::File::open(path)?;
67 let mut hasher = Sha256::new();
68 let mut buf = vec![0u8; 1 << 20];
69 loop {
70 let n = file.read(&mut buf)?;
71 if n == 0 {
72 break;
73 }
74 hasher.update(&buf[..n]);
75 }
76 Ok(hasher
77 .finalize()
78 .iter()
79 .map(|b| format!("{b:02x}"))
80 .collect())
81}
82
83pub fn verify_file_hash(path: &Path, expected: &str) -> Result<HashVerifyResult, HashError> {
94 let content = fs::read(path)?;
95 let expected = expected.trim();
96
97 let expected = if expected.contains(' ') {
99 expected.split_whitespace().next().unwrap_or(expected)
100 } else {
101 expected
102 };
103
104 let expected_lower = expected.to_lowercase();
106 let (algorithm, expected_hex) = if let Some(hex) = expected_lower.strip_prefix("sha256:") {
107 ("SHA-256", hex.to_string())
108 } else if let Some(hex) = expected_lower.strip_prefix("sha512:") {
109 ("SHA-512", hex.to_string())
110 } else {
111 match expected.len() {
112 64 => ("SHA-256", expected.to_string()),
113 128 => ("SHA-512", expected.to_string()),
114 _ => {
115 return Err(HashError::UnrecognizedFormat {
116 length: expected.len(),
117 });
118 }
119 }
120 };
121
122 let actual_hex = match algorithm {
123 "SHA-256" => {
124 let mut hasher = Sha256::new();
125 hasher.update(&content);
126 hasher
127 .finalize()
128 .iter()
129 .map(|b| format!("{b:02x}"))
130 .collect::<String>()
131 }
132 "SHA-512" => {
133 let mut hasher = Sha512::new();
134 hasher.update(&content);
135 hasher
136 .finalize()
137 .iter()
138 .map(|b| format!("{b:02x}"))
139 .collect::<String>()
140 }
141 _ => unreachable!(),
142 };
143
144 let expected_lower = expected_hex.to_lowercase();
145 let verified = actual_hex == expected_lower;
146
147 Ok(HashVerifyResult {
148 verified,
149 algorithm: algorithm.to_string(),
150 expected: expected_lower,
151 actual: actual_hex,
152 })
153}
154
155pub fn read_hash_file(path: &Path) -> Result<String, HashError> {
163 let content = fs::read_to_string(path)?;
164 let trimmed = content.trim();
165 let hash = trimmed.split_whitespace().next().unwrap_or(trimmed);
166 Ok(hash.to_string())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use std::io::Write;
173 use tempfile::NamedTempFile;
174
175 #[test]
176 fn verify_sha256_match() {
177 let mut f = NamedTempFile::new().unwrap();
178 f.write_all(b"hello world").unwrap();
179 f.flush().unwrap();
180
181 let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
183 let result = verify_file_hash(f.path(), expected).unwrap();
184 assert!(result.verified);
185 assert_eq!(result.algorithm, "SHA-256");
186 }
187
188 #[test]
189 fn verify_sha256_mismatch() {
190 let mut f = NamedTempFile::new().unwrap();
191 f.write_all(b"hello world").unwrap();
192 f.flush().unwrap();
193
194 let expected = "0000000000000000000000000000000000000000000000000000000000000000";
195 let result = verify_file_hash(f.path(), expected).unwrap();
196 assert!(!result.verified);
197 }
198
199 #[test]
200 fn verify_prefixed_sha256() {
201 let mut f = NamedTempFile::new().unwrap();
202 f.write_all(b"hello world").unwrap();
203 f.flush().unwrap();
204
205 let expected = "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
206 let result = verify_file_hash(f.path(), expected).unwrap();
207 assert!(result.verified);
208 }
209
210 #[test]
211 fn verify_sha256sum_file_format() {
212 let mut f = NamedTempFile::new().unwrap();
213 f.write_all(b"hello world").unwrap();
214 f.flush().unwrap();
215
216 let expected =
217 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 somefile.json";
218 let result = verify_file_hash(f.path(), expected).unwrap();
219 assert!(result.verified);
220 }
221
222 #[test]
223 fn verify_bad_length() {
224 let mut f = NamedTempFile::new().unwrap();
225 f.write_all(b"test").unwrap();
226 f.flush().unwrap();
227
228 let result = verify_file_hash(f.path(), "abcdef");
229 assert!(result.is_err());
230 }
231
232 #[test]
233 fn read_hash_file_format() {
234 let mut f = NamedTempFile::new().unwrap();
235 writeln!(f, "abcd1234 sbom.json").unwrap();
236 f.flush().unwrap();
237
238 let hash = read_hash_file(f.path()).unwrap();
239 assert_eq!(hash, "abcd1234");
240 }
241}