Security Engineering · Supply-Chain Security

What a SHA-512 Integrity Hash in Package Metadata Actually Proves

Subresource Integrity, package lockfile digests, and Go checksums look alike but answer different questions about who computed the hash and what it covers.

An integrity hash appears in three places in a normal build: in the HTML that loads a script from a CDN, in the lockfile that pins a dependency, and in the checksum database that a language toolchain consults. They are all written as base64 or hex strings derived from a SHA-256 or SHA-512 digest, and they are frequently described in the same sentence as if they were the same control. They are not. The differences that matter are which party computed the digest, when the consumer learns the expected value, and what exactly the digest is computed over. Answering those three questions for each mechanism tells you what an attacker would have to compromise to bypass it.

Three questions to ask of any integrity check

Who computed the expected value? Either the producer of the artifact, some trusted third party, or the consumer itself at an earlier point in time. A hash computed by the same server that serves the artifact protects against corruption in transit and against an intermediary that alters the bytes. It does not protect against a compromised origin.

When does the consumer learn it, and over which channel? If the expected value arrives in the same response as the artifact, an attacker who can alter one can alter the other. Separation of channel is what makes the check meaningful, and the strength of the mechanism is bounded by the strength of that separate channel.

What is covered? A digest over an archive covers the archive bytes, not the files inside it after extraction, and not the tool that extracts it. A digest over a tarball is also sensitive to compression settings and file ordering in a way that a digest over a canonicalized file tree is not. Confusing the two is the source of a large fraction of “the checksum does not match on rebuild” reports.

Applying those questions to the three common cases gives a sharp picture.

Subresource Integrity binds a page to a CDN’s bytes

Subresource Integrity is the W3C mechanism for <script> and <link rel="stylesheet"> elements. An integrity attribute holds one or more space-separated hash expressions, each of the form alg-value, where the algorithm is one of the SHA-256, SHA-384, or SHA-512 names used in the digest() expression grammar of the Hash-based Message Authentication Code and digest family, and the value is a base64 encoding of the raw digest. The browser fetches the resource with CORS in the same-origin-bypassing way, computes the digest over the response body bytes, and refuses to execute or apply the resource if none of the listed expressions matches.

The detail that surprises people is that SRI computes over the decoded, whitespace-preserved response body exactly as received. It does not normalize line endings, it does not strip a trailing newline, and it does not decompress and recompress. A CDN that re-encodes a file with a different content encoding is fine, because the digest is defined over the decoded body; a CDN that appends a source-map comment, applies a formatter, or serves a slightly different minification is not. This is why the hash must be regenerated when the asset pipeline changes, and why a build that hashes the pre-minification file and serves the post-minification file produces a console error rather than a working page.

The attribute grammar has a subtle rule: if any token in the list uses an algorithm the browser does not recognize, that token is ignored, but if the list contains only unrecognized algorithms the resource must fail closed. This allows a page to list a strong hash alongside a future algorithm without breaking older browsers. There is no fallback to “no integrity” on mismatch, which makes SRI strictly fail-closed once it is present.

What it protects: an attacker who can modify the CDN’s copy of the file but not the HTML that references it. What it does not protect: anything about the page that references it, since whoever can change the integrity attribute can change it to match a malicious file. Its security is exactly the security of the HTML delivery path.

Lockfile digests bind a client to a specific archive

Package managers record a digest per artifact, but they differ in what happens when it is absent. In npm’s lockfile the integrity field uses the SRI string format and is computed by the registry over the tarball it serves; the client verifies on download. In older lockfile formats the field did not exist, and installs from those lockfiles perform no verification beyond transport security. Cargo records a checksum per crate in its lockfile and verifies against the registry index.

Go is the outlier and the most instructive case. The go.sum file records module versions with two hashes: one over the module zip archive and one over the go.mod file alone, in the form h1: followed by a base64 SHA-256 digest of a canonicalized hash input. The go.sum is populated on first download by the client, which makes it a trust-on-first-use record in the narrow sense, and the ecosystem closes that gap with the checksum database: a public, append-only, transparent log of module hashes, consulted through the GOSUMDB mechanism with a default database maintained by the Go team, where the client verifies that the log has been consistent and that the recorded hash matches the downloaded content. The interesting property is that the database is not trusted blindly: its responses are signed, and clients can be configured to use an alternate database or disable it entirely with GONOSUMDB and related settings, which is a decision with an obvious security cost.

The Go design highlights a general truth: a per-artifact hash in a lockfile proves only that the artifact is the same one the lockfile author saw. It says nothing about whether that author was entitled to choose it, or whether the artifact was what the maintainer intended to publish. The trust-on-first-use window is precisely the attack surface, and the checksum database is an attempt to replace it with a global append-only commitment.

Why hashes leak information and why that is fine

Digests found in public lockfiles and HTML are not secrets. A SHA-256 over a public file is recomputable by anyone, so publishing it costs nothing. The one case where a digest is sensitive is when the input is low-entropy and secret, such as a password or a short token, and that is the case where a plain hash is the wrong construction entirely: a password needs a salted, memory-hard key derivation function rather than a fast digest. Integrity hashes over artifacts always take high-entropy or public input, so the fast-hash choice is appropriate and its collision resistance is the property being relied upon.

# Go's two records: archive zip hash and go.mod hash, both SHA-256 h1.
# Recompute the same value a client checks after download.
go mod download -json example.com/mod@v1.2.3 | grep -E 'Sum|GoModSum'
sha256sum "$(go env GOMODCACHE)/cache/download/example.com/mod/@v/v1.2.3.zip"

Two distinct hashes appear for a single module because the go.mod file participates in module graph construction before the archive is downloaded. Hashing the go.mod separately lets the client reject a graph that would have been computed from a different go.mod than the one later shipped in the archive. The general pattern, a digest per signed unit of meaning rather than per file on disk, is the same pattern that appears in every attestation format discussed in provenance attestation and digest binding.

Failure modes that make a correct hash useless

Hashing the wrong representation. A pipeline that hashes a directory tree by walking it in filesystem order produces a digest that changes when the filesystem returns directory entries in a different order. Any tree hash must sort paths and specify a separator and normalization rules, or it is not a digest of the content but of the traversal.

Verifying after extraction. If a tool verifies the tarball digest after unpacking, or worse, unpacks first and verifies a manifest inside, it has already written attacker-controlled paths to disk. Tar archives can contain absolute paths and .. components, so extraction must reject them and verification must precede it.

Algorithm confusion. An integrity attribute or signature blob that names its own algorithm invites an attacker to name a weak one. SRI’s list-of-expressions design sidesteps this by letting the page author fix the acceptable set; systems that parse the algorithm out of the untrusted blob must maintain an allow-list, and must reject anything outside it even if the digest verifies.

Trusting the index that supplies the hash. In ecosystems where the registry serves both the artifact and the digest, one compromise yields both. This is structurally weaker than a separate log, and it means that the useful control in that ecosystem is whether the client is configured to consult a second source at all.

The boundary between what a hash can prove and what requires a signature is where the next mechanism starts. A hash answers “are these the bytes that were expected”; it cannot answer “who decided what to expect”, which is the question that keyed signatures and transparency logs address, and it cannot answer “is this the newest expected value”, which is what the version and timestamp roles in threshold roles and rollback protection in the Update Framework exist to establish.

References

  1. Subresource Integrity — W3C Recommendation
  2. go mod verify and checksum database — Go project documentation
  3. FIPS 180-4: Secure Hash Standard — NIST