gpg — OpenPGP Encryption & Signing#
What it is#
gpg is the command-line interface to GnuPG (GNU Privacy Guard), the free implementation of the OpenPGP standard (RFC 4880) maintained by Werner Koch and the GnuPG team since 1997. It provides public-key cryptography for files, email, and git — generating keypairs, signing artefacts, verifying signatures, and encrypting/decrypting data with strong asymmetric and symmetric ciphers. Reach for gpg when you need detached signatures (release tarballs, git commits), email encryption (PGP/MIME), or password-store backends; for modern file-only encryption with smaller keys, age is a simpler alternative.
Install#
GnuPG is preinstalled on most Linux distributions and macOS via Homebrew. The current development line is 2.5.x (2.5.19 was released April 2026 and introduces post-quantum ML-KEM support); 2.4.x went unmaintained two months after 2.5.19 landed, so plan on moving to 2.5+ for new keys. Legacy gpg1 (1.4) remains for verifying very old signatures but should not be used for new key generation.
# Debian/Ubuntu
sudo apt install gnupg
# RHEL/Fedora
sudo dnf install gnupg2
# macOS
brew install gnupg
# Verify
gpg --version
Output:
gpg (GnuPG) 2.5.19
libgcrypt 1.11.0
Copyright (C) 2026 g10 Code GmbH
Home: /home/alice/.gnupg
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA, KYBER
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, ...
AEAD: EAX, OCB
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2
Syntax#
GPG dispatches by long flags rather than subcommands. Output goes to stdout by default unless redirected with -o; --armor switches binary output to base64-armoured text for transport over email or pasted into a web form.
gpg [OPTIONS] --<action> [ARGS...]
gpg --gen-key # interactive
gpg --encrypt -r ALICE FILE
gpg --decrypt FILE.gpg
gpg --sign FILE
gpg --verify FILE.sig FILE
Output: (none — exits 0 on success)
Essential options#
| Flag | Meaning |
|---|---|
-a / --armor | ASCII-armoured (base64) output instead of binary |
-o FILE / --output FILE | Write to FILE instead of stdout |
-r USER / --recipient USER | Encrypt for this public key |
-u USER / --local-user USER | Sign with this secret key |
-s / --sign | Make a signature |
-b / --detach-sign | Make a detached signature (separate .sig) |
--clearsign | Sign a text file, leaving the body human-readable |
-e / --encrypt | Encrypt for one or more -r recipients |
-c / --symmetric | Symmetric encryption (passphrase, no keypair) |
-d / --decrypt | Decrypt (also verifies if signed) |
--verify | Verify a signature only |
--list-keys / -k | List public keys |
--list-secret-keys / -K | List secret keys |
--fingerprint | Show key fingerprints |
--export / --export-secret-keys | Print key to stdout |
--import | Import a key from stdin or file |
--edit-key USER | Open the interactive key editor |
--delete-key / --delete-secret-key | Remove a key from the keyring |
The GnuPG home directory#
GnuPG stores keys, the trust database, and the agent socket in ~/.gnupg/ (overridable with GNUPGHOME or --homedir). Treat this directory as sensitive — back it up before any destructive operation.
ls -la ~/.gnupg/
GNUPGHOME=/tmp/gpgtest gpg --gen-key # isolated keyring for testing
# Back up the entire home dir before key edits
tar czf ~/gnupg-backup-$(date +%F).tar.gz -C ~ .gnupg
Output:
drwx------ 5 alice alice 4096 May 24 14:00 .
drwx------ 30 alice alice 4096 May 24 14:00 ..
-rw------- 1 alice alice 1280 May 24 14:00 gpg.conf
-rw------- 1 alice alice 9100 May 24 14:00 pubring.kbx
drwx------ 2 alice alice 4096 May 24 14:00 private-keys-v1.d
drwx------ 2 alice alice 4096 May 24 14:00 openpgp-revocs.d
-rw------- 1 alice alice 1600 May 24 14:00 trustdb.gpg
Generate a keypair#
--full-generate-key walks through every option — algorithm, key size, expiry, identity. For most users the safer-by-default --quick-gen-key produces a modern Ed25519 signing key with an ECDH encryption subkey in one line. Always set a real expiry (1–2 years) — you can extend it later, but a key with no expiry can never be retired cleanly if you lose the secret.
# Interactive — pick algorithm, size, expiry, identity
gpg --full-generate-key
# One-shot: modern ed25519 keypair, 2-year expiry
gpg --quick-gen-key "Alice Dev <alice@example.com>" ed25519 default 2y
# Batch (unattended) — useful in scripts
cat > keyparams.conf <<EOF
%echo Generating Alice Dev key
Key-Type: EDDSA
Key-Curve: ed25519
Subkey-Type: ECDH
Subkey-Curve: cv25519
Name-Real: Alice Dev
Name-Email: alice@example.com
Expire-Date: 2y
Passphrase: change-me
%commit
%echo Done
EOF
gpg --batch --gen-key keyparams.conf
Output:
gpg: key 9F1B2C3D4E5F6789 marked as ultimately trusted
gpg: revocation certificate stored as '/home/alice/.gnupg/openpgp-revocs.d/A1B2...rev'
public and secret key created and signed.
pub ed25519 2026-05-24 [SC] [expires: 2028-05-24]
A1B2C3D4E5F6789012345678901234567890ABCD
uid Alice Dev <alice@example.com>
sub cv25519 2026-05-24 [E] [expires: 2028-05-24]
List, fingerprint, and identify keys#
Keys are referenced by fingerprint (40 hex chars), long ID (last 16), short ID (last 8 — avoid; collisions exist), or user ID substring. Always prefer fingerprints for anything important.
gpg --list-keys # all public keys
gpg --list-keys alice@example.com # by user ID substring
gpg --list-secret-keys # secret keys you hold
gpg --fingerprint alice@example.com # show fingerprint
gpg --list-keys --keyid-format=long # show 16-char long IDs
# Machine-readable
gpg --list-keys --with-colons | awk -F: '/^pub/ {print $5}'
Output:
/home/alice/.gnupg/pubring.kbx
------------------------------
pub ed25519 2026-05-24 [SC] [expires: 2028-05-24]
A1B2 C3D4 E5F6 7890 1234 5678 9012 3456 7890 ABCD
uid [ultimate] Alice Dev <alice@example.com>
sub cv25519 2026-05-24 [E] [expires: 2028-05-24]
Export and import keys#
--export writes the public key; --export-secret-keys writes the secret half (treat this output as highly sensitive). -a produces ASCII-armoured output that’s safe to paste into emails or commits.
# Export public key
gpg --export -a alice@example.com > alice.pub.asc
# Export secret key (BACK UP THIS FILE, then store it offline)
gpg --export-secret-keys -a alice@example.com > alice.sec.asc
# Export revocation certificate (run this immediately after key gen)
gpg --gen-revoke -a alice@example.com > alice.revoke.asc
# Import a key
gpg --import bob.pub.asc
# Import from URL
curl -s https://example.com/release.pub.asc | gpg --import
Output:
gpg: key B0B1B2B3B4B5B6B7: public key "Bob Smith <bob@example.com>" imported
gpg: Total number processed: 1
gpg: imported: 1
Sign a file#
A signature proves the signer held the corresponding secret key at the time of signing. There are three flavours: inline (the original --sign, wraps the data in an OpenPGP packet), clear-signed (text remains readable), and detached (signature lives in a separate .sig / .asc file — the canonical form for release artefacts).
# Inline (binary OpenPGP message containing the original)
gpg --sign report.txt # produces report.txt.gpg
# Clear-signed text — body is still readable
gpg --clearsign release-notes.md # produces release-notes.md.asc
# Detached binary signature
gpg --detach-sign archive.tar.gz # produces archive.tar.gz.sig
# Detached ASCII signature (typical for tarballs distributed on the web)
gpg --detach-sign --armor archive.tar.gz # produces archive.tar.gz.asc
# Sign with a specific key
gpg -u alice@example.com -ab archive.tar.gz
Output:
$ ls archive.tar.gz*
archive.tar.gz
archive.tar.gz.asc
Verify a signature#
--verify checks a signature against the original file. For detached signatures, pass both files; GPG prints the signer’s identity and a Good signature or BAD signature verdict.
# Detached
gpg --verify archive.tar.gz.asc archive.tar.gz
# Inline / clearsigned (extract & verify in one go)
gpg --decrypt release-notes.md.asc # prints body, exits 0 if valid
# Just the verify step on an inline file
gpg --verify report.txt.gpg
Output:
gpg: Signature made Sun May 24 14:00:00 2026 UTC
gpg: using EDDSA key A1B2C3D4E5F6789012345678901234567890ABCD
gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
# BAD signature output:
gpg: BAD signature from "Alice Dev <alice@example.com>" [ultimate]
The exit code is 0 on a good signature and non-zero otherwise — fine to use in scripts.
if gpg --verify --quiet archive.tar.gz.asc archive.tar.gz 2>/dev/null; then
echo "signature OK"
else
echo "signature FAILED"; exit 1
fi
Output: (none — exits 0 on success)
Encrypt for a recipient#
--encrypt -r ID encrypts a file so that only the holder of the recipient’s secret key can decrypt it. Multiple -r flags add multiple recipients (each gets a copy of the session key). Combine with --sign to make the message both confidential and authenticated.
# Encrypt for Bob; only Bob can decrypt
gpg --encrypt -r bob@example.com message.txt
# Encrypt + sign (recommended) — Bob can verify it really came from Alice
gpg --encrypt --sign -r bob@example.com -u alice@example.com message.txt
# Multiple recipients (yourself + Bob, so you can read it later)
gpg -e -r bob@example.com -r alice@example.com message.txt
# ASCII-armoured for pasting into email
gpg -ea -r bob@example.com message.txt # produces message.txt.asc
Output:
$ ls message.txt*
message.txt
message.txt.gpg # binary form
message.txt.asc # ASCII-armoured form
Decrypt#
--decrypt (or just -d, or passing the encrypted file as input) prints the plaintext to stdout. Use -o to write to a file. If the message was signed, GPG verifies the signature automatically and reports it alongside the decryption.
# Decrypt to stdout
gpg --decrypt message.txt.gpg
# Decrypt to a file
gpg -o message.txt -d message.txt.gpg
# Decrypt a signed-and-encrypted file (verifies sig too)
gpg -d secrets.gpg
Output:
gpg: encrypted with 256-bit ECDH key, ID 0123456789ABCDEF, created 2026-05-24
"Alice Dev <alice@example.com>"
gpg: Signature made Sun May 24 14:00:00 2026 UTC
gpg: using EDDSA key A1B2C3D4E5F6789012345678901234567890ABCD
gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
<plaintext body here>
Symmetric (passphrase-only) encryption#
-c / --symmetric skips the recipient model entirely — the file is encrypted with a passphrase you type interactively. Good for “send a single file to one person over an untrusted channel”; bad for multi-recipient or long-term storage.
gpg -c archive.tar # produces archive.tar.gpg
gpg -ca archive.tar # ASCII-armoured
# Choose the cipher (AES256 is the modern default; spell it out for old GPG)
gpg --cipher-algo AES256 -c archive.tar
# Non-interactive (for scripts) — read passphrase from file
gpg --batch --passphrase-file pw.txt -c archive.tar
Output:
$ ls archive.tar*
archive.tar
archive.tar.gpg
Edit a key — expiry, subkeys, UIDs#
--edit-key opens an interactive sub-shell where you can extend expiry, add or revoke subkeys, add new user IDs, change the passphrase, and trust other keys. Type help at the prompt to see all sub-commands.
gpg --edit-key alice@example.com
Output:
gpg> expire # extend the primary key's expiry
gpg> key 1 # select subkey 1
gpg> expire # extend it too
gpg> adduid # add an additional name/email
gpg> passwd # change the passphrase
gpg> trust # set trust level (1..5)
gpg> save # commit and exit
# Non-interactive: extend expiry by 2 years
gpg --quick-set-expire ALICE_FPR 2y
# Revoke a key (after losing the secret, use the saved revocation cert)
gpg --import alice.revoke.asc
Output:
gpg: key A1B2C3D4E5F6789012345678901234567890ABCD: "Alice Dev <alice@example.com>" revoked
Trust model#
GPG marks each known key with a trust level. The keys you generated yourself are ultimate; everything else starts unknown until you certify it. The default “web of trust” model only validates a key if it’s been signed by enough keys you trust.
| Level | Meaning |
|---|---|
unknown | No information |
never | Don’t trust this key’s signatures on other keys |
marginal | Trust signatures partially (n marginal = 1 full) |
full | Trust this key to certify others |
ultimate | Same as full, used for your own keys |
# Sign someone's key after verifying their fingerprint in person/by video
gpg --sign-key bob@example.com
# Set trust without signing (private decision)
gpg --edit-key bob@example.com
gpg> trust
gpg> 4 # I trust fully
gpg> save
# Disable trust checks entirely (TOFU model — Trust On First Use)
gpg --trust-model=tofu --verify file.asc file
Output:
gpg: 3 marginal(s) needed, 1 complete(s) needed, classic trust model
gpg: depth: 0 valid: 1 signed: 1 trust: 0-, 0q, 0n, 0m, 0f, 1u
Keyservers#
Public keyservers distribute keys by ID. Modern best practice points to keys.openpgp.org (validates email ownership before publishing) rather than the old SKS pool.
# Set default keyserver (in ~/.gnupg/gpg.conf)
echo 'keyserver hkps://keys.openpgp.org' >> ~/.gnupg/gpg.conf
# Publish your public key
gpg --send-keys A1B2C3D4E5F6789012345678901234567890ABCD
# Search for a key
gpg --search-keys alice@example.com
# Fetch a known key by fingerprint
gpg --recv-keys A1B2C3D4E5F6789012345678901234567890ABCD
# Refresh all local keys (catch revocations)
gpg --refresh-keys
Output:
gpg: sending key A1B2C3D4E5F6789012345678901234567890ABCD to hkps://keys.openpgp.org
gpg: key A1B2C3D4E5F6789012345678901234567890ABCD: public key "Alice Dev <alice@example.com>" imported
gpg: Total number processed: 1
gpg: imported: 1
Sign git commits and tags#
Git can shell out to gpg to sign commits, tags, and merges; GitHub, GitLab, and Gitea display a “Verified” badge for valid signatures whose key you’ve uploaded to your profile.
# 1. Tell git which key to use
gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey A1B2C3D4E5F67890
# 2. Sign every commit automatically
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# 3. (macOS only) tell gpg to use a TTY-aware pinentry
echo "use-agent" >> ~/.gnupg/gpg.conf
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent
# 4. Make a signed commit / tag explicitly
git commit -S -m "Signed commit"
git tag -s v1.0 -m "Release 1.0"
# Inspect signatures on the log
git log --show-signature
git tag -v v1.0
Output:
commit a1b2c3d4 (HEAD -> main)
gpg: Signature made Sun May 24 14:00:00 2026 UTC
gpg: using EDDSA key A1B2C3D4E5F6789012345678901234567890ABCD
gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
Author: Alice Dev <alice@example.com>
Date: Sun May 24 14:00:00 2026 +0000
Signed commit
Upload gpg --armor --export alice@example.com to your Git host’s “GPG keys” settings page so signatures verify on the web UI.
gpg-agent and pinentry#
gpg-agent caches your passphrase between invocations so you don’t retype it constantly, and delegates passphrase prompts to a pinentry-* program (curses, GTK, Qt, or mac). Tune the cache time in ~/.gnupg/gpg-agent.conf (see Configuration below).
# Reload agent after changing config
gpgconf --kill gpg-agent
gpgconf --launch gpg-agent
# Status
gpg-connect-agent 'keyinfo --list' /bye
Output:
S KEYINFO A1B2C3D4... D - - P - - -
OK
Configuration#
GnuPG reads three plain-text config files under ~/.gnupg/ (or $GNUPGHOME). Each is loaded by a different component, so options must go in the right file or they’re silently ignored. Settings apply on next invocation for gpg.conf, after gpgconf --reload gpg-agent for gpg-agent.conf, and after gpgconf --reload dirmngr for dirmngr.conf.
| File | Read by | Purpose |
|---|---|---|
~/.gnupg/gpg.conf | gpg | Default flags, algorithm preferences, keyserver, output formatting |
~/.gnupg/gpg-agent.conf | gpg-agent | Passphrase cache TTL, pinentry binary, SSH support |
~/.gnupg/dirmngr.conf | dirmngr | Keyserver, HKP/HKPS options, Tor routing, CRL fetching |
gpg.conf — sensible defaults#
These defaults prefer modern algorithms, suppress noisy headers, and avoid leaking the short-ID footgun.
# ~/.gnupg/gpg.conf
keyid-format 0xlong
with-fingerprint
no-emit-version
no-comments
personal-cipher-preferences AES256 AES192 AES
personal-digest-preferences SHA512 SHA384 SHA256
personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed
default-preference-list SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed
cert-digest-algo SHA512
s2k-cipher-algo AES256
s2k-digest-algo SHA512
s2k-mode 3
s2k-count 65011712
keyserver hkps://keys.openpgp.org
# GnuPG 2.5+: opt in to OCB AEAD for symmetric encryption
use-ocb-sym
gpg-agent.conf — cache and pinentry#
# ~/.gnupg/gpg-agent.conf
default-cache-ttl 3600 # 1 hour after last use
max-cache-ttl 28800 # 8 hours absolute max
pinentry-program /usr/bin/pinentry-curses
# Uncomment to make gpg-agent serve SSH keys too:
# enable-ssh-support
dirmngr.conf — keyserver and network#
dirmngr is the daemon that actually talks to keyservers and downloads CRLs; gpg --send-keys and gpg --recv-keys are thin clients that hand the request to it. Override the keyserver here (or in gpg.conf) and tunnel over Tor for metadata privacy.
# ~/.gnupg/dirmngr.conf
keyserver hkps://keys.openpgp.org
hkp-cacert /etc/ssl/certs/ca-certificates.crt
# Route all keyserver traffic through Tor (requires tor running on 9050):
# use-tor
# Tune timeouts for flaky networks:
connect-timeout 15
http-timeout 30
# Reload daemons after edits
gpgconf --reload gpg-agent
gpgconf --reload dirmngr
# Show everything gpgconf knows about
gpgconf --list-options gpg | head
gpgconf --check-options gpg-agent
Output: (none — exits 0 on success)
The 2026 OpenPGP landscape: LibrePGP vs RFC 9580#
OpenPGP’s standardisation has split. After years of stalemate, the IETF published RFC 9580 (“OpenPGP”, July 2024) defining a v6 key format with mandatory AEAD (authenticated encryption) and obsoleting RFC 4880. GnuPG’s maintainer Werner Koch declined to implement v6 and instead drives LibrePGP — a competing successor based on the older crypto-refresh draft, defining its own incompatible v5 key format. Proton, Sequoia-PGP, and the IETF back RFC 9580; GnuPG ships LibrePGP. The two ecosystems generate keys that the other side cannot fully consume, so most deployments still use the v4 format from RFC 4880 to maximise interoperability.
# Inspect a key's packet version (v4 / v5 / v6)
gpg --list-packets alice.pub.asc | grep -E 'version|public key packet'
Output:
:public key packet:
version 4, algo 22, created 1716566400, expires 0
Post-quantum cryptography is landing#
GnuPG 2.5.19 (April 2026) was the first stable release with post-quantum encryption — specifically ML-KEM (Kyber, FIPS-203) as a key-encapsulation mechanism, exposed as new Pubkey: KYBER and used in composite (classical + PQ) subkeys following the draft-ietf-openpgp-pqc IETF draft. The 2.4 series went unmaintained two months after 2.5.19 shipped. Sequoia-PGP gained PQ support in late 2025 (also via composite ML-KEM subkeys), so cross-implementation interop for PQ messages is now possible but still draft-stage.
# GnuPG 2.5+: generate a key with a post-quantum encryption subkey
gpg --full-generate-key --expert # pick "(13) ECC + Kyber" or similar
Output: (none — opens interactive prompt)
Sequoia-PGP and sq as a modern alternative#
Sequoia-PGP is a Rust reimplementation of OpenPGP led by ex-GnuPG developers. Its CLI, sq, is a clean redesign with git-style subcommands (sq key generate, sq encrypt, sq sign) and uses RFC 9580 v6 keys by default. Two related projects let you opt out of GnuPG without losing tooling compatibility:
sqv— a tiny signature-verifier; Debian’saptuses it by default to verify archive signatures on most architectures.sequoia-chameleon-gnupg(Debian packagegpg-from-sq) — a drop-in replacement for thegpgbinary that speaks the GnuPG CLI but runs on Sequoia underneath. Installing it diverts the realgpgtogpg-g10code.
# Try sq alongside gpg (Debian/Ubuntu)
sudo apt install sq sqv
sq key generate --userid "Alice Dev <alice@example.com>" --output alice-sq.key
sq inspect alice-sq.key
# Drop-in replacement for /usr/bin/gpg
sudo apt install gpg-from-sq
gpg --version # now reports Sequoia's gpg-sq
Output:
gpg-sq (sequoia-chameleon-gnupg 0.x.y) ...
A reimplementation of the gpg interface using Sequoia.
Reach for sq (or age) for new greenfield deployments, but keep gpg for anything that needs OpenPGP v4 interoperability — email plugins, legacy release pipelines, and the long tail of distros that haven’t migrated yet.
Common pitfalls#
- Short IDs collide. Never reference a key by an 8-char short ID; collisions have been demonstrated. Use the full fingerprint, or at minimum the 16-char long ID (
--keyid-format=long). - No revocation certificate stored. Generate it the moment you create a keypair (
--gen-revoke) and stash it offline — without it, a lost secret key can never be repudiated. - Encrypting for only the recipient. If you encrypt a file with
-r bob@example.comand don’t add-r yourself, you can’t read it later. Add yourself as a second recipient for archival files. --symmetric+ bad passphrase. GnuPG won’t tell you the passphrase is wrong, just that it can’t decrypt. Test the passphrase by decrypting immediately after encryption.pinentryhangs in CI. Set--batch --pinentry-mode loopbackand pass--passphraseor--passphrase-file. Otherwise GPG waits forever for a TTY that doesn’t exist.- macOS commits show “unverified”. Install
pinentry-macvia Homebrew and pointgpg-agent.confat it, or commits will fail silently because the GUI passphrase prompt can’t show. - Old DSA/1024-bit keys. Anything generated before 2018 with DSA-1024 or RSA-1024 is obsolete. Migrate to Ed25519 / RSA-4096 and revoke the old key.
--allow-secret-key-importis gone in 2.x. Just use--import.
Real-world recipes#
Produce and verify a detached signature for a release#
The most common open-source release flow — ship the artefact plus a .asc signature, document the signing key’s fingerprint on the project page.
# Sign
gpg --detach-sign --armor -u alice@example.com release-1.0.tar.gz
sha256sum release-1.0.tar.gz > release-1.0.tar.gz.sha256
# What the consumer runs
gpg --recv-keys A1B2C3D4E5F6789012345678901234567890ABCD
gpg --verify release-1.0.tar.gz.asc release-1.0.tar.gz
sha256sum -c release-1.0.tar.gz.sha256
Output:
gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
release-1.0.tar.gz: OK
Encrypt a backup directory for offsite storage#
Tar + GPG produces a single encrypted blob you can ship to any storage backend.
tar czf - /home/alice/photos \
| gpg --encrypt -r alice@example.com -o photos-$(date +%F).tar.gz.gpg
# Restore later
gpg -d photos-2026-05-24.tar.gz.gpg | tar xzf - -C /tmp/restore
Output:
$ ls -lh photos-2026-05-24.tar.gz.gpg
-rw------- 1 alice alice 1.4G May 24 14:00 photos-2026-05-24.tar.gz.gpg
Verify a downloaded tarball without trusting any global keyring#
Use a per-project GNUPGHOME so the project’s signing key doesn’t pollute your personal trust db.
PROJECT_HOME=$(mktemp -d)
GNUPGHOME=$PROJECT_HOME gpg --import upstream-release.pub.asc
GNUPGHOME=$PROJECT_HOME gpg --verify release-1.0.tar.gz.asc release-1.0.tar.gz
rm -rf "$PROJECT_HOME"
Output:
gpg: key A1B2...ABCD: public key "Upstream Release <release@example.org>" imported
gpg: Good signature from "Upstream Release <release@example.org>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
Sign apt repositories#
Most distros verify package indexes with GPG. To host an apt repo you sign the Release file and publish the public key for clients to install in /etc/apt/trusted.gpg.d/.
# Sign the Release file (creates Release.gpg detached + InRelease inline)
gpg --default-key alice@example.com -abs -o Release.gpg Release
gpg --default-key alice@example.com --clearsign -o InRelease Release
# Publish the signer's public key
gpg --export -a alice@example.com > /var/www/repo/keyring.asc
Output:
$ ls Release*
Release InRelease Release.gpg
Encrypt a one-off file for a recipient you just met#
You have their public key on a keyserver but no signed trust path yet. --trust-model=always skips the trust check for this one operation.
gpg --recv-keys 1234567890ABCDEF
gpg --trust-model=always -e -r 1234567890ABCDEF message.txt
Output:
$ ls message.txt*
message.txt
message.txt.gpg
Re-encrypt every file in a directory after rotating recipients#
When someone leaves the team you re-issue the key list. Loop over each .gpg file, decrypt, re-encrypt with the new recipient set.
NEW_RECIPS=("-r alice@example.com" "-r charlie@example.com")
for f in *.gpg; do
base="${f%.gpg}"
gpg -d "$f" | gpg -e ${NEW_RECIPS[@]} -o "$base.new.gpg"
mv "$base.new.gpg" "$f"
done
Output:
$ ls *.gpg | wc -l
12
CI: verify a release artefact with no interactivity#
Useful inside Docker build steps or GitHub Actions. The --batch --pinentry-mode loopback combination silences passphrase prompts; the key here has no passphrase since it only verifies, never signs.
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
curl -fsSL https://example.com/release.pub.asc | gpg --batch --import
gpg --batch --verify release-1.0.tar.gz.asc release-1.0.tar.gz
Output:
gpg: Good signature from "Upstream Release <release@example.org>"
Rotate (extend) an expired key without losing the identity#
When your primary key reaches its expiry, extend it before key consumers fall off the trust path.
gpg --quick-set-expire A1B2C3D4E5F6789012345678901234567890ABCD 2y
# Extend each subkey too
for sub in $(gpg --list-keys --with-subkey-fingerprints alice@example.com \
| awk '/^fpr:::::::::[A-F0-9]+:/' RS=':' | sed -n 2~1p); do
gpg --quick-set-expire A1B2C3D4E5F6789012345678901234567890ABCD 2y "$sub"
done
# Republish
gpg --send-keys A1B2C3D4E5F6789012345678901234567890ABCD
Output:
gpg: sending key A1B2C3D4E5F6789012345678901234567890ABCD to hkps://keys.openpgp.org
[!TIP] Add
--always-trustto encryption commands only when you’ve manually verified the recipient out-of-band (phone call, in-person fingerprint check). Bypassing the trust db should be a deliberate choice, not the default.
[!TIP] For modern file-only encryption between machines you control,
age(github.com/FiloSottile/age) is dramatically simpler — small keys, no web of trust, one binary. Usegpgwhen you need OpenPGP interoperability (email, package signing, git).
Sources#
- GnuPG 2.5.19 release announcement — post-quantum encryption
- draft-ietf-openpgp-pqc — Post-Quantum Cryptography in OpenPGP (IETF)
- RFC 9580 — OpenPGP (IETF, July 2024)
- OpenPGP updates, LibrePGP and RFC 9580 — DidiSoft
- Sequoia-PGP — homepage and project overview
- Post-Quantum Cryptography in Sequoia PGP (Nov 2025)
- sq feature comparison with gpg — Sequoia blog
- Sequoia PGP, sq, gpg-from-sq, v6 OpenPGP, and Debian — DebConf 24
- OpenPGP/Sequoia — Debian Wiki
- gpg-from-sq package — Debian sid