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 verify_file_hash(path: &Path, expected: &str) -> Result<HashVerifyResult, HashError> {
66 let content = fs::read(path)?;
67 let expected = expected.trim();
68
69 let expected = if expected.contains(' ') {
71 expected.split_whitespace().next().unwrap_or(expected)
72 } else {
73 expected
74 };
75
76 let expected_lower = expected.to_lowercase();
78 let (algorithm, expected_hex) = if let Some(hex) = expected_lower.strip_prefix("sha256:") {
79 ("SHA-256", hex.to_string())
80 } else if let Some(hex) = expected_lower.strip_prefix("sha512:") {
81 ("SHA-512", hex.to_string())
82 } else {
83 match expected.len() {
84 64 => ("SHA-256", expected.to_string()),
85 128 => ("SHA-512", expected.to_string()),
86 _ => {
87 return Err(HashError::UnrecognizedFormat {
88 length: expected.len(),
89 });
90 }
91 }
92 };
93
94 let actual_hex = match algorithm {
95 "SHA-256" => {
96 let mut hasher = Sha256::new();
97 hasher.update(&content);
98 hasher
99 .finalize()
100 .iter()
101 .map(|b| format!("{b:02x}"))
102 .collect::<String>()
103 }
104 "SHA-512" => {
105 let mut hasher = Sha512::new();
106 hasher.update(&content);
107 hasher
108 .finalize()
109 .iter()
110 .map(|b| format!("{b:02x}"))
111 .collect::<String>()
112 }
113 _ => unreachable!(),
114 };
115
116 let expected_lower = expected_hex.to_lowercase();
117 let verified = actual_hex == expected_lower;
118
119 Ok(HashVerifyResult {
120 verified,
121 algorithm: algorithm.to_string(),
122 expected: expected_lower,
123 actual: actual_hex,
124 })
125}
126
127pub fn read_hash_file(path: &Path) -> Result<String, HashError> {
135 let content = fs::read_to_string(path)?;
136 let trimmed = content.trim();
137 let hash = trimmed.split_whitespace().next().unwrap_or(trimmed);
138 Ok(hash.to_string())
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::io::Write;
145 use tempfile::NamedTempFile;
146
147 #[test]
148 fn verify_sha256_match() {
149 let mut f = NamedTempFile::new().unwrap();
150 f.write_all(b"hello world").unwrap();
151 f.flush().unwrap();
152
153 let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
155 let result = verify_file_hash(f.path(), expected).unwrap();
156 assert!(result.verified);
157 assert_eq!(result.algorithm, "SHA-256");
158 }
159
160 #[test]
161 fn verify_sha256_mismatch() {
162 let mut f = NamedTempFile::new().unwrap();
163 f.write_all(b"hello world").unwrap();
164 f.flush().unwrap();
165
166 let expected = "0000000000000000000000000000000000000000000000000000000000000000";
167 let result = verify_file_hash(f.path(), expected).unwrap();
168 assert!(!result.verified);
169 }
170
171 #[test]
172 fn verify_prefixed_sha256() {
173 let mut f = NamedTempFile::new().unwrap();
174 f.write_all(b"hello world").unwrap();
175 f.flush().unwrap();
176
177 let expected = "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
178 let result = verify_file_hash(f.path(), expected).unwrap();
179 assert!(result.verified);
180 }
181
182 #[test]
183 fn verify_sha256sum_file_format() {
184 let mut f = NamedTempFile::new().unwrap();
185 f.write_all(b"hello world").unwrap();
186 f.flush().unwrap();
187
188 let expected =
189 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 somefile.json";
190 let result = verify_file_hash(f.path(), expected).unwrap();
191 assert!(result.verified);
192 }
193
194 #[test]
195 fn verify_bad_length() {
196 let mut f = NamedTempFile::new().unwrap();
197 f.write_all(b"test").unwrap();
198 f.flush().unwrap();
199
200 let result = verify_file_hash(f.path(), "abcdef");
201 assert!(result.is_err());
202 }
203
204 #[test]
205 fn read_hash_file_format() {
206 let mut f = NamedTempFile::new().unwrap();
207 writeln!(f, "abcd1234 sbom.json").unwrap();
208 f.flush().unwrap();
209
210 let hash = read_hash_file(f.path()).unwrap();
211 assert_eq!(hash, "abcd1234");
212 }
213}