1use base64::Engine;
9use sha2::{Digest, Sha512};
10
11pub fn verify(
14 name: &str,
15 bytes: &[u8],
16 integrity: &str,
17) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
18 let expected_b64 = integrity
19 .split_whitespace()
20 .find_map(|token| token.strip_prefix("sha512-"))
21 .ok_or_else(|| format!("package `{name}`: no sha512 integrity to verify against"))?;
22 let expected = base64::engine::general_purpose::STANDARD
26 .decode(expected_b64)
27 .map_err(|e| format!("package `{name}`: malformed sha512 integrity: {e}"))?;
28 if expected.as_slice() != Sha512::digest(bytes).as_slice() {
29 return Err(format!(
30 "package `{name}`: integrity mismatch — the downloaded tarball does not match \
31 the expected sha512"
32 )
33 .into());
34 }
35 Ok(())
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 #[test]
43 fn verify_checks_sha512_and_rejects_tampering() {
44 let bytes = b"a downloaded tarball's bytes";
45 let good = format!(
46 "sha512-{}",
47 base64::engine::general_purpose::STANDARD.encode(Sha512::digest(bytes))
48 );
49 verify("p", bytes, &good).expect("matching sha512 passes");
50
51 let mut tampered = bytes.to_vec();
52 tampered[0] ^= 0xff;
53 assert!(verify("p", &tampered, &good).is_err(), "flipped byte fails");
54
55 assert!(verify("p", bytes, "sha1-deadbeef").is_err());
57 }
58
59 #[test]
60 fn verify_rejects_malformed_base64() {
61 assert!(verify("p", b"x", "sha512-@@@@").is_err());
63 }
64
65 #[test]
66 fn verify_finds_sha512_among_algorithms_and_tolerates_whitespace() {
67 let bytes = b"multi-algorithm payload";
69 let b64 = base64::engine::general_purpose::STANDARD.encode(Sha512::digest(bytes));
70 let integrity = format!(" sha1-deadbeef sha512-{b64} ");
71 verify("p", bytes, &integrity).expect("sha512 is found among the listed algorithms");
72 }
73
74 #[test]
75 fn verify_rejects_a_short_digest() {
76 let short = base64::engine::general_purpose::STANDARD.encode(b"only nine");
79 let integrity = format!("sha512-{short}");
80 assert!(
81 verify("p", b"a downloaded tarball's bytes", &integrity).is_err(),
82 "a sub-64-byte digest cannot match"
83 );
84 }
85
86 #[test]
87 fn verify_accepts_exactly_the_pinned_bytes_and_nothing_else() {
88 for payload in [b"".as_slice(), b"x", b"a slightly longer tarball payload"] {
91 let good = format!(
92 "sha512-{}",
93 base64::engine::general_purpose::STANDARD.encode(Sha512::digest(payload))
94 );
95 verify("p", payload, &good).expect("the exact payload verifies");
96 for i in 0..payload.len() {
97 let mut tampered = payload.to_vec();
98 tampered[i] ^= 0xff;
99 assert!(
100 verify("p", &tampered, &good).is_err(),
101 "flipping byte {i} must fail"
102 );
103 }
104 }
105 }
106}