Signal9Signal9 — home
online
all lessons

GnuPG Essentials: Keys, Encryption and Signatures You Can Verify

Intermediate30 min8 steps
What you'll learn
  • 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
  1. 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 -c proves 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, and SHA256SUMS vouches 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 only gpgv, 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's apt uses 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 -1
    gpg (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 -v prints the path of each name it finds and stays silent for the rest, so only gpgv appears. 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?

  2. 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 2y makes 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 pub is 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 ~/.gnupg on 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.org
    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: 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 (-k for short)
    Lists the public half. pub is the primary key with its algorithm, creation date and capabilities (S sign, C certify). The 40-hex line is the fingerprint. uid shows the user ID and its validity, [ultimate] because it is your own key. sub is the encryption subkey. The checking the trustdb lines 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.org
    sec   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: sec and ssb instead of pub and sub. --keyid-format long adds 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?

  3. 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 writes FILE.gpg next 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.txt
    A 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.txt
    gpg: 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 pinentry anywhere, add --pinentry-mode loopback.

    Example — not runnable on your lab box

    gpg --pinentry-mode loopback --symmetric /root/gpgdemo/creds.txt
    Enter 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.gpg without 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.gpg
    gpg: 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 FILE to write it to a file. The two gpg: 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.gpg
    gpg: 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?

  4. 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 never
    gpg: 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. --export never 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.asc
    pub   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-keys prints 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.asc
    gpg: 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.txt
    gpg: 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.txt
    gpg: 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 key
    This 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 y out 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.org
    pub   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.org
    pub  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); --yes answers 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.org
    gpg: 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. -o names 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.gpg
    gpg: encrypted with cv25519 key, ID D391F152A3B9A55B, created 2026-10-02
          "Alice Ops <alice@example.org>"
    db_password=hunter2-prod
    Alice 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.gpg
    gpg: 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 key
    You 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?

  5. 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:

    • --sign wraps the file and the signature together in one OpenPGP message;
    • --clearsign keeps the text readable with the signature appended below it;
    • --detach-sign writes 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.gpg pattern 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 SHA256SUMS
    08f3d276b24f9fa61486be6dfd6cccc0fabf38595a40deb54d6e2fffe42fa3d4  creds.txt
    c8fa9725389173bc8cc35538d7cd7977f68ee44fa1178748cb0c4be32449ca77  creds.txt.gpg
    A manifest of two files, exactly as in the hashing lesson. The .gpg file'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.asc next to the manifest and leaves the manifest itself unchanged. Without --armor you get a binary SHA256SUMS.sig instead; 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/SHA256SUMS
    gpg: 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/SHA256SUMS
    gpg: 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.sig against X, the .sig must be the detached form.

    Check yourself

    Which gpg option writes a signature to its own file and leaves the signed file untouched?

  6. 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.asc
    gpg: 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/SHA256SUMS
    gpg: 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 key with exit 2. They can see which key fingerprint made the signature, and nothing else.

    gpgv on 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 --keyring the 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 key and exit 2. Name the key you trust with gpgv --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?

  7. 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 the using EDDSA key lines. 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.gpg instead turns it into No 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's signed-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?

  8. 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 --batch and --passphrase '…' from any of them to see the loopback prompt instead. Then do the labs: signature-verify (Ubuntu box, gpgv only) 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 1y
    A 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.net
    Find 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.txt
    A detached signature, note.txt.sig. -u picks 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.txt
    Should 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.gpg
    A symmetric round trip: encrypt with a passphrase, decrypt it again.
Recap
  • 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 → what it does / what to watch for
CommandDoesWatch for
gpg --pinentry-mode loopback --quick-generate-key 'Name <email>'New key pairNote the fingerprint; back up the .rev file
gpg -k / gpg -K / gpg --fingerprintList public / secret keysIdentify keys by full fingerprint
gpg -c FILE / gpg -d FILE.gpgSymmetric encrypt / decryptSend the passphrase by another channel; remove the plaintext
gpg --armor --export ID / gpg --show-keys F / gpg --import FShare, inspect and add public keysNever --export-secret-keys to anyone
gpg --lsign-key IDCertify a key you have verifiedOnly after the fingerprint matched out of band
gpg -e -r FPR -o OUT FILEEncrypt to a recipientAdd -r yourself to keep a readable copy
gpg --detach-sign FILE / gpg --verify SIG FILESign / verifyRead the verdict and the fingerprint, not just the exit code
gpgv --keyring ./key.gpg SIG FILEVerify against exactly one keyringThe keyring is your whole trust decision