Skip to main content

npm_utils/
integrity.rs

1//! Subresource-Integrity verification of downloaded tarballs.
2//!
3//! npm pins each tarball's `sha512-<base64>` digest — in a `package-lock.json` and in the
4//! registry's `dist.integrity`. [`verify`] checks the downloaded bytes against it before they
5//! are trusted, exactly as `npm install` / `npm ci` do. An integrity string with no sha512
6//! component is an error: we never install unverified.
7
8use base64::Engine;
9use sha2::{Digest, Sha512};
10
11/// Verify `bytes` against a Subresource-Integrity string (`sha512-<base64>`, possibly several
12/// space-separated algorithms — we require and check the sha512 one). `name` is for messages.
13pub fn verify(name: &str, bytes: &[u8], integrity: &str) -> Result<(), Box<dyn std::error::Error>> {
14    let expected = integrity
15        .split_whitespace()
16        .find_map(|token| token.strip_prefix("sha512-"))
17        .ok_or_else(|| format!("package `{name}`: no sha512 integrity to verify against"))?;
18    let actual = base64::engine::general_purpose::STANDARD.encode(Sha512::digest(bytes));
19    if actual != expected {
20        return Err(format!(
21            "package `{name}`: integrity mismatch — the downloaded tarball does not match \
22             the expected sha512"
23        )
24        .into());
25    }
26    Ok(())
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    #[test]
34    fn verify_checks_sha512_and_rejects_tampering() {
35        let bytes = b"a downloaded tarball's bytes";
36        let good = format!(
37            "sha512-{}",
38            base64::engine::general_purpose::STANDARD.encode(Sha512::digest(bytes))
39        );
40        verify("p", bytes, &good).expect("matching sha512 passes");
41
42        let mut tampered = bytes.to_vec();
43        tampered[0] ^= 0xff;
44        assert!(verify("p", &tampered, &good).is_err(), "flipped byte fails");
45
46        // An integrity string with no sha512 component is rejected (npm-ci-strict).
47        assert!(verify("p", bytes, "sha1-deadbeef").is_err());
48    }
49}