Skip to content
ecdsa.com

SSH vs GPG commit signing in Git: which key should sign your commits

Both put a signature into the commit object that Git and code forges can verify. SSH signing reuses keys developers already have; OpenPGP brings expiry, revocation and a trust model of its own.

SSH vs GPG commit signing is the choice of which key Git uses when you run git commit -S or git tag -s. With OpenPGP (the default, usually GnuPG), Git stores an OpenPGP signature in the commit's gpgsig header. With SSH (gpg.format=ssh, Git 2.34 and later), Git calls ssh-keygen -Y sign and stores an SSHSIG signature in the same place. Either way the signature covers the commit object, which names the tree, the parents, the author and the message, so one signature authenticates the whole history up to that commit.

At a glance

SSH signingOpenPGP (GPG) signing
Git supportGit 2.34 (November 2021), gpg.format=sshgit commit -S since Git 1.7.9 (2012); signed tags much earlier
Signing toolssh-keygen -Y sign, OpenSSH 8.1 or later (avoid 8.7)gpg or another OpenPGP implementation
Signature formatSSHSIG with namespace git, armored BEGIN SSH SIGNATUREOpenPGP signature packet, armored BEGIN PGP SIGNATURE
Size in the commit object (Ed25519, measured)306 bytes279 bytes
Who is trustedKeys listed in gpg.ssh.allowedSignersFileKeys in the keyring with sufficient owner trust
Expiry and rotationvalid-after and valid-before per key in allowed signers (OpenSSH 8.8+)Expiry date inside the key, extendable; subkeys
Revocationgpg.ssh.revocationFile (key list or KRL)Revocation certificate published with the key
Hardware keysFIDO2 keys (ed25519-sk, ecdsa-sk), agents backed by secure enclavesOpenPGP smart cards and security keys with an OpenPGP applet
Certificate authoritiesSSH certificates through cert-authority linesNone; identities certified by other keys
GitHub "Verified"Since 23 August 2022Since April 2016
GitLab "Verified"Since 15.7 (December 2022), generally available in 15.8Yes

Signing and verifying both ways

The script below creates an SSH key and an OpenPGP key for the same identity in a temporary directory, signs one commit with each and verifies both (Git 2.54, OpenSSH 10.3, GnuPG 2.5.21). Global Git configuration is disabled so nothing on your machine changes.

One repository, one SSH-signed and one OpenPGP-signed commit
#!/bin/sh
base=$(pwd)
dir=$(mktemp -d "$base/commitdemo.XXXXXX")
cd "$dir" || exit 1
GNUPGHOME=$(mktemp -d /tmp/gpg.XXXXXX)   # short path: gpg-agent sockets need one
export GNUPGHOME GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
export GIT_AUTHOR_NAME=Alice GIT_AUTHOR_EMAIL=alice@example.org
export GIT_COMMITTER_NAME=Alice GIT_COMMITTER_EMAIL=alice@example.org

# One SSH key with an allowed_signers entry, one OpenPGP key
ssh-keygen -q -t ed25519 -N '' -C alice@example.org -f alice
echo "alice@example.org namespaces=\"git\" $(cat alice.pub)" > allowed_signers
gpg --batch --quiet --passphrase '' --quick-gen-key "Alice <alice@example.org>" ed25519 sign 1y
fpr=$(gpg --list-keys --with-colons alice@example.org 2>/dev/null | awk -F: '/^fpr/ { print $10; exit }')

# Two commits: the first signed with SSH, the second with OpenPGP
git init -q repo && cd repo || exit 1
git config gpg.ssh.allowedSignersFile ../allowed_signers
echo one > file && git add file
git -c gpg.format=ssh -c user.signingkey=../alice.pub commit -q -S -m "signed with SSH"
echo two >> file
git commit -q -a -S"$fpr" -m "signed with OpenPGP" 2>/dev/null

for c in HEAD~1 HEAD; do
  echo "== $(git log -1 --format=%s "$c")"
  git verify-commit "$c" 2>&1 | grep -E "Good|using"
  git cat-file commit "$c" | awk '/^gpgsig/,/END/' > sig.txt
  echo "$(head -1 sig.txt | cut -c8-) ($(wc -c < sig.txt | tr -d ' ') bytes in the commit object)"
done
gpgconf --kill gpg-agent
cd "$base" && rm -rf "$dir" "$GNUPGHOME"
Output
== signed with SSH
Good "git" signature for alice@example.org with ED25519 key SHA256:7z1vKkx5UJmp2Qrc5ucSDqImz8+r8pfG21roc32f/2A
-----BEGIN SSH SIGNATURE----- (306 bytes in the commit object)
== signed with OpenPGP
gpg:                using EDDSA key 6A4E1CC8308072568C9E240CDDBB1B4BE59BFC00
gpg: Good signature from "Alice <alice@example.org>" [ultimate]
-----BEGIN PGP SIGNATURE----- (279 bytes in the commit object)

The SSH verification names the principal from allowed_signers and the key fingerprint; the OpenPGP verification names the user ID and reports trust from the keyring. The sizes are similar because both carry one Ed25519 signature; SSHSIG also embeds the full public key, OpenPGP only its fingerprint. The SSH signature verifier and OpenPGP inspector decode either block from git cat-file commit.

Deciding whose signatures count

A valid signature only matters if the verifier knows which keys belong to whom. With SSH, that mapping is the allowed_signers file: one line per principal and key, optionally restricted to the git namespace and to a validity window. Git checks valid-after and valid-before against the time the signature was made, so a retired key can stay in the file with an end date and old commits remain verifiable. A line with cert-authority trusts every key certified by an organization's SSH certificate authority, which suits companies that already issue short-lived SSH certificates. The file can live in the repository, so changes to it are themselves reviewed and signed.

With OpenPGP, the mapping lives in each verifier's keyring and trust database, which is why CI systems and forges import keys explicitly. The web of trust can connect strangers but is rarely used outside long-standing communities. Keys carry their own expiry dates and revocation certificates, and subkeys let a developer keep the primary key offline and rotate signing subkeys without changing identity.

Forges add their own layer: GitHub and GitLab show "Verified" when the signing key is registered to the account of the committer's verified e-mail address. That badge depends on the forge's records; GitHub's persistent verification, in preview since November 2024, stores the result at push time so later key removal does not change it.

When to choose which

  • Individual developers on GitHub or GitLab: SSH, ideally with an ed25519-sk key on a security key, so signing needs a touch.
  • Teams and companies: SSH with an allowed_signers file in the repository, or with SSH certificates from your existing CA.
  • Projects that verify upstream tags with OpenPGP (Linux kernel, many distributions, release managers with published keys): OpenPGP.
  • Commits made in CI or by bots: neither long-lived key; use keyless signing with short-lived certificates (for example Sigstore's gitsign, see Sigstore vs GPG signing) or the forge's own bot signatures.
  • Regulated environments with X.509 smart cards: gpg.format=x509 with an S/MIME signing tool, supported since Git 2.19.

Migration notes

  1. Configure SSH signing: git config --global gpg.format ssh, user.signingkey set to your public key file (or key:: with the key), and commit.gpgsign true and tag.gpgsign true.
  2. Register the key as a signing key on your forge; GitHub and GitLab accept the same key for authentication and signing, but a separate signing key is easier to rotate.
  3. Publish allowed signers in the repository (for example .gitsigners), point gpg.ssh.allowedSignersFile at it in CI and run git verify-commit on merges; add valid-after when you add a key and valid-before when you retire one.
  4. Keep old OpenPGP public keys available to verifiers and on forge accounts, so commits signed before the switch still verify.
  5. Enforce signed commits on protected branches (GitHub rulesets, GitLab push rules) once every committer has switched.

References

All comparisons