🍱 Lunchbox Hands

security

How to Verify a SHA-256 Checksum (and What It Proves)

How to verify a SHA-256 checksum on Windows, macOS and Linux, why a matching hash proves less than you think, and what to check when it does not match.

You download a 5 GB ISO, the page says SHA256: 9bc6..., you run the command, the strings match, and you feel safe. A matching checksum proves exactly one thing: the bytes on your disk are the bytes that produced that digest. It says nothing about who produced the digest. If the hash was printed on the same page that served the download, an attacker who can change one can change the other — and has. Here are the commands for every OS, then the part most guides skip.

The commands, per OS

OS / shellCommand
Windows (PowerShell)Get-FileHash .\ubuntu.iso
Windows (PowerShell, other algorithm)Get-FileHash .\ubuntu.iso -Algorithm MD5
Windows (cmd, no PowerShell)certutil -hashfile ubuntu.iso SHA256
macOSshasum -a 256 ubuntu.iso
Linuxsha256sum ubuntu.iso
Anything with OpenSSLopenssl dgst -sha256 ubuntu.iso

Four details that trip people up:

  • Get-FileHash defaults to SHA-256. You only pass -Algorithm when you want something else. It prints uppercase hex in a table; the comparison you actually want is (Get-FileHash .\ubuntu.iso).Hash -eq '9bc6...', which returns True or False. PowerShell’s -eq is case-insensitive on strings, so uppercase output versus a lowercase digest on the download page is not a problem. (-ceq is the case-sensitive one, and you don’t want it here.)
  • certutil output is chattier than it looks. It prints a header line, the digest, and a CertUtil: -hashfile command completed successfully. footer. On Windows 7 and 8 it also printed the digest as space-separated byte pairs; Windows 10 and 11 print one continuous string. Strip spaces before comparing if you’re scripting against it.
  • macOS has shasum, and recent versions also have sha256sum. macOS 26 ships an Apple-built /sbin/sha256sum, but it’s a slim reimplementation — sha256sum --help there prints usage: sha256sum [-bctwz], with none of GNU coreutils’ long options. shasum -a 256 has been on every Mac for years and is the portable choice.
  • openssl dgst labels its own output. Modern OpenSSL prints SHA2-256(ubuntu.iso)= 9bc6..., not SHA256(...). Same digest, different label — don’t let the prefix convince you that you hashed the wrong thing.

If you’d rather not think about any of this, drop the file on the File Checksum Verifier: it hashes locally in your browser and tells you match or mismatch in a colored banner.

Verifying against a .sha256 or SHA256SUMS file

Serious projects don’t publish a single digest on a page — they publish a checksum file listing every artifact, which is what you should be checking against.

sha256sum -c SHA256SUMS
# ubuntu-24.04.1-desktop-amd64.iso: OK

On a mismatch you get both a per-file line and a summary, and a non-zero exit code:

ubuntu-24.04.1-desktop-amd64.iso: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

GNU coreutils’ --ignore-missing is essential here, because SHA256SUMS usually lists a dozen images and you downloaded one:

sha256sum -c --ignore-missing SHA256SUMS

macOS equivalent, using the checksum file the publisher shipped:

shasum -a 256 -c ubuntu.iso.sha256

The file format is worth recognizing on sight, because two dialects are in circulation. GNU tools emit <digest> <filename> — two spaces, or a space and an asterisk for binary mode. BSD tools emit SHA256 (ubuntu.iso) = 9bc6..., and OpenSSL a close cousin of it (SHA2-256(ubuntu.iso)= 9bc6...). All are valid; a parser that only knows one of them will reject the others.

The trust ladder

This is the part that matters, and it’s a ladder, not a yes/no.

What you verifiedWhat it provesWhat it does not
Hash printed on the same page as the downloadYour copy is byte-identical to what that page describedNothing about whether the page is honest or intact
Hash from a different host — distro mirror index, package manager, release notes on a separate domainThe download host alone can’t lie to you; two systems must be compromisedStill nothing about the publisher’s intent
Detached signature (GPG, minisign, signify, Sigstore) checked against a key you obtained independentlyThe artifact was signed by the holder of that keyThat the key holder is trustworthy

Rung one is integrity only. It catches a truncated download, a flaky mirror, a corrupted USB write — which is genuinely most of what goes wrong, and why checksums are worth running. It does not catch tampering, because a compromised web server serves the hash and the file from the same place.

That is not hypothetical. In February 2016 the Linux Mint website was compromised: the download links were pointed at a backdoored 17.3 Cinnamon ISO carrying an IRC-controlled bot, and the checksums displayed on the site were changed to match. Every user who dutifully compared the hash to the page got a green light.

Rung three is the fix, and it’s a two-step dance. Ubuntu, for example, publishes SHA256SUMS alongside SHA256SUMS.gpg:

gpg --verify SHA256SUMS.gpg SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS

The signature authenticates the list; the hashes then authenticate the files. Both steps are load-bearing, and the whole thing still rests on having the right public key — a key you fetched from the same compromised page proves nothing. This is the same structural idea as a keyed HMAC: an unkeyed hash can be recomputed by anyone, including whoever swapped the file, so authenticity always requires a secret or a key you already trust.

And the ceiling is real: in 2024 the xz-utils backdoor shipped inside release tarballs signed by the project’s own maintainer. Signatures prove provenance, not good intentions.

”The checksum doesn’t match” — work down this list

In roughly descending order of likelihood:

  1. Partial or resumed download. Check the file size against the published size first; it’s a one-second test that explains most mismatches.
  2. Wrong file. amd64 vs arm64, 24.04 vs 24.04.1, the torrent vs the direct download. The digest is for one exact artifact.
  3. Different algorithm. You’re comparing an MD5 to a SHA-256. Count hex characters: 32 = MD5, 40 = SHA-1, 64 = SHA-256, 128 = SHA-512. If the lengths differ, you never had a chance.
  4. Copy-paste damage. A trailing space, a line-wrapped digest that picked up a newline mid-string, or an invisible character from a docs site’s styled code block. Digests are case-insensitive hex; they are not whitespace-tolerant in a naive ==.
  5. Line-ending conversion. If a text file or script passed through anything that rewrote LF to CRLF — a text-mode transfer, an editor, core.autocrlf on Windows — the bytes changed, so the hash changed. (This is not the -b/-t flag: on POSIX systems binary and text mode compute the same digest.)
  6. You hashed the wrong thing. The .zip instead of the extracted binary, the .sha256 sidecar instead of the payload, or a symlink.
  7. The file really is different. Re-download from another mirror. If it mismatches again, stop and go look for a signature.

Before you trust the file once it does match, it’s also worth knowing that a checksum says nothing about a file’s type — see magic bytes and file signatures for how software actually decides what a file is.

MD5 and SHA-1: fine for corruption, useless against tampering

The distinction is between two attacks. A collision means an attacker crafts two files with the same digest — practical for MD5 since 2004 and demonstrated for SHA-1 by Google and CWI’s SHAttered in 2017. A second preimage means finding a different file matching a digest someone else already published; that remains infeasible even for MD5.

So if a publisher gave you an MD5 years ago and you’re checking a download against it, a random attacker can’t easily forge a match. But a malicious or compromised publisher can prepare a benign file and an evil file with the same MD5 in advance, get the benign one blessed, and ship the other — which is roughly how the Flame malware forged a Microsoft code-signing certificate in 2012 with a chosen-prefix MD5 collision.

The rule: MD5 and SHA-1 detect accidents, not adversaries. CRC32 detects only transmission errors and isn’t a cryptographic hash at all. Use SHA-256 or BLAKE3 when you get to choose, and when you don’t, use whatever the publisher used — but know which rung of the ladder you’re standing on. (For hashing short strings rather than files, the Hash Generator covers MD5 through SHA-512; when a shared secret is involved, you want the HMAC Generator instead.)

How a browser hashes a 4 GB file without uploading it

The obvious objection to a web-based checker is that it must be uploading your file. It isn’t, and the mechanism is worth knowing because it also explains why the built-in browser crypto API can’t do this job.

crypto.subtle.digest() — the Web Crypto API — takes a complete buffer. To hash a 4 GB ISO with it you’d have to materialize 4 GB in memory first. So our verifier uses hash-wasm, a WebAssembly implementation with an incremental interface: init(), update(chunk), update(chunk), …, digest(). The file is read through Blob.slice() in 8 MiB slices, each slice is fed to the hasher and discarded, and memory stays flat no matter how big the file is. The hasher reports progress after every slice, which is what feeds the progress bar and what gives Cancel a place to take effect.

Nothing leaves your machine — not the bytes, not the filename. The only request the tool itself makes is a one-time fetch of the lazy-loaded hashing chunk (hash-wasm ships its WebAssembly base64-inlined into JavaScript, so there is no separate .wasm to download). Watch the Network tab while you hash: no upload, whatever the file size.

What to do

  • Always verify, even from a page you trust — it catches the corrupt-download case, which is the common one.
  • Get the hash from somewhere other than the download page when you can: the project’s signed release notes, a package index, a mirror on a different domain.
  • For anything with real consequences, verify a signature, not just a hash, and get the key from a channel you already trust.
  • Compare by tooling, not by eye. Humans checking 64 hex characters read the first four, the last four, and nothing in between.
  • Drop the file and the expected value into the File Checksum Verifier. It handles CRC32, MD5, SHA-1, SHA-256, SHA-512, SHA3-256 and BLAKE3; guesses the algorithm from the digest length and offers to enable it; accepts a bare hex digest in any case (with spaces, colons or dashes between bytes), a base64 digest, a pasted sha256sum or SHA256 (file) = ... line, or an entire SHA256SUMS file matched by filename. Drop the .sha256 sidecar onto the dropzone and it fills the expected box for you — then drop the download and read the banner.