GnuPG Essentials: Keys, Encryption and Signatures You Can Verify
- Explain which half of a key pair signs, verifies, encrypts and decrypts, and why a signature adds authenticity to a checksum
- Generate a key pair with gpg, list it, and identify it by fingerprint; keep the revocation certificate
- Use --pinentry-mode loopback on a box without a pinentry program
- Encrypt and decrypt a file symmetrically with gpg -c
- Export, inspect, import and certify a public key, then encrypt a file only its owner can open
- Make detached signatures and verify them with gpg --verify and gpgv --keyring
- Read gpg's four verdicts and exit codes correctly, and refuse a key that arrived with the file it signs
Pick up where hashing left off
Explain what a key pair does that a checksum cannot, and which half of the pair each operation uses.
The hashing lesson ended on a promise.
sha256sum -cproves a file matches a digest, which is integrity. It cannot prove the digest is the publisher's, because whoever can swap the file on a server can usually swap the checksum next to it. Proving authenticity needs a signature, and the hashing lesson only showed one as a transcript. This lesson makes it real: you'll create keys, encrypt, sign, and read the verdicts on real boxes.GnuPG (
gpg) implements OpenPGP, which is built on key pairs. Each pair has two halves that only work together:- the private key stays with its owner. It signs and it decrypts;
- the public key is meant to be handed out. It verifies signatures and encrypts to the owner.
So a signature is something only the private key could have produced and anyone with the public key can check. Encryption is the mirror image: anyone with your public key can lock a file that only your private key opens. The chain of trust from the hashing lesson becomes concrete. You trust a public key, the key verifies a signature, the signature vouches for
SHA256SUMS, andSHA256SUMSvouches for every file it lists.Two GnuPG binaries matter here, and the two lab images differ. The RHEL box behind this lesson's key, encryption and signing demos ships the full
gpg. The Ubuntu box behind the verification demos ships onlygpgv, a small verify-only tool that checks a signature against a keyring you name and can do nothing else: no keys, no encryption, no signing. Ubuntu'saptuses it the same way to check repository signatures.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --version | head -1gpg (GnuPG) 2.3.3
- --version
- print the version, then the supported algorithms (head keeps the first line)
On the RHEL lab box: the full GnuPG suite, version 2.3. Anything that creates or uses a private key needs this binary. Runnable in the signature-verify room — that room’s setup plants the files it reads
command -v gpg gpgv/usr/bin/gpgv
- command -v
- print where each command would be found, or nothing if it isn’t installed
On the Ubuntu lab box: command -vprints the path of each name it finds and stays silent for the rest, so onlygpgvappears. That box can verify a signature but cannot make one or hold a private key, which is exactly what a machine that only installs software needs.Check yourself
To check a signature someone else made, which of their keys do you need?
A key of your own, and what its fingerprint is
Generate a key pair, list it, and identify it by fingerprint rather than by name.
A new key needs a user ID, normally
Name <email>, and a passphrase that encrypts the private key on disk.gpg --quick-generate-key 'Name <email>'takes the defaults. On the RHEL 9 lab box that is an RSA 3072 primary key for signing and certifying[SC], an RSA 3072 subkey for encryption[E], and a two-year expiry. Name an algorithm to choose differently:gpg --quick-generate-key 'Name <email>' future-default default 2ymakes the modern Ed25519 + Curve25519 pair instead.The lab boxes ship gpg without a pinentry program, the small helper that normally pops up a passphrase dialog. Without it, any command that needs a passphrase fails with
No pinentry. The fix is--pinentry-mode loopback, which tells gpg to ask in your terminal itself. Typed interactively it looks like this:Example — not runnable on your lab box
gpg --pinentry-mode loopback --quick-generate-key 'Demo Learner <demo@example.org>'About to create a key for: "Demo Learner <demo@example.org>" Continue? (Y/n) y We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. Enter passphrase: gpg: /root/.gnupg/trustdb.gpg: trustdb created gpg: key 83A722E962C77822 marked as ultimately trusted gpg: directory '/root/.gnupg/openpgp-revocs.d' created gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/EE6A3220308C336529D2723D83A722E962C77822.rev' public and secret key created and signed. pub rsa3072 2026-10-02 [SC] [expires: 2028-10-01] EE6A3220308C336529D2723D83A722E962C77822 uid Demo Learner <demo@example.org> sub rsa3072 2026-10-02 [E]- --pinentry-mode loopback
- gpg asks for the passphrase itself, in the terminal, instead of through a pinentry program (this box has none)
The interactive form. gpg confirms the user ID, asks for the passphrase once (loopback mode doesn't ask you to repeat it, so type carefully), generates the keys and prints a summary. The long hex string under pubis the fingerprint. This transcript is from the RHEL lab box; the fingerprint and dates are different every time.The demos in this lesson pass the passphrase on the command line so every block runs unattended. That is fine for a throwaway demo key and wrong for a real one, as the callout below explains.
Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'demo only' --quick-generate-key 'Demo Learner <demo@example.org>'gpg: directory '/root/.gnupg' created gpg: keybox '/root/.gnupg/pubring.kbx' created gpg: /root/.gnupg/trustdb.gpg: trustdb created gpg: key 128CBE4D29616B04 marked as ultimately trusted gpg: directory '/root/.gnupg/openpgp-revocs.d' created gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/CBE724323E1F256EE884C04C128CBE4D29616B04.rev'
- --batch
- never prompt; fail instead of asking a question
- --pinentry-mode loopback
- gpg asks for the passphrase itself, in the terminal, instead of through a pinentry program (this box has none)
- --passphrase '…'
- take the passphrase from the command line. Demo only: it lands in your shell history and in
ps
The same key generation without prompts. gpg creates ~/.gnupgon first use, marks your own new key as ultimately trusted (you made it, so you vouch for it), and writes a revocation certificate named after the fingerprint.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --list-keys demo@example.orggpg: checking the trustdb gpg: marginals needed: 3 completes needed: 1 trust model: pgp gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u gpg: next trustdb check due at 2028-10-01 pub rsa3072 2026-10-02 [SC] [expires: 2028-10-01] CBE724323E1F256EE884C04C128CBE4D29616B04 uid [ultimate] Demo Learner <demo@example.org> sub rsa3072 2026-10-02 [E]- --list-keys
- show public keys; a name, e-mail or fingerprint narrows the list (
-kfor short)
Lists the public half. pubis the primary key with its algorithm, creation date and capabilities (Ssign,Ccertify). The 40-hex line is the fingerprint.uidshows the user ID and its validity,[ultimate]because it is your own key.subis the encryption subkey. Thechecking the trustdblines appear once after the keyring changes.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --list-secret-keys --keyid-format long demo@example.orgsec rsa3072/128CBE4D29616B04 2026-10-02 [SC] [expires: 2028-10-01] CBE724323E1F256EE884C04C128CBE4D29616B04 uid [ultimate] Demo Learner <demo@example.org> ssb rsa3072/C52BF9487B4C57D7 2026-10-02 [E]- --list-secret-keys
- show keys whose private half is in this keyring (
-K) - --keyid-format long
- print 16-hex key IDs next to the algorithm
The keys you hold the private half of: secandssbinstead ofpubandsub.--keyid-format longadds the 16-hex key ID, which is the last 16 digits of the fingerprint. You'll meet key IDs in error messages, but identify keys by the full fingerprint, because short IDs have been forged.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
ls /root/.gnupg/openpgp-revocs.d/CBE724323E1F256EE884C04C128CBE4D29616B04.rev
The revocation certificate gpg wrote at generation time. If the private key is ever lost or stolen, importing this file and publishing the result tells everyone the key must no longer be trusted. Copy it somewhere offline now; it is the one file you can't recreate once the key is gone. Check yourself
Two keys in a keyring both show the user ID 'Alice Ops <alice@example.org>'. What tells you which one is really Alice's?
Symmetric first: one passphrase, no keys
Encrypt and decrypt a file with a passphrase alone, and know when that is the right tool.
Most of the time you just need a file to cross an untrusted channel, like a USB stick, a ticket attachment or a shared drive, and the other person can get a passphrase from you some other way. That is symmetric encryption: one secret both locks and unlocks, and no keys or keyrings are involved.
gpg --symmetric(-c) turns the passphrase into an AES-256 key and writesFILE.gpgnext to the original.Start with a file to protect:
Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
mkdir -p /root/gpgdemo && printf 'db_password=hunter2-prod\n' > /root/gpgdemo/creds.txtA small secret to practise on. Every later demo works inside /root/gpgdemo.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --symmetric /root/gpgdemo/creds.txtgpg: problem with the agent: No pinentry gpg: error creating passphrase: Operation cancelled gpg: symmetric encryption of '/root/gpgdemo/creds.txt' failed: Operation cancelled
- --symmetric
- encrypt with a passphrase only (
-c)
Without loopback mode this fails on the lab box. gpg asks its agent for a passphrase, the agent has no pinentry program to ask you with, and the operation is cancelled. If you see No pinentryanywhere, add--pinentry-mode loopback.Example — not runnable on your lab box
gpg --pinentry-mode loopback --symmetric /root/gpgdemo/creds.txtEnter passphrase:
- --pinentry-mode loopback
- gpg asks for the passphrase itself, in the terminal, instead of through a pinentry program (this box has none)
The interactive form: gpg prompts once in the terminal and writes /root/gpgdemo/creds.txt.gpgwithout printing anything else.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'correct horse battery' --symmetric /root/gpgdemo/creds.txt- --batch
- never prompt; fail instead of asking a question
- --pinentry-mode loopback
- gpg asks for the passphrase itself, in the terminal, instead of through a pinentry program (this box has none)
- --passphrase '…'
- take the passphrase from the command line. Demo only: it lands in your shell history and in
ps
The unattended form used for the rest of the demo. Silent on success. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
file /root/gpgdemo/creds.txt.gpg/root/gpgdemo/creds.txt.gpg: GPG symmetrically encrypted data (AES256 cipher)
The result is binary OpenPGP data. Add --armor(-a) to get a text file you can paste into an e-mail; gpg then names it.asc.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'correct horse battery' --decrypt /root/gpgdemo/creds.txt.gpggpg: AES256.CFB encrypted data gpg: encrypted with 1 passphrase db_password=hunter2-prod
- --decrypt
- decrypt to stdout, or to the file named by -o (
-d)
--decrypt(-d) writes the plaintext to standard output; add-o FILEto write it to a file. The twogpg:lines on stderr say what kind of encryption was used.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'wrong guess' --decrypt /root/gpgdemo/creds.txt.gpggpg: AES256.CFB encrypted data gpg: encrypted with 1 passphrase gpg: decryption failed: Bad session key
A wrong passphrase derives a wrong key, which gpg reports as Bad session key, with exit status 2 and no plaintext.Check yourself
You encrypted backup.tar with gpg -c. What does the person you send it to need in order to open it?
Encrypt to someone else, and the prompt you must not skip
Exchange public keys, authenticate one by fingerprint, and encrypt a file only its owner can open.
Public-key encryption removes the shared secret: you encrypt with the recipient's public key, and only their private key can decrypt. The hard part is no longer keeping a secret. It is being sure the public key really belongs to the person you mean.
To play a second person on one box, the demo gives Alice her own keyring with
--homedir, which is all a separate machine would be from gpg's point of view.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
mkdir -m 700 /root/gpgdemo/alice && gpg --homedir /root/gpgdemo/alice --batch --pinentry-mode loopback --passphrase '' --quick-generate-key 'Alice Ops <alice@example.org>' future-default default nevergpg: keybox '/root/gpgdemo/alice/pubring.kbx' created gpg: /root/gpgdemo/alice/trustdb.gpg: trustdb created gpg: key 0CCA19E18C8DA86E marked as ultimately trusted gpg: directory '/root/gpgdemo/alice/openpgp-revocs.d' created gpg: revocation certificate stored as '/root/gpgdemo/alice/openpgp-revocs.d/836EEC02267964B053D0F25A0CCA19E18C8DA86E.rev'
- --homedir DIR
- use DIR as the keyring instead of ~/.gnupg
- future-default
- algorithm: Ed25519 primary + Curve25519 encryption subkey
Alice's key pair, in her own keyring: an Ed25519 signing key with a Curve25519 encryption subkey ( future-default), no expiry, and no passphrase because it's a throwaway.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --homedir /root/gpgdemo/alice --armor --export alice@example.org > /root/gpgdemo/alice.asc- --armor
- ASCII output instead of binary (
-a) - --export
- write public keys only
Alice exports her public key as armored text, the file she would e-mail you or put on a web page. --exportnever includes private keys; that takes--export-secret-keys, which you should never send anyone.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --show-keys /root/gpgdemo/alice.ascpub ed25519 2026-10-02 [SC] 836EEC02267964B053D0F25A0CCA19E18C8DA86E uid Alice Ops <alice@example.org> sub cv25519 2026-10-02 [E]- --show-keys
- list the keys in a file without importing them
Look before you import. --show-keysprints what a key file contains without adding anything to your keyring, so you can compare the fingerprint first.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --import /root/gpgdemo/alice.ascgpg: key 0CCA19E18C8DA86E: public key "Alice Ops <alice@example.org>" imported gpg: Total number processed: 1 gpg: imported: 1
Adds Alice's public key to your keyring. Importing does not mean trusting: the key is now known, but nothing says it is really Alice's. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --encrypt -r alice@example.org /root/gpgdemo/creds.txtgpg: D391F152A3B9A55B: There is no assurance this key belongs to the named user gpg: /root/gpgdemo/creds.txt: encryption failed: Unusable public key
- --encrypt
- public-key encryption (
-e) - -r NAME
- recipient: an e-mail, name or fingerprint
Refused. gpg won't encrypt to a key nobody has vouched for when it can't ask you about it. The ID it names is Alice's encryption subkey. Run interactively, gpg asks instead of refusing:
Example — not runnable on your lab box
gpg --encrypt -r alice@example.org /root/gpgdemo/creds.txtgpg: A1126609FA4B3D96: There is no assurance this key belongs to the named user sub cv25519/A1126609FA4B3D96 2026-10-02 Alice Ops <alice@example.org> Primary key fingerprint: 099E 9782 2B6D B8AF EFD9 BA0B C08F A527 B440 9BD8 Subkey fingerprint: A3C8 224C EAF2 7C2A 92EB 5CEF A112 6609 FA4B 3D96 It is NOT certain that the key belongs to the person named in the user ID. If you *really* know what you are doing, you may answer the next question with yes. Use this key anyway? (y/N) n gpg: /root/gpgdemo/creds.txt: encryption failed: Unusable public keyThis prompt is the key-distribution problem, printed on your screen. gpg is saying that it can encrypt, but has no idea whether this key belongs to Alice or to someone who typed her name into a key. Answering yout of habit is how mail ends up readable by the wrong person.The right answer is to check, once. Get Alice's fingerprint through a channel an attacker can't also control: she reads it to you on a call, it's printed in a contract, or it's on her badge at a meeting. Compare it with the key you imported, then record that you checked by certifying the key with your own. A local signature (
--lsign-key) is never exported, so it affects only your keyring.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --fingerprint alice@example.orgpub ed25519 2026-10-02 [SC] 836E EC02 2679 64B0 53D0 F25A 0CCA 19E1 8C8D A86E uid [ unknown] Alice Ops <alice@example.org> sub cv25519 2026-10-02 [E]- --fingerprint
- list keys with the fingerprint grouped for reading
The fingerprint in groups of four, the format people read aloud and print. Compare every group. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --yes --pinentry-mode loopback --passphrase 'demo only' --lsign-key alice@example.orgpub ed25519/0CCA19E18C8DA86E created: 2026-10-02 expires: never usage: SC trust: unknown validity: unknown sub cv25519/D391F152A3B9A55B created: 2026-10-02 expires: never usage: E [ unknown] (1). Alice Ops <alice@example.org> pub ed25519/0CCA19E18C8DA86E created: 2026-10-02 expires: never usage: SC trust: unknown validity: unknown Primary key fingerprint: 836E EC02 2679 64B0 53D0 F25A 0CCA 19E1 8C8D A86E Alice Ops <alice@example.org> Are you sure that you want to sign this key with your key "Demo Learner <demo@example.org>" (128CBE4D29616B04) The signature will be marked as non-exportable.- --lsign-key
- certify a key with a local, non-exportable signature
- --yes
- assume yes to confirmation questions
Certifies Alice's key with your key, locally. Interactively, gpg shows the fingerprint one last time and asks Really sign? (y/N);--yesanswers for the demo. Your passphrase is needed because this is a signature made with your private key.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --list-keys alice@example.orggpg: checking the trustdb gpg: marginals needed: 3 completes needed: 1 trust model: pgp gpg: depth: 0 valid: 1 signed: 1 trust: 0-, 0q, 0n, 0m, 0f, 1u gpg: depth: 1 valid: 1 signed: 0 trust: 1-, 0q, 0n, 0m, 0f, 0u gpg: next trustdb check due at 2028-10-01 pub ed25519 2026-10-02 [SC] 836EEC02267964B053D0F25A0CCA19E18C8DA86E uid [ full ] Alice Ops <alice@example.org> sub cv25519 2026-10-02 [E]Alice's user ID is now [ full ]valid, because a key you trust ultimately (yours) has certified it.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --encrypt -r alice@example.org -o /root/gpgdemo/for-alice.gpg /root/gpgdemo/creds.txt- -o FILE
- write the output to FILE
Now it works, silently. -onames the output file.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --homedir /root/gpgdemo/alice --batch --decrypt /root/gpgdemo/for-alice.gpggpg: encrypted with cv25519 key, ID D391F152A3B9A55B, created 2026-10-02 "Alice Ops <alice@example.org>" db_password=hunter2-prodAlice opens it with her private key. gpg names the subkey the file was encrypted to. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --decrypt /root/gpgdemo/for-alice.gpggpg: encrypted with cv25519 key, ID D391F152A3B9A55B, created 2026-10-02 "Alice Ops <alice@example.org>" gpg: public key decryption failed: No secret key gpg: decryption failed: No secret keyYou can't open it, even though you made it. It was encrypted to Alice's key only. To keep a readable copy, add yourself as a second recipient ( -r alice@example.org -r demo@example.org).Check yourself
gpg says 'There is no assurance this key belongs to the named user'. What should you do before you answer yes?
Sign without encrypting: detached signatures
Make a detached signature over a checksum manifest and verify it, the way software projects publish releases.
A signature doesn't hide anything. It proves who produced a file and that it hasn't changed since. gpg hashes the file and signs the hash with your private key. There are three output forms:
--signwraps the file and the signature together in one OpenPGP message;--clearsignkeeps the text readable with the signature appended below it;--detach-signwrites only the signature, to its own file.
Projects publish detached signatures because the release stays untouched and usable whether or not you check it. This is the
SHA256SUMS+SHA256SUMS.gpgpattern from the hashing lesson. One signature over the manifest covers every file the manifest lists. Now you can build it yourself:Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
cd /root/gpgdemo && sha256sum creds.txt creds.txt.gpg > SHA256SUMS && cat SHA256SUMS08f3d276b24f9fa61486be6dfd6cccc0fabf38595a40deb54d6e2fffe42fa3d4 creds.txt c8fa9725389173bc8cc35538d7cd7977f68ee44fa1178748cb0c4be32449ca77 creds.txt.gpg
A manifest of two files, exactly as in the hashing lesson. The .gpgfile's digest differs every run, because encryption is randomised.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'demo only' --armor --detach-sign /root/gpgdemo/SHA256SUMS- --batch
- never prompt; fail instead of asking a question
- --pinentry-mode loopback
- gpg asks for the passphrase itself, in the terminal, instead of through a pinentry program (this box has none)
- --passphrase '…'
- take the passphrase from the command line. Demo only: it lands in your shell history and in
ps - --detach-sign
- write only the signature, in a separate file (
-b)
Writes SHA256SUMS.ascnext to the manifest and leaves the manifest itself unchanged. Without--armoryou get a binarySHA256SUMS.siginstead; both verify the same way.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
head -1 /root/gpgdemo/SHA256SUMS.asc-----BEGIN PGP SIGNATURE-----
An armored signature is a few lines of text between BEGIN and END markers. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --verify /root/gpgdemo/SHA256SUMS.asc /root/gpgdemo/SHA256SUMSgpg: Signature made Fri Oct 2 16:53:21 2026 UTC gpg: using RSA key CBE724323E1F256EE884C04C128CBE4D29616B04 gpg: Good signature from "Demo Learner <demo@example.org>" [ultimate]
- --verify SIG FILE
- check a detached signature against the file it signs
Signature first, then the file it covers. gpg reports when the signature was made, by which key (fingerprint), and Good signature.[ultimate]is the validity of the user ID: your own key.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'demo only' --clearsign -o /root/gpgdemo/notice.asc /root/gpgdemo/SHA256SUMS && gpg --verify /root/gpgdemo/notice.asc /root/gpgdemo/SHA256SUMSgpg: not a detached signature
- --clearsign
- readable text with the signature appended
A clear-signed file is not a detached signature, and gpg says so when you hand it one. If a project tells you to verify X.sigagainstX, the.sigmust be the detached form.Check yourself
Which gpg option writes a signature to its own file and leaves the signed file untouched?
Read the verdicts properly
Tell apart a good, an untrusted, a BAD and an uncheckable signature, by message and by exit status.
People tend to read signature checks as "it worked" or "it didn't". gpg can give four different answers, and only one of them means what people usually assume:
| gpg says | exit | what it proves |
|---|---|---|
|
Good signature … [ultimate]/[full]| 0 | the file is unchanged and the key is one you have verified ||
Good signature … [unknown]+WARNING: This key is not certified| 0 | the file is unchanged since this key signed it. Who owns the key is unproven ||
BAD signature| 1 | the file changed after it was signed (or the signature was damaged) ||
Can't check signature: No public key| 2 | nothing: you don't have the signer's key |The second row is the dangerous one. It exits 0, so a script that checks only the exit status passes it. Here are all four, starting with Alice's view of your signature. She has your public key but hasn't certified it:
Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --armor --export demo@example.org > /root/gpgdemo/demo.asc && gpg --homedir /root/gpgdemo/alice --import /root/gpgdemo/demo.ascgpg: key 128CBE4D29616B04: public key "Demo Learner <demo@example.org>" imported gpg: Total number processed: 1 gpg: imported: 1
Alice imports your public key, and only imports it. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --homedir /root/gpgdemo/alice --verify /root/gpgdemo/SHA256SUMS.asc /root/gpgdemo/SHA256SUMSgpg: Signature made Fri Oct 2 16:53:21 2026 UTC gpg: using RSA key CBE724323E1F256EE884C04C128CBE4D29616B04 gpg: checking the trustdb gpg: marginals needed: 3 completes needed: 1 trust model: pgp gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u gpg: Good signature from "Demo Learner <demo@example.org>" [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: CBE7 2432 3E1F 256E E884 C04C 128C BE4D 2961 6B04
Good signature, followed by the warning. The bytes match the key, but Alice has never confirmed the key is yours, so gpg won't claim the signature "belongs to the owner". It prints the fingerprint so she can check it.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
cp /root/gpgdemo/SHA256SUMS /root/gpgdemo/SHA256SUMS.orig && echo 'deadbeef backdoor.sh' >> /root/gpgdemo/SHA256SUMS && gpg --verify /root/gpgdemo/SHA256SUMS.asc /root/gpgdemo/SHA256SUMS; echo "exit=$?"gpg: Signature made Fri Oct 2 16:53:21 2026 UTC gpg: using RSA key CBE724323E1F256EE884C04C128CBE4D29616B04 gpg: BAD signature from "Demo Learner <demo@example.org>" [ultimate] exit=1
One added line and the signature is BAD, with exit status 1. This is the attack from the hashing lesson, caught: an attacker can edit the manifest, but can't produce a matching signature without your private key.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
mv /root/gpgdemo/SHA256SUMS.orig /root/gpgdemo/SHA256SUMS && mkdir -m 700 /root/gpgdemo/stranger && gpg --homedir /root/gpgdemo/stranger --verify /root/gpgdemo/SHA256SUMS.asc /root/gpgdemo/SHA256SUMS; echo "exit=$?"gpg: keybox '/root/gpgdemo/stranger/pubring.kbx' created gpg: Signature made Fri Oct 2 16:53:21 2026 UTC gpg: using RSA key CBE724323E1F256EE884C04C128CBE4D29616B04 gpg: Can't check signature: No public key exit=2
The original manifest is back. A stranger with an empty keyring checks it and gets No public keywith exit 2. They can see which key fingerprint made the signature, and nothing else.gpgvon the Ubuntu box reports the same verdicts without the trust layer: it trusts every key in the keyring you give it and nothing else. That makes--keyringthe whole security decision. Without it, gpgv falls back to~/.gnupg/trustedkeys.kbx, which doesn't exist on the lab box:Runnable in the signature-verify room — that room’s setup plants the files it reads
cd "$(ls -d /root/downloads/*/ | head -1)" && gpgv SHA256SUMS.sig SHA256SUMS; echo "exit=$?"gpgv: unknown type of key resource 'trustedkeys.kbx' gpgv: keyblock resource '/root/.gnupg/trustedkeys.kbx': General error gpgv: Signature made Thu Oct 1 22:53:24 2026 UTC gpgv: using EDDSA key A2F5DCF32E815D7707BA3E75A9D3C7EFC35C7982 gpgv: Can't check signature: No public key exit=2
- --keyring FILE
- trust exactly the keys in FILE
In the signature-verify room's first mirror directory: no keyring named, no default keyring to fall back on, so the result is No public keyand exit 2. Name the key you trust withgpgv --keyring FILE SIG DATA, and give FILE as a path (./key.gpg): a bare filename is looked up inside~/.gnupg.Check yourself
gpg prints 'Good signature' and then 'WARNING: This key is not certified with a trusted signature!'. Has it proved that the file comes from the person named in the key?
The mistake: trusting the key that came with the file
Explain why a signature checked against a key from the same source proves nothing, and where trustworthy keys come from.
A valid signature proves the bytes match a key, nothing more. If the key came from the same place as the file, you have only proved that whoever controls that place is consistent with themselves. An attacker who can replace a tarball on a mirror can also replace the checksum file, re-sign it with their own key, and put that key on the same page with the vendor's name in its user ID. Every check then passes, and none of them meant anything.
The signature-verify room is built on this. Each of its four mirrors offers a
signing-key.gpg, and the vendor's real fingerprint is in the brief, which stands in for a support contract that reached you separately:Runnable in the signature-verify room — that room’s setup plants the files it reads
grep -A2 'fingerprint' /root/case/brief.txt/root/case/vendor-keyring.gpg, and its fingerprint, as printed in the contract, is: 409B 5C19 EEE5 3730 E238 E5AE 4FB6 303F DF96 1A39
The out-of-band fingerprint. It differs per session. Runnable in the signature-verify room — that room’s setup plants the files it reads
for m in /root/downloads/*/; do echo "== $m"; (cd "$m" && gpgv --keyring ./signing-key.gpg SHA256SUMS.sig SHA256SUMS 2>&1 | grep -E 'using|signature'); done== /root/downloads/community/ gpgv: using EDDSA key A2F5DCF32E815D7707BA3E75A9D3C7EFC35C7982 gpgv: Good signature from "Ledgerline Systems Release Signing <release@ledgerline.example>" == /root/downloads/eu-west/ gpgv: using EDDSA key 409B5C19EEE53730E238E5AE4FB6303FDF961A39 gpgv: BAD signature from "Ledgerline Systems Release Signing <release@ledgerline.example>" == /root/downloads/fastdl/ gpgv: using EDDSA key 409B5C19EEE53730E238E5AE4FB6303FDF961A39 gpgv: Good signature from "Ledgerline Systems Release Signing <release@ledgerline.example>" == /root/downloads/pacific/ gpgv: using EDDSA key 409B5C19EEE53730E238E5AE4FB6303FDF961A39 gpgv: Good signature from "Ledgerline Systems Release Signing <release@ledgerline.example>"
The trap: verify each mirror against the key that mirror supplied. Three mirrors report Good signature, all from "the vendor", but read theusing EDDSA keylines. One good signature comes from a different fingerprint than the other two and than the brief. That is a key someone created with the vendor's name. Verifying against/root/case/vendor-keyring.gpginstead turns it intoNo public key, which is the verdict you need to see.Where trustworthy keys come from, roughly from best to worst:
- a key you already have, installed through a channel you trust. Distribution keyrings (
/usr/share/keyrings/, apt'ssigned-by=) arrive with the OS itself; - a fingerprint obtained separately from the key: read out on a call, printed on paper, shown in person, or published on several independent channels you can cross-check;
- a key certified by someone whose key you have already verified (the "web of trust").
A keyserver is a directory, not an authority. Anyone can upload a key with any name on it, so a key fetched from one is a candidate that still needs its fingerprint checked.
Check yourself
A mirror ships a tarball, SHA256SUMS, SHA256SUMS.sig and 'the vendor's key'. You verify with that key and get Good signature. What have you proved about the vendor?
- a key you already have, installed through a channel you trust. Distribution keyrings (
Try it in your live terminal
Run a full sign, verify and encrypt round trip yourself, then take on the two labs.
On the RHEL box (the gpg-keys-and-signing room), make a practice identity and run the whole cycle. These commands pass the passphrase inline so you can paste them. Drop
--batchand--passphrase '…'from any of them to see the loopback prompt instead. Then do the labs: signature-verify (Ubuntu box,gpgvonly) has you pick the one genuine release out of four mirrors, and gpg-keys-and-signing (RHEL box) has you build a working setup and authenticate a colleague's key before encrypting to it.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'practice pass' --quick-generate-key 'Practice Me <me@example.net>' future-default default 1yA practice key pair: Ed25519 + Curve25519, valid for a year. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --list-secret-keys --keyid-format long me@example.netFind its fingerprint, and the revocation certificate named after it in ~/.gnupg/openpgp-revocs.d/. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
echo 'ship it' > /root/note.txt && gpg --batch --pinentry-mode loopback --passphrase 'practice pass' -u me@example.net --detach-sign /root/note.txtA detached signature, note.txt.sig. -upicks which of your keys signs when you have more than one.Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --verify /root/note.txt.sig /root/note.txtShould say Good signature. Edit note.txt and run it again to watch it turn BAD. Runnable in the gpg-keys-and-signing room — that room’s setup plants the files it reads
gpg --batch --pinentry-mode loopback --passphrase 'practice pass' -c -o /root/note.txt.gpg /root/note.txt && gpg --batch --pinentry-mode loopback --passphrase 'practice pass' -d /root/note.txt.gpgA symmetric round trip: encrypt with a passphrase, decrypt it again.
- The private key signs and decrypts; the public key verifies and encrypts. A signature adds authenticity to a checksum, which only gives integrity.
- A key's identity is its fingerprint, not the name and e-mail in its user ID, which anyone can type. Keep the revocation certificate gpg writes at generation.
- The lab boxes have no pinentry program: add --pinentry-mode loopback and gpg asks for the passphrase in the terminal.
- gpg -c encrypts with a passphrase alone. gpg -e -r FPR encrypts to a public key, but only after you have checked its fingerprint out of band and certified it (--lsign-key).
- Projects publish detached signatures over SHA256SUMS. Verify the signature with a key you trust, then run sha256sum -c. Neither check is enough on its own.
- Good + trusted, Good + 'not certified', BAD and 'No public key' are four different verdicts. Exit 0 covers the first two, so scripts should use gpgv --keyring or check VALIDSIG.
- A signature checked against a key that came with the file proves nothing about the publisher.
| Command | Does | Watch for |
|---|---|---|
gpg --pinentry-mode loopback --quick-generate-key 'Name <email>' | New key pair | Note the fingerprint; back up the .rev file |
gpg -k / gpg -K / gpg --fingerprint | List public / secret keys | Identify keys by full fingerprint |
gpg -c FILE / gpg -d FILE.gpg | Symmetric encrypt / decrypt | Send the passphrase by another channel; remove the plaintext |
gpg --armor --export ID / gpg --show-keys F / gpg --import F | Share, inspect and add public keys | Never --export-secret-keys to anyone |
gpg --lsign-key ID | Certify a key you have verified | Only after the fingerprint matched out of band |
gpg -e -r FPR -o OUT FILE | Encrypt to a recipient | Add -r yourself to keep a readable copy |
gpg --detach-sign FILE / gpg --verify SIG FILE | Sign / verify | Read the verdict and the fingerprint, not just the exit code |
gpgv --keyring ./key.gpg SIG FILE | Verify against exactly one keyring | The keyring is your whole trust decision |
