A signed release comes as two files: the download itself and a small signature
next to it, usually .sig or .asc. A third thing has to come from somewhere
else: the project's public key, and a fingerprint you can trust. With all three,
one command tells you whether the file is byte for byte what the key's owner
signed.
If you can install GnuPG, use it. On Debian or Ubuntu it is
sudo apt install gnupg, on a Mac brew install gnupg, and on Windows it comes
with Gpg4win.
gpg --import signature_key.asc
gpg --verify gnupg-2.5.24.tar.bz2.sig gnupg-2.5.24.tar.bz2
Signature first, file second. Then compare the fingerprint it prints with the one the project publishes. That second step is the one that counts.
What you need, and where it comes from
| Thing | Example | Where it comes from |
|---|---|---|
| The file | gnupg-2.5.24.tar.bz2 |
The download page or any mirror |
| The signature | gnupg-2.5.24.tar.bz2.sig |
Next to the file |
| The public key | signature_key.asc |
The project's own site, its source repository, or a keyserver |
| The fingerprint | 6DAA 6E64 A76D 2840 … |
Somewhere you trust that a mirror can't change |
The signature can travel with the file because it is useless without the key. The key is where all the trust sits, so get it from the project itself, not from the mirror that served the download.
Verifying a real release: GnuPG 2.5.24
GnuPG signs its own tarballs. These are the files from
https://gnupg.org/ftp/gcrypt/gnupg/, and the key file from
https://gnupg.org/signature_key.asc, checked with GnuPG 2.4.7 on Debian 13.
curl -O https://gnupg.org/ftp/gcrypt/gnupg/gnupg-2.5.24.tar.bz2
curl -O https://gnupg.org/ftp/gcrypt/gnupg/gnupg-2.5.24.tar.bz2.sig
curl -O https://gnupg.org/signature_key.asc
Before you have the key
Run the check first, and gpg tells you which key it needs:
gpg --verify gnupg-2.5.24.tar.bz2.sig gnupg-2.5.24.tar.bz2
gpg: Signature made Wed Sep 23 11:40:27 2026 UTC
gpg: using EDDSA key 6DAA6E64A76D2840571B4902528897B826403ADA
gpg: Can't check signature: No public key
gpg: Signature made Thu Sep 24 00:40:06 2026 UTC
gpg: using EDDSA key AC8E115BF73E2D8D47FA9908E98E9B2D19C6C8BD
gpg: Can't check signature: No public key
This release carries two signatures, from two maintainers. "No public key" is not a failure: nothing was checked, so it says nothing either way. The exit code is 2.
Import the key
gpg --import signature_key.asc
gpg: key 528897B826403ADA: public key "Werner Koch (dist signing 2020)" imported
gpg: key E98E9B2D19C6C8BD: public key "Niibe Yutaka (GnuPG Release Key)" imported
…
gpg: Total number processed: 6
gpg: imported: 6
The file holds six keys, one per person or role that signs GnuPG releases. Four lines are left out above. Importing a key doesn't mean trusting it: anyone can make a key with any name on it.
Verify again
gpg --verify gnupg-2.5.24.tar.bz2.sig gnupg-2.5.24.tar.bz2
gpg: Signature made Wed Sep 23 11:40:27 2026 UTC
gpg: using EDDSA key 6DAA6E64A76D2840571B4902528897B826403ADA
gpg: Good signature from "Werner Koch (dist signing 2020)" [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: 6DAA 6E64 A76D 2840 571B 4902 5288 97B8 2640 3ADA
gpg: Signature made Thu Sep 24 00:40:06 2026 UTC
gpg: using EDDSA key AC8E115BF73E2D8D47FA9908E98E9B2D19C6C8BD
gpg: Good signature from "Niibe Yutaka (GnuPG Release Key)" [unknown]
gpg: Signature notation: manu=2,2.5+1.12,2,2
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: AC8E 115B F73E 2D8D 47FA 9908 E98E 9B2D 19C6 C8BD
Exit code 0. Two lines matter here, and they say different things.
Good signature, and the warning under it
"Good signature" is the maths. The file hasn't changed by a single byte since it was signed, and it was signed by the private key matching the public key you imported.
"WARNING: This key is not certified with a trusted signature!" is about the
key, not the file. GnuPG has no record that this key belongs to Werner Koch,
because nothing in your keyring vouches for it. The [unknown] after the name
says the same. Everybody sees this warning the first time, with every project.
It doesn't mean something is wrong. It means one check is still yours to do.
That check is the fingerprint. If an attacker replaced the tarball on a mirror,
they could replace the .sig and serve their own key too, under the same name,
and gpg would print "Good signature" for that just as happily. What they can't
do is make their key's fingerprint match the real one.
-
Find the fingerprint somewhere other than the place you got the key. For GnuPG, its signature key page lists each key with its fingerprint. Other projects put it in their README, their security page or their documentation.
-
Compare it, all 40 characters, with the
Primary key fingerprintline. Not the short key ID: those can be forged. -
If it matches, tell GnuPG you checked, with your own key (make one first if you have none):
gpg --quick-lsign-key 6DAA6E64A76D2840571B4902528897B826403ADA gpg --quick-lsign-key AC8E115BF73E2D8D47FA9908E98E9B2D19C6C8BDA local signature stays in your keyring and is never published.
After that, the same command's Good signature lines read:
gpg: Good signature from "Werner Koch (dist signing 2020)" [full]
gpg: Good signature from "Niibe Yutaka (GnuPG Release Key)" [full]
with no warning under them, for this release and every later one signed by those keys. Skipping the local signature is fine too, as long as you read the fingerprint each time.
Getting the key and the fingerprint from the same site you downloaded from is weaker than it looks: whoever controls the site controls both. It still beats a mirror, and it beats no check at all. The strongest sources are the ones an attacker would have to compromise separately: a key bundled with the previous release you already trust, a signed announcement, the project's repository, or a maintainer in person.
When it fails
To see a failure, one byte of a copy of the tarball was changed:
gpg --verify gnupg-2.5.24.tar.bz2.sig tampered.tar.bz2
gpg: Signature made Wed Sep 23 11:40:27 2026 UTC
gpg: using EDDSA key 6DAA6E64A76D2840571B4902528897B826403ADA
gpg: BAD signature from "Werner Koch (dist signing 2020)" [unknown]
gpg: Signature made Thu Sep 24 00:40:06 2026 UTC
gpg: using EDDSA key AC8E115BF73E2D8D47FA9908E98E9B2D19C6C8BD
gpg: BAD signature from "Niibe Yutaka (GnuPG Release Key)" [unknown]
Exit code 1. One changed byte in 8.4 MB is enough. A BAD signature means the file isn't the one that was signed: a cut-off download, a different build or version, an archive someone repacked, or an attack. Download it again from the project's own site before you assume the worst, and don't run it in the meantime.
| gpg says | Exit code | Means | Do this |
|---|---|---|---|
Good signature and the not certified warning |
0 | The file matches the key | Compare the fingerprint with the published one |
Good signature with [full] or [ultimate] |
0 | The file matches a key you already checked | Nothing. You're done |
BAD signature |
1 | The file, or the signature, has changed | Download again; don't use it |
Can't check signature: No public key |
2 | Nothing was checked | Get the key it names, check its fingerprint, import it |
If you run only the signature, gpg --verify gnupg-2.5.24.tar.bz2.sig, gpg
looks for the file by dropping .sig and says so (assuming signed data in 'gnupg-2.5.24.tar.bz2'). Naming both is clearer.
When the project signs a checksum file instead
Many projects sign one list of hashes rather than every file. Node.js does: each
release has a SHASUMS256.txt, a detached SHASUMS256.txt.sig, and a
clearsigned SHASUMS256.txt.asc. Its README lists the release keys by
fingerprint and gives a keyserver command for each. Checked here on the
v26.12.0 release:
gpg --keyserver hkps://keys.openpgp.org --recv-keys C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C
gpg --verify SHASUMS256.txt.sig SHASUMS256.txt
gpg: Signature made Wed Oct 14 15:21:34 2026 UTC
gpg: using RSA key C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C
gpg: Good signature from "Richard Lau <[email protected]>" [unknown]
gpg: aka "Richard Lau <[email protected]>" [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
That proves the list. Then check your download against it. The list covers 34 files, so tell the checker to skip the ones you didn't download:
sha256sum -c --ignore-missing SHASUMS256.txt # Linux
shasum -a 256 -c --ignore-missing SHASUMS256.txt # macOS
node-v26.12.0-headers.tar.gz: OK
Both steps are needed: the signature on its own says nothing about your file, and the hash on its own says nothing about who published it. Checking hashes, including on Windows, has its own guide: check a file against its checksum.
gpg --verify SHASUMS256.txt.asc checks the text inside the .asc, and
says so when a file of the same name sits beside it:
WARNING: not a detached signature; file 'SHASUMS256.txt' was NOT verified!
When both are offered, verify the detached .sig against the .txt, and
check hashes against that same .txt.
Detached, clearsigned or inline
A signature comes in three shapes, and you can tell them apart by looking.
| Shape | Made with | Looks like | Verify with |
|---|---|---|---|
| Detached | gpg --detach-sign (-b) |
A separate .sig, or .asc starting BEGIN PGP SIGNATURE |
gpg --verify file.sig file |
| Clearsigned | gpg --clearsign |
Readable text between BEGIN PGP SIGNED MESSAGE and the signature |
gpg --verify file.asc |
| Inline | gpg --sign (-s) |
One binary .gpg holding the data and the signature |
gpg --verify file.gpg, then gpg -d -o file file.gpg to get the data out |
Releases are almost always detached, because the download stays untouched: the tarball you check is the tarball you unpack. Clearsigning suits text people read anyway, such as checksum lists and security advisories. Inline signing wraps the data, so you can't use the file until you unwrap it.
In a script
gpg --verify exits 0 on a good signature from any key in your keyring, with
or without the warning. In a build script or a Dockerfile, use gpgv with a
keyring holding only the keys you checked:
gpg --dearmor < signature_key.asc > gnupg-keys.gpg
gpgv --keyring ./gnupg-keys.gpg gnupg-2.5.24.tar.bz2.sig gnupg-2.5.24.tar.bz2
gpgv: Signature made Wed Sep 23 11:40:27 2026 UTC
gpgv: using EDDSA key 6DAA6E64A76D2840571B4902528897B826403ADA
gpgv: Good signature from "Werner Koch (dist signing 2020)"
gpgv: Signature made Thu Sep 24 00:40:06 2026 UTC
gpgv: using EDDSA key AC8E115BF73E2D8D47FA9908E98E9B2D19C6C8BD
gpgv: Good signature from "Niibe Yutaka (GnuPG Release Key)"
It exits 1 on a BAD signature, so a script running with set -e stops there,
and no other key you happen to hold can satisfy it. Commit the keyring file next to the script, after
checking its fingerprints once.
In the browser
The same check works in a tab, with nothing uploaded. Keep the project's key in this browser first, then verify against it.
- Drop the key file on PGP Import Keys, or paste it under Key block.
- Turn on Keep this key in this browser, then press Read this key.
Every key in the block is listed with its full fingerprint (GnuPG's
signature_key.ascreads as six). - Compare the fingerprints with the published ones. Stop here if they don't match.
- Press Import this key (for a block of several, the button gives the number of keys).
- Drop the download on PGP Verify File and
the
.sigor.ascon Drop the signature file. - Press Verify file.
- Read the headline. Valid signature from and a name is GnuPG's Good
signature. Check that the fingerprint under Signed by is the one the
project publishes: like
gpg --verify, the tab accepts a signature from any key you keep. Signature is not valid is GnuPG's BAD signature.
A release and its .sig, checked in the browser against the release key.
There is no separate step for vouching for a key, and no "not certified" warning: a key you kept is a key the tab checks against. So the fingerprint comparison before you import is the whole of that step. Keys can't be kept in a private window or with site data blocked for this site, and then there is nothing to verify against.
Without the signer's key, the tab says "Signature could not be checked", shown
as a warning rather than a failure, with the key ID the signature claims: the
last 16 characters of the fingerprint gpg prints in its using … key line.
No key, so nothing was checked. The tool names the key it needs.
Its Import the signer's key link opens PGP Import Keys. "Not checked" is the honest answer here, like gpg's "No public key": it says nothing either way about the file.
Then use the file
With a good signature from a fingerprint you checked, the file is the one the
project released. Unpack it, install it, or pass it on with the .sig beside
it, so the next person can run the same check. Keep the imported key, in GnuPG
or in the browser: the next release will most likely be signed by the same one,
and the check takes a second.