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 signing | OpenPGP (GPG) signing | |
|---|---|---|
| Git support | Git 2.34 (November 2021), gpg.format=ssh | git commit -S since Git 1.7.9 (2012); signed tags much earlier |
| Signing tool | ssh-keygen -Y sign, OpenSSH 8.1 or later (avoid 8.7) | gpg or another OpenPGP implementation |
| Signature format | SSHSIG with namespace git, armored BEGIN SSH SIGNATURE | OpenPGP signature packet, armored BEGIN PGP SIGNATURE |
| Size in the commit object (Ed25519, measured) | 306 bytes | 279 bytes |
| Who is trusted | Keys listed in gpg.ssh.allowedSignersFile | Keys in the keyring with sufficient owner trust |
| Expiry and rotation | valid-after and valid-before per key in allowed signers (OpenSSH 8.8+) | Expiry date inside the key, extendable; subkeys |
| Revocation | gpg.ssh.revocationFile (key list or KRL) | Revocation certificate published with the key |
| Hardware keys | FIDO2 keys (ed25519-sk, ecdsa-sk), agents backed by secure enclaves | OpenPGP smart cards and security keys with an OpenPGP applet |
| Certificate authorities | SSH certificates through cert-authority lines | None; identities certified by other keys |
| GitHub "Verified" | Since 23 August 2022 | Since April 2016 |
| GitLab "Verified" | Since 15.7 (December 2022), generally available in 15.8 | Yes |
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.
#!/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"
== 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-skkey on a security key, so signing needs a touch. - Teams and companies: SSH with an
allowed_signersfile 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=x509with an S/MIME signing tool, supported since Git 2.19.
Migration notes
- Configure SSH signing:
git config --global gpg.format ssh,user.signingkeyset to your public key file (orkey::with the key), andcommit.gpgsign trueandtag.gpgsign true. - 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.
- Publish allowed signers in the repository (for example
.gitsigners), pointgpg.ssh.allowedSignersFileat it in CI and rungit verify-commiton merges; addvalid-afterwhen you add a key andvalid-beforewhen you retire one. - Keep old OpenPGP public keys available to verifiers and on forge accounts, so commits signed before the switch still verify.
- Enforce signed commits on protected branches (GitHub rulesets, GitLab push rules) once every committer has switched.
References
- Git documentation: git-config gpg.format, gpg.ssh.allowedSignersFile, gpg.ssh.revocationFile
- Git 2.34 release notes
- OpenSSH 8.1 release notes and ssh-keygen manual, ALLOWED SIGNERS
- OpenSSH PROTOCOL.sshsig
- RFC 9580, OpenPGP
- GitHub changelog: SSH commit verification
- GitLab: sign commits and tags with SSH keys