DEV Community

Cover image for How to Verify a Download: SHA-256, GPG and Sigstore
Muhammad Ali Mughal
Muhammad Ali Mughal

Posted on

How to Verify a Download: SHA-256, GPG and Sigstore

A matching SHA-256 checksum proves one thing: the file you have is the file the checksum describes. It does not prove the file is safe, or that it came from the project you think it did. If an attacker can replace the installer on a download mirror, they can usually replace the SHA256SUMS file sitting next to it too, and your sha256sum -c will print a reassuring OK. The checksum is only as trustworthy as the channel you got it from, which is why serious projects sign their checksum files and why package registries now publish provenance.

I build PixelValut, a set of browser-side developer and security tools, and verifying downloads is a step I script into every server build and CI pipeline I own. Below is the workflow with real command output from a Node.js release, what each step defends against, and where most teams stop one step too early.

How do you verify a downloaded file is authentic?

Direct answer: Compute the file's SHA-256 hash and compare it with the hash the publisher lists, then verify the publisher's signature on that checksum list with GPG, Sigstore or the platform's own signing, using a key fingerprint you obtained independently. A hash alone detects corruption; only a verified signature ties the file to the publisher.

What each check actually protects against

Threat scenario What the attacker controls SHA-256 from same server Signed checksums (GPG/Sigstore) Pinned hash in a lockfile
Corrupted or truncated download Nothing, just a bad network Detects it Detects it Detects it
Compromised mirror or CDN node The file and the checksum next to it Misses it Detects it, the signature fails Detects it, if the pin predates the attack
Look-alike download site Everything on that domain Misses it Detects it, if you check the key fingerprint Not applicable
Malicious release by a stolen maintainer key The signing key itself Misses it Misses it Misses it on first install
Dependency swapped in a registry Package tarball Misses it Detects it with registry signatures Detects it, the integrity hash differs

Two lessons come out of this table. A checksum served from the same place as the file only catches accidents. And no verification step protects you from a legitimate key used maliciously, which is why provenance and short-lived signing identities exist.

Step 1: Compute and compare the hash

I use the Node.js v22.11.0 Linux binary as the example because Node publishes both a checksum list and its signature. Download the file and SHASUMS256.txt, then let sha256sum do the comparison instead of eyeballing 64 hex characters:

$ curl -sO https://nodejs.org/dist/v22.11.0/node-v22.11.0-linux-x64.tar.xz
$ curl -sO https://nodejs.org/dist/v22.11.0/SHASUMS256.txt
$ grep linux-x64.tar.xz SHASUMS256.txt
83bf07dd343002a26211cf1fcd46a9d9534219aad42ee02847816940bf610a72  node-v22.11.0-linux-x64.tar.xz
$ sha256sum node-v22.11.0-linux-x64.tar.xz
83bf07dd343002a26211cf1fcd46a9d9534219aad42ee02847816940bf610a72  node-v22.11.0-linux-x64.tar.xz
$ grep node-v22.11.0-linux-x64.tar.xz SHASUMS256.txt | sha256sum -c -
node-v22.11.0-linux-x64.tar.xz: OK
Enter fullscreen mode Exit fullscreen mode

On macOS the equivalent is shasum -a 256 file, and on Windows certutil -hashfile file SHA256 or PowerShell's Get-FileHash file -Algorithm SHA256.

If you are on a machine without a terminal, the SHA-256 file checksum tool hashes a dropped file in the browser, shows progress on large files and compares the result with the published value. For short strings or smaller files, the hash generator computes MD5, SHA-1, SHA-256 and SHA-512 locally without uploading anything. If someone hands you a hash with no label, the hash identifier ranks the likely algorithm by length and character set.

Step 2: Verify the signature on the checksum list

Node signs SHASUMS256.txt with a release key. On a clean machine, GPG first reports that it does not know the key:

$ curl -sO https://nodejs.org/dist/v22.11.0/SHASUMS256.txt.sig
$ gpg --verify SHASUMS256.txt.sig SHASUMS256.txt
gpg:                using RSA key C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C
gpg: Can't check signature: No public key
Enter fullscreen mode Exit fullscreen mode

Import that key from a source that is independent of the download server. For Node, that is the nodejs/release-keys repository on GitHub, and the fingerprints are also listed in the project's README (output trimmed, signer name redacted):

$ curl -sL https://raw.githubusercontent.com/nodejs/release-keys/main/keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc | gpg --import
gpg: key C43CEC45C17AB93C: public key imported
$ gpg --verify SHASUMS256.txt.sig SHASUMS256.txt
gpg: Good signature from "<Node.js releaser>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: C82F A3AE 1CBE DC6B E46B  9360 C43C EC45 C17A B93C
Enter fullscreen mode Exit fullscreen mode

"Good signature" is the part that matters. The warning only means you have not personally certified the key in GPG's web of trust; it goes away if you sign the key locally after checking the fingerprint. What you must do is compare that fingerprint against the independently published list. If you skip that comparison, you have only proved the file was signed by someone.

An antique brass key resting on a folded letter, representing the publisher's signing key

Step 3: Prove the checks fail when they should

A verification step you have never seen fail is a step you have not tested. I append one byte to each file and rerun:

$ printf 'x' >> node-v22.11.0-linux-x64.tar.xz
$ grep node-v22.11.0-linux-x64.tar.xz SHASUMS256.txt | sha256sum -c -
node-v22.11.0-linux-x64.tar.xz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

$ echo '#' >> SHASUMS256.txt
$ gpg --verify SHASUMS256.txt.sig SHASUMS256.txt
gpg: BAD signature from "<Node.js releaser>" [unknown]
Enter fullscreen mode Exit fullscreen mode

Both commands exit non-zero, which is what lets a Dockerfile or CI job stop the build instead of shipping the file. Always rely on exit codes in scripts, never on someone reading the output.

Step 4: Dependencies are downloads too

Most of what reaches production arrives through a package manager, and the same two layers exist there.

Integrity hashes in lockfiles. npm records a SHA-512 integrity value for every package in package-lock.json, and the install fails if the tarball does not match:

"node_modules/express": {
  "version": "4.21.1",
  "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
  "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ=="
}
Enter fullscreen mode Exit fullscreen mode

That pin protects you on every install after the first. It cannot tell you whether the first version you locked was legitimate.

Registry signatures and provenance. npm audit signatures checks the registry's signature on every installed package and any Sigstore-backed provenance attestations. On a fresh project with Express installed:

$ npm audit signatures
audited 69 packages in 0s

69 packages have verified registry signatures

1 package has a verified attestation
Enter fullscreen mode Exit fullscreen mode

A provenance attestation links a package version to the source repository and CI workflow that built it, signed with a short-lived Sigstore certificate rather than a long-lived personal key. GitHub offers the same model for release artifacts through gh attestation verify <file> --repo <owner>/<repo>. For container images, pull by digest (image@sha256:...) rather than by mutable tag, so a re-pushed tag cannot change what you deploy.

I covered what malicious packages actually look like once they are installed in deobfuscating malicious npm and browser payloads.

Threat scenarios worth rehearsing

  • The look-alike download page. A search ad leads to a domain one character off the real one, with a matching-looking checksum on the page. The hash matches because the attacker published both. The defense is the signature plus a fingerprint from the real project, and checking the domain itself, which I walk through in how to inspect a suspicious link.
  • The compromised mirror. Same story, legitimate domain. Signed checksums catch it; same-server hashes do not.
  • The "curl | bash" installer. No checksum at all. Download the script, read it, pin its hash in your provisioning code and verify before executing.
  • The MD5-only project. MD5 still detects accidental corruption, but collisions are practical, so it does not stand up to a deliberate attacker. Treat it as an integrity hint, not authentication.

Download verification checklist

  1. Download from the project's canonical domain, typed or bookmarked, not from an ad.
  2. Fetch the checksum list and its signature, not just the file.
  3. Get the signing key or identity from an independent channel and record its full fingerprint.
  4. Run gpg --verify (or the project's Sigstore or attestation command) and require "Good signature".
  5. Run sha256sum -c against the signed list, never a hand comparison.
  6. Script both checks so a failure exits non-zero and stops the build.
  7. Pin verified hashes in Dockerfiles and provisioning scripts.
  8. Commit lockfiles and fail CI on integrity mismatches.
  9. Run npm audit signatures (or your ecosystem's equivalent) in CI.
  10. Reject MD5 and SHA-1 as the only check for anything security-relevant.

Frequently asked questions

Is a SHA-256 checksum enough to trust a download?

Only against accidental corruption, or when the checksum came from a channel the attacker cannot control. If the checksum sits on the same server as the file, an attacker who replaces one can replace both. Verify a signature over the checksum list for real authenticity.

What does "This key is not certified with a trusted signature" mean?

It means you have not personally certified the key in GPG's web of trust. The signature itself is still valid. Confirm the fingerprint against the project's independently published list; if it matches, the warning is expected and safe to accept.

Is HTTPS not enough to protect a download?

HTTPS protects the connection between you and the server. It does nothing if the server, mirror or CDN node is serving a malicious file, or if you are on a look-alike domain with a valid certificate. Signatures protect the file itself, wherever it was hosted.

Is MD5 or SHA-1 still acceptable for checksums?

For detecting accidental corruption, they work. For security they are broken: practical collision attacks exist for both. Use SHA-256 or SHA-512, and prefer projects that sign their checksum lists.

What is Sigstore, and how is it different from GPG?

Sigstore signs artifacts with short-lived certificates tied to an identity, such as a CI workflow, and records each signature in a public transparency log. There is no long-lived private key to steal or a key server to trust. GPG relies on long-lived keys whose fingerprints you verify yourself.

How do I verify a file on Windows without extra tools?

Run Get-FileHash .\file.zip -Algorithm SHA256 in PowerShell, or certutil -hashfile file.zip SHA256 in Command Prompt, then compare with the signed checksum list. Gpg4win provides the gpg --verify step on Windows.


Follow & Connect

Top comments (0)