OpenPGP is the older of the two practical ways to sign Git commits, and it is frequently described as either the serious option or the obsolete one, depending on who is writing.
Neither is right. GPG has capabilities SSH signing does not — real expiry, designed revocation, subkeys — and a user experience that has kept adoption low for thirty years. This lesson covers what it actually offers, and when that is worth the cost.
The short answer
Section titled “The short answer”{/* Generate a signing key that expires in a year */}gpg --quick-generate-key "Your Name <you@example.com>" ed25519 sign 1y
{/* Find its long key ID */}gpg --list-secret-keys --keyid-format=long
{/* Export the public half to paste into GitHub */}gpg --armor --export YOUR_KEY_IDThen point Git at it:
git config --global user.signingkey YOUR_KEY_IDgit config --global commit.gpgsign truegit config --global tag.gpgsign trueThe email address in the key’s user ID must match a verified email on your GitHub account, or the signature will verify cryptographically and still show as Unverified.
What OpenPGP adds
Section titled “What OpenPGP adds”Three properties distinguish it from SSH signing, and all three are about the key’s lifecycle rather than about the signature itself.
Expiry is part of the key. An OpenPGP key carries an expiry date that clients enforce. A key set to expire in a year does so whether or not anyone remembers. You can extend it — expiry is a property you can update and re-publish — which makes it a periodic decision rather than a cliff.
An SSH key has no expiry. Nothing stops you rotating one on a schedule, but nothing prompts you either, and in practice SSH keys live for years.
Revocation is a designed mechanism. GnuPG generates a revocation certificate at key creation. Publishing it marks the key revoked in a way that other OpenPGP clients understand, independently of any single platform. Removing an SSH key from GitHub revokes it on GitHub; there is no portable statement.
Subkeys separate roles. An OpenPGP key can have a master key that certifies, and separate subkeys for signing and encryption. The master key can live offline — on an encrypted drive in a drawer — while a signing subkey lives on your laptop. If the laptop is compromised, you revoke the subkey and keep your identity and its accumulated trust.
That last one is the genuine architectural advantage, and it has no SSH equivalent.
Generating a key
Section titled “Generating a key”The quick path, which is enough for commit signing:
gpg --quick-generate-key "Your Name <you@example.com>" ed25519 sign 1yWhat it doesCreates an Ed25519 key usable for signing, with a one-year expiry, in one command.
Why we run itIt avoids the interactive prompts of --full-generate-key while still setting an expiry, which the interactive default does not.
Expected resultA revocation certificate path is printed, and the key appears in gpg --list-secret-keys.
Real output from GnuPG 2.4.4, with paths shortened:
gpg: revocation certificate stored as '.../openpgp-revocs.d/DF5907C252877E0A20F461BDBEBAD3A412503365.rev'That line is the most important thing GnuPG will tell you, and it scrolls past unread more often than not. See revocation below.
For full control — multiple user IDs, subkeys, a specific algorithm — use the interactive form:
gpg --full-generate-keyGitHub supports RSA, ElGamal, DSA, ECDH, ECDSA and EdDSA keys. Ed25519 falls under EdDSA and is a good default; RSA 4096 is the maximally interoperable choice if anything else in your toolchain has opinions.
Finding the key ID
Section titled “Finding the key ID”gpg --list-secret-keys --keyid-format=longWhat it doesLists secret keys with their long-form key IDs and full fingerprints.
Why we run itGit and GitHub both need an identifier for the key, and the short form is not unique enough to rely on.
Expected resultA sec line with algorithm and key ID, the full fingerprint beneath it, and a uid line.
Real output for the demonstration key generated above:
sec ed25519/BEBAD3A412503365 2026-09-01 [SC] [expires: 2027-09-01] DF5907C252877E0A20F461BDBEBAD3A412503365uid [ultimate] Demo User <demo@example.com>Reading it:
| Part | Meaning |
|---|---|
sec | A secret (private) key is present |
ed25519 | The algorithm |
BEBAD3A412503365 | The long key ID — what you give Git |
[SC] | Capabilities: Sign and Certify |
[expires: 2027-09-01] | The expiry you set |
| The 40-character line | The full fingerprint |
Use the full fingerprint where anything accepts it. Key IDs are a truncation of the fingerprint, and short IDs in particular are short enough that collisions have been demonstrated deliberately.
Registering with GitHub
Section titled “Registering with GitHub”Export the public half:
gpg --armor --export BEBAD3A412503365The output is a block beginning -----BEGIN PGP PUBLIC KEY BLOCK-----. Paste the whole thing,
including the header and footer lines, into the GPG keys section of your GitHub account settings.
The email address must match
Section titled “The email address must match”For GitHub to show Verified, the email address in the key’s user ID must be a verified email address on your account, and it must match the email address on the commit.
This produces the most common failure: a correctly signed commit, a correctly uploaded key, and an
Unverified badge — because git config user.email is your personal address while the key says
your work one, or the address is not verified on the account.
A key can carry multiple user IDs, which is the clean fix for people who commit under more than one address:
gpg --edit-key BEBAD3A412503365{/* then: adduid, follow the prompts, then save */}Configuring Git
Section titled “Configuring Git”git config --global user.signingkey BEBAD3A412503365git config --global commit.gpgsign truegit config --global tag.gpgsign trueSince Git 2.34, gpg.format selects the signing backend. It defaults to openpgp, so GPG needs no
explicit setting — but if you have previously configured SSH signing globally, you will need to unset
or override it:
{/* Check what is currently in force before assuming */}git config --get gpg.formatIf GnuPG is installed as gpg2 on your distribution, point Git at it:
git config --global gpg.program gpg2The pinentry problem
Section titled “The pinentry problem”The most common practical failure with GPG signing is not cryptographic. It is that gpg needs to
prompt for your passphrase, and in a terminal — especially over SSH, in a container, or from an
editor — there may be no working way to prompt.
The symptom is a commit failing with gpg failed to sign the data, or hanging.
{/* Tell gpg-agent which terminal to prompt on, for the current shell */}export GPG_TTY=$(tty)Adding that line to your shell profile resolves most cases. For headless environments, configure a pinentry program suited to the context, or use a key with no passphrase held somewhere the passphrase would not have protected anyway — a decision to make deliberately, not by accident.
Revocation is something you prepare in advance
Section titled “Revocation is something you prepare in advance”This is GPG’s genuine advantage over SSH signing, and it only works if you do one thing at creation time.
GnuPG writes a revocation certificate to openpgp-revocs.d/ when the key is generated. That file
lets you publish “this key is revoked” even if you have lost the private key or forgotten the
passphrase — which are precisely the situations in which you most need to revoke it.
-
Find it now, not later:
Terminal window ls ~/.gnupg/openpgp-revocs.d/ -
Copy it somewhere safe and offline. A password manager entry or an encrypted drive. Not the same laptop as the key, because the scenario you are preparing for includes losing that laptop.
-
When you need it, import and publish:
Terminal window gpg --import ~/path/to/FINGERPRINT.revgpg --keyserver hkps://keys.openpgp.org --send-keys FINGERPRINT -
Remove the key from GitHub as well. The revocation certificate is an OpenPGP statement; GitHub’s registration is separate, and removing it is what stops GitHub showing new signatures as verified.
Note what revocation does not do. Signatures made before the revocation still verify against the key — OpenPGP has no trusted timestamp in the commit, so “signed before or after the compromise?” cannot be answered from the signature alone. Revocation stops future trust; it does not retroactively invalidate the past.
Protecting the private key
Section titled “Protecting the private key”The private key material lives in ~/.gnupg/. Everything about protecting it follows from that.
Use a strong passphrase. It is what stands between a copied ~/.gnupg directory and your
identity.
Back it up deliberately, encrypted. Losing a signing key is not a security incident but it is disruptive — you lose the ability to sign as that identity, and any accumulated trust in it.
Consider a hardware token. OpenPGP smartcards and YubiKeys hold the private key on the device, which converts file theft into a non-event and typically requires a touch per signature.
Use subkeys if the key matters. Keep the certifying master key offline; keep only a signing subkey on the laptop. Compromise of the laptop then costs you a subkey rather than your identity.
Set ~/.gnupg permissions correctly. GnuPG warns about unsafe permissions for a reason; the
directory should be 700.
Subkeys, concretely
Section titled “Subkeys, concretely”The offline-master pattern is GPG’s strongest architectural idea and the one least often explained in practical terms.
A key generated with --quick-generate-key ... sign has capabilities [SC] — it both certifies and
signs. That single key is your identity and your day-to-day signing credential, which is exactly
what you want to separate.
The structure worth building instead:
Master key [C] Certify only. Lives offline. └── Subkey [S] Sign. Lives on the laptop.Adding a signing subkey:
gpg --edit-key YOUR_FINGERPRINT{/* then: addkey, choose a sign-only algorithm, set an expiry, then save */}Git then signs with the subkey. Reference it explicitly by appending ! to the subkey ID, which tells
GnuPG to use that key rather than choosing:
git config --global user.signingkey SUBKEY_ID!The payoff arrives on a bad day. A compromised laptop means revoking one signing subkey and issuing another. Your identity, its user IDs, any endorsements it has collected and every past signature made by other subkeys remain intact. Without subkeys, the same event costs you the whole key.
The cost is real: the master key has to live somewhere you can reach when you need to certify something, and “somewhere safe and offline” is a process, not a directory. For an individual signing their own commits this is probably more machinery than the situation warrants. For someone who signs releases that other people verify, it is proportionate.
Distributing the public key
Section titled “Distributing the public key”GitHub’s registration is enough for GitHub to show Verified. It is not enough for anyone verifying your signatures outside GitHub — someone checking a release tag from a clone, for instance.
Three routes, with different properties:
A keyserver. keys.openpgp.org is the modern default and verifies email addresses before
distributing user IDs.
gpg --keyserver hkps://keys.openpgp.org --send-keys YOUR_FINGERPRINTWeb Key Directory. The public key served over HTTPS from a well-known path on your own domain. GnuPG looks there automatically for an address at that domain, which makes it the least ceremonial option for an organisation that controls its domain.
In the repository. A KEYS file or an allowed_signers equivalent committed alongside the code.
Simple and self-contained, with the caveat that whoever can merge a change to that file can add a
key — so the file is protected by whatever protects the repository.
Whichever you choose, publish the full fingerprint wherever you tell people about the key. A short key ID is not a safe identifier; deliberate collisions on short IDs have been demonstrated.
The threat model
Section titled “The threat model”Threat. A commit or tag is signed by someone other than the identity it claims — either through a stolen private key, or by convincing verifiers to trust a key that is not yours.
Attack surface. The ~/.gnupg directory and anything that can read it. A running gpg-agent
with a cached passphrase. Backups. The distribution channel for your public key, since a verifier who
fetches the wrong key verifies the wrong signatures happily.
Impact. Commits and releases that appear to come from a trusted maintainer. For a project whose releases people verify, this is the strongest form of the attack — a signed release is exactly what downstream consumers are trained to trust.
Control. A passphrase on the key; hardware backing where the stakes justify it; subkeys so a laptop compromise is bounded; an expiry so an abandoned key stops working; a revocation certificate stored somewhere the key is not.
Verification. Confirm the revocation certificate exists and is stored off the machine. Confirm
your key has an expiry with gpg --list-keys. Confirm that the fingerprint you publish matches the
key you actually sign with — not the key ID, the fingerprint.
That last check is more useful than it sounds. The gap between “the key I sign with” and “the key people verify against” opens quietly whenever a key is rotated and a README is not.
GPG or SSH: choosing
Section titled “GPG or SSH: choosing”| GPG / OpenPGP | SSH | |
|---|---|---|
| Setup | Key generation, agent configuration, pinentry | Often already done |
| Expiry | Built into the key format | None |
| Revocation | Portable revocation certificate | Remove from the platform |
| Subkeys | Yes — offline master, online signing subkey | No equivalent |
| Hardware support | OpenPGP smartcards, YubiKeys | -sk key types |
| Local verification | Works from the keyring | Needs an allowed_signers file |
| Failure modes | pinentry, expiry, keyring state | Key registered for the wrong purpose |
| Ecosystem | Package signing, email, keyservers | Git and SSH |
Choose SSH if you want commit signing adopted across a team this quarter. The lowest-friction option that people actually use beats the more capable one they abandon.
Choose GPG if you need enforced expiry, portable revocation, offline master keys, or you already have an OpenPGP ecosystem — Linux distribution packaging, release signing conventions, an existing web of trust.
Choose neither for build artifacts. Signing a commit is about source authorship. Signing what a build produced is a different problem with a better answer — see Sigstore and artifact attestations, which avoid long-lived key management entirely.
Common mistakes
Section titled “Common mistakes”Never saving the revocation certificate. It is generated once, printed once, and is the only recovery path if you lose the key. Move it somewhere else on day one.
Key email not matching the commit email. The most frequent cause of a valid signature showing as Unverified.
No expiry. The generator’s default, and a key that outlives the laptop, the job and the memory of having created it.
Exporting the secret key. One word’s difference from the correct command, and unrecoverable once it has been pasted anywhere.
Assuming revocation reaches backwards. It stops future trust. Signatures already made still verify.
Master key on the laptop. If you are going to the trouble of using GPG, use subkeys. Otherwise a compromised laptop costs you the identity rather than a component of it.
Believing GPG is more secure than SSH signing. They produce equivalent signatures over the same object. The differences are lifecycle management, not cryptographic strength.
Mental model
Section titled “Mental model”An OpenPGP key is an identity with a lifecycle — it can expire, be revoked, delegate to subkeys and accumulate endorsements. An SSH key is a credential. That difference is the whole reason to choose one over the other; the signatures they produce mean the same thing.
What you learned
Section titled “What you learned”- GPG’s advantages over SSH signing are lifecycle features: expiry, portable revocation and subkeys
gpg --quick-generate-keysets an expiry; the interactive default does not- The revocation certificate is written at key creation and is your only recovery if the key is lost
- GitHub supports RSA, ElGamal, DSA, ECDH, ECDSA and EdDSA keys
- The key’s user ID email must be verified on your account and match the commit email
gpg --armor --exportis public;--export-secret-keysis not, and the two look similarGPG_TTYresolves most passphrase prompt failures- Revocation stops future trust and does not invalidate signatures already made
- Subkeys let a compromise cost you a component rather than the identity
- Neither GPG nor SSH is the right tool for signing build artifacts
Exercise
Section titled “Exercise”Use a disposable repository and a throwaway GnuPG home so nothing touches your real keyring:
export GNUPGHOME=$(mktemp -d) && chmod 700 "$GNUPGHOME"-
Generate a key with
gpg --quick-generate-key "Test User <test@example.com>" ed25519 sign 1y. Predict: what does it print about revocation? -
Run
ls "$GNUPGHOME/openpgp-revocs.d/"and confirm the certificate exists. -
Run
gpg --list-secret-keys --keyid-format=longand identify the key ID, the fingerprint and the[SC]capabilities. -
Configure a local repository to sign with it and make a signed commit. Verify with
git log --show-signature. -
Change
user.emailto an address the key does not carry, and commit again. Predict: does the signature still verify locally? Does GitHub show Verified? These have different answers. -
Import the revocation certificate with
gpg --importand rungpg --list-keys. Predict: what changes? Then verify an existing signature again and observe that it still validates. -
Remove the temporary GnuPG home:
rm -rf "$GNUPGHOME".
Related lessons
Section titled “Related lessons”Get the repository security checklists — setup, rulesets, tokens — from the Professional Toolkit.