Skip to content

Git GPG and OpenPGP Commit Signing

Lesson 6 of 9Intermediate13 min readGit Security & DevSecOps · Repository SecurityVerified: GnuPG 2.4.4 and git 2.43.0 on Ubuntu 24.04; GitHub GPG key documentation, September 2026

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.

Terminal window
{/* 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_ID

Then point Git at it:

Terminal window
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
git config --global tag.gpgsign true

The 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.

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.

The quick path, which is enough for commit signing:

Terminal window
gpg --quick-generate-key "Your Name <you@example.com>" ed25519 sign 1y

What 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:

Terminal window
gpg --full-generate-key

GitHub 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.

Terminal window
gpg --list-secret-keys --keyid-format=long

What 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]
DF5907C252877E0A20F461BDBEBAD3A412503365
uid [ultimate] Demo User <demo@example.com>

Reading it:

PartMeaning
secA secret (private) key is present
ed25519The algorithm
BEBAD3A412503365The long key ID — what you give Git
[SC]Capabilities: Sign and Certify
[expires: 2027-09-01]The expiry you set
The 40-character lineThe 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.

Export the public half:

Terminal window
gpg --armor --export BEBAD3A412503365

The 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.

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:

Terminal window
gpg --edit-key BEBAD3A412503365
{/* then: adduid, follow the prompts, then save */}
Terminal window
git config --global user.signingkey BEBAD3A412503365
git config --global commit.gpgsign true
git config --global tag.gpgsign true

Since 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:

Terminal window
{/* Check what is currently in force before assuming */}
git config --get gpg.format

If GnuPG is installed as gpg2 on your distribution, point Git at it:

Terminal window
git config --global gpg.program gpg2

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.

Terminal window
{/* 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.

  1. Find it now, not later:

    Terminal window
    ls ~/.gnupg/openpgp-revocs.d/
  2. 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.

  3. When you need it, import and publish:

    Terminal window
    gpg --import ~/path/to/FINGERPRINT.rev
    gpg --keyserver hkps://keys.openpgp.org --send-keys FINGERPRINT
  4. 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.

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.

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:

Terminal window
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:

Terminal window
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.

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.

Terminal window
gpg --keyserver hkps://keys.openpgp.org --send-keys YOUR_FINGERPRINT

Web 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.

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 / OpenPGPSSH
SetupKey generation, agent configuration, pinentryOften already done
ExpiryBuilt into the key formatNone
RevocationPortable revocation certificateRemove from the platform
SubkeysYes — offline master, online signing subkeyNo equivalent
Hardware supportOpenPGP smartcards, YubiKeys-sk key types
Local verificationWorks from the keyringNeeds an allowed_signers file
Failure modespinentry, expiry, keyring stateKey registered for the wrong purpose
EcosystemPackage signing, email, keyserversGit 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.

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.

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.

  • GPG’s advantages over SSH signing are lifecycle features: expiry, portable revocation and subkeys
  • gpg --quick-generate-key sets 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 --export is public; --export-secret-keys is not, and the two look similar
  • GPG_TTY resolves 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

Use a disposable repository and a throwaway GnuPG home so nothing touches your real keyring:

Terminal window
export GNUPGHOME=$(mktemp -d) && chmod 700 "$GNUPGHOME"
  1. Generate a key with gpg --quick-generate-key "Test User <test@example.com>" ed25519 sign 1y. Predict: what does it print about revocation?

  2. Run ls "$GNUPGHOME/openpgp-revocs.d/" and confirm the certificate exists.

  3. Run gpg --list-secret-keys --keyid-format=long and identify the key ID, the fingerprint and the [SC] capabilities.

  4. Configure a local repository to sign with it and make a signed commit. Verify with git log --show-signature.

  5. Change user.email to 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.

  6. Import the revocation certificate with gpg --import and run gpg --list-keys. Predict: what changes? Then verify an existing signature again and observe that it still validates.

  7. Remove the temporary GnuPG home: rm -rf "$GNUPGHOME".

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

Get the repository security checklists — setup, rulesets, tokens — from the Professional Toolkit.