Verify downloads with checksums on Linux
Hash a file with sha256sum and verify it against a SHA256SUMS file. Then flip one byte and watch the check fail, so you learn what a checksum really proves.
Verify a download with a checksum in two minutes
To verify a download with a checksum, hash the file you received and let a tool compare that hash against the one the publisher printed. sha256sum does both halves of the job: on its own it prints a digest, and with -c it reads a list of digests and reports which files match. This guide runs the whole loop on a file you create, then breaks that file on purpose so you watch the failure happen instead of reading about it.
Keep one sentence in mind the whole way through. A checksum tells you whether the bytes you hold are the bytes that produced the digest, and it tells you nothing about who produced them. That second question needs a signature and a key you trust. The last part of this guide shows exactly where the line between the two sits.
Make a file to practise on
Work in a scratch directory so nothing here touches the rest of the system. Every command below comes from GNU coreutils, the base command set present on any Ubuntu or Debian server, so there is nothing to install.
mkdir -p ~/checksum-demo
cd ~/checksum-demo
printf 'the payload of a totally ordinary download\n' > payload.txt
sha256sum payload.txtYou get one line: 64 hexadecimal characters, two spaces, then the file name. Those 64 characters are the file's digest. Run the command again and the line is identical, because hashing is deterministic: the same input always gives the same output. Change one character of the file and run it again, and the digest does not shift a little. It looks completely different, because flipping one input bit flips about half of the output bits. That property is what makes a 64-character string a usable stand-in for a 4 GB image.
Save a SHA256SUMS file, then check it
A digest on screen is useless a day later. Write it to a file, in the format sha256sum itself writes, so the tool can read it back later.
sha256sum payload.txt > SHA256SUMS
cat SHA256SUMS
sha256sum -c SHA256SUMSsha256sum -c reads each line of the list, hashes the file named on that line, and compares the two digests. A healthy run prints one line per file:
payload.txt: OKCheck the exit status as well, because a script reads that and never reads the text. echo $? prints 0 after a clean run. The name SHA256SUMS is a convention rather than a rule, but distributions and most release pages use it, so use it too and the next person knows what the file holds without opening it.
Flip one byte and watch the check fail
Now break the file on purpose. This writes a single byte at offset 5 and leaves everything else alone, so the file keeps its length and its name.
printf 'X' | dd of=payload.txt bs=1 seek=5 conv=notrunc status=none
cat payload.txt
sha256sum -c SHA256SUMSconv=notrunc is the flag that matters: without it, dd truncates the file at the point it stops writing, and you would be testing a much more obvious kind of damage. The check now prints:
payload.txt: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchecho $? prints 1. FAILED means the file was read and its digest did not match the one in the list. Put the original bytes back and confirm the check returns to OK:
printf 'the payload of a totally ordinary download\n' > payload.txt
sha256sum -c SHA256SUMSThat is the entire habit. One byte of difference, anywhere in the file, produces FAILED. A download cut short by a dropped connection, a mirror serving yesterday's build, a proxy that rewrote the file in transit, a disk that returned a bad block: all of them land on that same line.
When the list names a file you did not download
A real SHA256SUMS file from a distribution lists every image the project ships, and you downloaded one of them. Reproduce that situation here.
printf 'a second file\n' > notes.txt
sha256sum payload.txt notes.txt > SHA256SUMS.all
rm notes.txt
sha256sum -c SHA256SUMS.allpayload.txt: OK
sha256sum: notes.txt: No such file or directory
notes.txt: FAILED open or read
sha256sum: WARNING: 1 listed file could not be readFAILED open or read is a different failure from FAILED, and mixing the two up wastes time. FAILED means the bytes are wrong. FAILED open or read means sha256sum never got the file at all, so nothing was compared. On a real download the usual cause is the working directory, because the names in the list are relative to where you run the command. Change into the directory holding the file and run it again. To check only what you actually have, ask for that:
sha256sum --ignore-missing -c SHA256SUMS.allThat prints payload.txt: OK and exits 0. If none of the listed names are present, --ignore-missing does not quietly succeed on zero files. It reports that no file was verified and exits non-zero, which is the behaviour you want, because a pass that checked nothing is the failure you would never notice.
Paste a published digest without reading it by eye
Comparing 64 hexadecimal characters by eye is where this habit really breaks. People check the first four characters and the last four and call it a match, and that is exactly the comparison a determined attacker plans for. Let the tool compare instead. Set EXPECTED to the digest you copied from the publisher, using EXPECTED= followed by the pasted value, then build the single line that -c expects:
printf '%s %s\n' "$EXPECTED" payload.txt > payload.sha256
cat payload.sha256
sha256sum -c payload.sha256Two spaces sit between the digest and the file name, which is why the format string carries two. That is the shape sha256sum writes and the shape -c parses. A file holding a digest and nothing else is not a checksum line at all, so the check rejects the whole file with no properly formatted checksum lines found rather than guessing which file you meant. Some projects publish the BSD tagged style instead, SHA256 (payload.txt) = followed by the digest. GNU coreutils writes that form with sha256sum --tag payload.txt and reads it back with -c, so either shape is fine to save.
When a check behaves oddly, look at the list itself with cat -A SHA256SUMS, which marks the end of every line with $ and shows characters you cannot otherwise see. A line ending in ^M$ picked up a carriage return from a Windows editor. GNU sha256sum ignores that trailing character and still prints OK, so a CRLF list is not the thing breaking your check, though tools outside coreutils are less forgiving about it. Normalise the copy you keep with tr -d '\r' < SHA256SUMS > SHA256SUMS.clean.
What does a checksum prove, and what does it not?
A checksum proves one thing: the bytes on your disk are the bytes that produced the published digest. That covers accidental damage completely. It also covers a careless attacker who swapped the file on a download mirror but could not touch the page that published the digest.
It proves nothing about authorship. A digest is a fact about bytes, not a fact about people. If one page serves both the file and the digest, then whoever can change one can change the other, and your OK line means only that the mirror agrees with itself. So here is the rule that makes checksums worth running: take the digest from somewhere other than where you took the file. The project's own domain over TLS (transport layer security) while the image came from a mirror or a torrent, for example. Now an attacker has to control two places instead of one.
The algorithm matters too. SHA-256 (secure hash algorithm, 256-bit output) has no known collision as of August 2026, which is why publishers use it. MD5 (message digest 5) and SHA-1 do not hold up: two different files with the same MD5 digest have been constructible since 2004, and a chosen-prefix SHA-1 collision was published in 2020. An MD5SUMS file still catches a truncated download, because random corruption is not a crafted collision. It cannot stop somebody who is trying to fool you. When a project publishes both, take the SHA-256 line.
Where signatures take over
A signature closes the gap a digest leaves open. The publisher signs the digest file with a private key, and you check it with their public key: gpg --verify SHA256SUMS.asc SHA256SUMS. If that passes, the list of digests came from whoever holds that key. Then sha256sum -c SHA256SUMS ties the file on your disk to the list, and the chain runs from the key all the way down to the bytes.
The weak point moves to the key. Fetching the key from the same page that served the file hands both halves back to the attacker. GnuPG is honest about this, and a first verification prints Good signature together with WARNING: This key is not certified with a trusted signature!. Good signature means the mathematics holds. It does not mean the key belongs to the project you have in mind. Get the fingerprint from a second source, such as the project's documentation on a different domain or a distribution package that already ships the key, and compare the full fingerprint instead of the last eight characters. This is the same care an SSH private key deserves, for the same reason: the key is the trust decision, and everything downstream inherits it.
Reproducible builds push the idea one step further. A published digest still ties you to a binary that one machine built. When a project's build is reproducible, anyone can compile the same source and get byte-identical output, so independent builders can confirm the published digest rather than asking you to take one server's word for it. That matters more each year, as more code arrives through automated pipelines and machine-written patches. Deciding what you accept into a build is a policy question, and open source policies for AI-assisted code work on the same supply chain from the other end.
Your package manager already does this for you
On Debian and Ubuntu, apt runs this chain on every install without being asked. The package index carries a SHA-256 digest for each .deb file. The Release file carries the digests of those index files, and InRelease carries a signature over Release, checked against the keys in /usr/share/keyrings and /etc/apt/trusted.gpg.d. When the chain breaks, apt says so: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY when a third-party repository's key is missing, or Hash Sum mismatch when the index you fetched does not match the signed Release, which usually means a caching proxy served a stale file or you caught a mirror mid-sync.
That is the standard to measure against when a project's home page tells you to pipe a script from curl straight into a shell. Nothing verifies the bytes and you never see them. The server can also return one thing to a script and something else to a browser, and you have no copy to inspect afterwards. Download to a file with curl -fsSL <url> -o install.sh, hash it, read it with less, and run it only then. The habit costs about twenty seconds, and it is the same one worth starting on a brand new VPS in its first ten minutes, before anything else is installed on the box.
Keep a list of digests for what you install by hand
Packages installed by apt are tracked. A binary you copied into /usr/local/bin is not, and nothing on the system is watching it. A digest list turns that into something you can check on demand:
printf 'a second file\n' > notes.txt
sha256sum *.txt > inventory.sha256
sha256sum -c --quiet inventory.sha256--quiet prints nothing when every file matches, and prints only the lines that failed when some do not, so silence is the pass and echo $? confirms it with 0. That is the form to put in a scheduled job. --status goes further and prints nothing at all, leaving you only the exit status. Point the same pattern at real files with sha256sum /usr/local/bin/* > ~/local-bin.sha256 and you have a baseline. Paths are stored in the list exactly as you typed them, so absolute paths make the check work from any directory.
Be clear about what that baseline is worth. It detects a changed file. It does not detect an attacker who already has root, because that attacker can rewrite inventory.sha256 as easily as they rewrote the binary. Keep the list off the machine if you want it to mean anything, which is part of the wider question of how much of a VPS you are actually trusting and who else can reach the disk underneath it.
FAQ
Does a matching checksum mean the download is safe?
No. It means the bytes you have match the digest you compared them against. If the attacker controls the page that published the digest, they publish the digest of their own file and your check prints OK. A match is a consistency claim. A safety claim needs a signature verified against a key you obtained from a different source, and only then does the digest inherit that trust.
Why does sha256sum -c print FAILED open or read?
Because it never read the file. A separate line just above it says No such file or directory with the name it looked for. The names inside a SHA256SUMS file are relative to the directory you run the command in, so change into the directory holding the download and run it again. If the list also names files you did not download, add --ignore-missing. A plain FAILED with no open or read is the opposite situation: the file was read, and its digest did not match.
Is MD5 good enough for verifying a download?
For accidental damage, yes. A truncated transfer or a bad disk block will not produce a matching MD5 digest by chance. Against an attacker, no. Two different files with the same MD5 digest have been constructible since 2004, and SHA-1 fell to a chosen-prefix collision in 2020. Take the SHA-256 line when a project publishes both, and read an MD5-only project as a sign of an old release process.
What is the difference between sha256sum -c and gpg --verify?
sha256sum -c proves a file matches a digest. gpg --verify proves a digest file was signed by the holder of a particular private key. They answer different questions, so run both when a project offers both. The signature makes the digest list trustworthy, and the digest list then makes the downloaded file trustworthy.
How do I check one file against a digest printed on a web page?
Do not compare the characters by eye. Save the digest and the file name on a single line, separated by two spaces, then run sha256sum -c against that file and read the OK or FAILED it prints. Building the line with printf '%s %s\n' avoids the formatting mistakes that make sha256sum reject the file with no properly formatted checksum lines found.