Signing Commits and Supply Chain
A security auditor reviews your repo's commit history. They point out that any contributor could have committed as "root@example.com" or "Linus Torvalds" — the author field is free text, trivially forged. The auditor asks how you verify that the person shown as the author is actually the person who wrote the code. The team's first instinct: "Well, they authenticated to push." But that authentication was for network-level access, not code-level attribution. There is no cryptographic link between the author identity and the authenticated pusher — unless commits are signed.
Commit signing attaches a cryptographic signature to each commit, tying the author identity to a private key under the author's control. GitHub, GitLab, and similar platforms verify signatures and display a "Verified" badge next to signed commits. For security-sensitive code (libraries, infrastructure, release artifacts), signing converts "someone typed this name" into "this commit provably came from the holder of key X." This lesson covers how to sign (GPG and SSH), how verification works in practice, and when the effort is worth it.
Why Commit Author Fields Are Not Trustworthy
# Pretend to be someone else for a commit
git -c user.name="Linus Torvalds" -c user.email="torvalds@linux-foundation.org" \
commit -m "totally legit"
git log -1 --format='%an <%ae>'
# Linus Torvalds <torvalds@linux-foundation.org>
That is Git accepting your input at face value. The user.name and user.email are config settings, not identity proofs. Without signing, the commit's author is whatever the committer's Git config said at commit time — no verification whatsoever.
When someone pushes a commit to your server, the server verifies they have permission to push. But the server does not verify that the author field matches the pusher. A malicious insider (or compromised account) can forge any author they like.
Commit authorship without signing is a social convention, not a security guarantee. The name in git log is trustworthy only to the extent you trust everyone who had write access AND trust their devices AND trust the connection. For security-sensitive projects (cryptography, infrastructure, releases), this is not enough.
What Signing Does
A signed commit includes an extra field in the commit object:
git cat-file -p <signed-commit-sha>
# tree abc123...
# parent def456...
# author Sharon <sharon@example.com> 1713600000 +0000
# committer Sharon <sharon@example.com> 1713600000 +0000
# gpgsig -----BEGIN PGP SIGNATURE-----
#
# iQIzBAABCAAdFiEE...
# ...
# =ABCD
# -----END PGP SIGNATURE-----
#
# feat: add login
The gpgsig field (for GPG) is a cryptographic signature over the rest of the commit content, created with the author's private key. Anyone with the author's public key can verify the signature — proving that:
- The commit was signed by the holder of the corresponding private key.
- The content has not been modified since signing (any edit changes the SHA, which changes what the signature was over).
The commit's SHA still includes the gpgsig line, so signing is not a "tack-on" — it is part of the commit's identity.
Two Signing Methods
Git supports two signing mechanisms:
- GPG / OpenPGP — traditional, widely supported, complex key management.
- SSH signing (Git 2.34+) — use your existing SSH key for signing; simpler, now the preferred option for most people.
Both produce verifiable signatures. Platforms (GitHub, GitLab, Gitea) support both.
GPG signing
# 1. Install GPG
# macOS: brew install gnupg
# Linux: apt install gnupg (usually already there)
# 2. Generate a key (if you don't have one)
gpg --full-generate-key
# Choose: RSA and RSA (default), 4096 bits, 2 years expiry (recommended)
# Name: your name
# Email: your email (match your Git email)
# 3. List keys
gpg --list-secret-keys --keyid-format=long
# sec rsa4096/ABC123DEF456 2026-04-20 [SC]
# Key fingerprint = ...
# uid Sharon <sharon@example.com>
# 4. Tell Git to use it
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# 5. Sign a commit
git commit -m "signed commit"
# Enter your GPG passphrase
# Commit is signed
Verify locally:
git log --show-signature -1
# commit abc123...
# gpg: Signature made Sat Apr 20 10:00:00 2026 UTC
# gpg: using RSA key ABC123DEF456
# gpg: Good signature from "Sharon <sharon@example.com>"
To use the key on GitHub/GitLab, upload the public key to your account settings:
gpg --armor --export ABC123DEF456 | pbcopy # copy public key to clipboard
# Paste into Settings → SSH and GPG keys → New GPG key
SSH signing (Simpler)
Git 2.34+ supports signing with your SSH key — the same key you already use to push. No GPG required.
# Configure Git to use SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Sign a commit
git commit -m "ssh-signed commit"
# No passphrase prompt if your key is in ssh-agent
Verify locally needs a trusted-keys file (for the verification to succeed against your local setup):
# ~/.ssh/allowed_signers — maps email to public keys
echo "sharon@example.com namespaces=\"git\" $(cat ~/.ssh/id_ed25519.pub)" >> ~/.ssh/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
git log --show-signature -1
# Good "git" signature for sharon@example.com with ED25519 key ...
Upload the same SSH public key to GitHub/GitLab, marked as "signing key" (separate from the "authentication key" role, though often the same physical key).
SSH signing is the modern default. Simpler key management (you already have an SSH key for pushes), no GPG dependency, no passphrase prompts if ssh-agent is configured, works identically on every platform (including Windows + WSL). Unless your team has existing GPG infrastructure, start with SSH signing.
Platform Verification
Once you sign, GitHub / GitLab / Bitbucket verify the signature against the public keys you uploaded to your account.
GitHub's "Verified" badge
┌────────────────────────────────────────────────┐
│ commit abc123... Verified│ ← green badge
│ Author: Sharon committed 2 days ago │
└────────────────────────────────────────────────┘
The "Verified" badge means: GitHub checked the signature and it matches a key registered to the account that authored the commit. An unsigned or mis-signed commit shows no badge (or "Unverified" in some views).
Click "Verified" to see:
- The signing key's fingerprint.
- Whether the committer is the same account (double-check).
- For SSH keys, the key's public fingerprint.
Vigilant mode (GitHub)
In GitHub account settings → SSH and GPG keys, enable Vigilant mode. Every commit attributed to you that is NOT signed will show "Unverified" — so anyone forging your author field gets flagged.
Required signatures on branches
In GitHub branch protection, you can require that all commits to a branch be signed. Unsigned pushes are rejected. Combine with CODEOWNERS and review requirements for defense in depth.
GitLab has similar features: enabled at the project or group level.
Signed Tags
Tags can also be signed, and for releases, they almost always should be:
# Sign a tag
git tag -s v1.2.3 -m "release 1.2.3"
# Verify
git tag -v v1.2.3
# object ...
# type commit
# tag v1.2.3
# tagger Sharon <sharon@example.com> 1713600000 +0000
#
# release 1.2.3
# gpg: Signature made ...
# gpg: Good signature from ...
For release tags, signed tags are the minimum practice. Many projects use signed tags as the authoritative record of a release:
- The tag commits to (signs over) a specific commit SHA.
- The signature comes from a known maintainer's key.
- Users verifying the tag can be sure they got the exact commit the maintainer intended.
Linux kernel, Git itself, Node.js, Rust, and many other high-visibility projects sign their release tags. It is a small investment for big credibility.
Verifying Signatures
# Check a specific commit
git log --show-signature <commit>
# Only show signed commits in log
git log --show-signature
# Require verified commits on a pull (blocks unverified)
git pull --verify-signatures
# For tags
git tag --verify v1.2.3
# In scripts
if git verify-commit abc123; then
echo "valid"
else
echo "invalid"
fi
Verification requires the public key to be available. For signatures from people you trust, fetch their public keys:
# For GPG
gpg --keyserver hkps://keys.openpgp.org --recv-keys ABC123DEF456
gpg --edit-key ABC123DEF456 # trust the key
# For SSH, add to allowed_signers file
CI-Side Enforcement
Require signed commits in CI to prevent unsigned code from merging:
# GitHub Actions
- name: Verify signatures
run: |
git log --format='%H %G?' origin/main..HEAD | \
awk '$2 != "G" && $2 != "U" { print "Unsigned commit: " $1; exit 1 }'
%G? codes:
G— good signatureB— bad signatureU— good signature, unknown validity (key is valid but not trusted)X— good signature, but expiredY— good signature, expired by the signerR— good signature, revokedE— signature cannot be checked (missing key)N— no signature
For strict enforcement: require G (or optionally U) for every commit. Reject otherwise.
Key Management
Keys are the hard part of signing. Practices:
Rotation
Keys should expire. Don't use a 20-year key. 1-2 year expiry is reasonable, with reminders to rotate.
- GPG:
gpg --edit-key→expire→ set new expiry. - SSH: just generate a new key; upload the new one; remove the old from platforms.
When you rotate, old signatures remain verifiable if you keep the old public key in the trust store. New commits use the new key.
Storage
- Hardware tokens (YubiKey, Nitrokey) hold the private key; the host never sees it. Best practice for sensitive projects.
- Keychain / OS credential store is fine for most users; the key is encrypted at rest, unlocked per session.
- Password manager as a backup of the encrypted key file.
Never commit a private key, never paste a private key into chat.
Compromise response
If your private key is compromised:
- Revoke the key in your platform account (GitHub, GitLab).
- Generate a new key and upload the public key.
- Publish a revocation certificate for GPG so users know the old key is no good (GPG-specific).
- Re-sign critical recent commits / tags if feasible.
- Audit the repo for any commits signed with the compromised key during the compromise window.
Supply Chain Implications
Signed commits are one layer in the broader software supply chain security story:
- Signed commits — provably authored by known signers.
- Signed tags — release points attested by maintainers.
- Provenance metadata (SLSA) — records of how artifacts were built.
- Signed artifacts (cosign, GPG on release binaries) — final produced files are signed.
- Reproducible builds — same inputs produce same outputs, verifiable independently.
Each layer prevents a different attack. Signed commits prevent "someone forged a commit in the repo." Signed artifacts prevent "the binary you downloaded differs from what was built." Reproducible builds prevent "the builder was compromised."
For libraries and infrastructure code, signed commits + signed tags is the minimum modern practice.
A maintainer of a widely-used open-source library had their GitHub account compromised. The attacker pushed malicious commits to main with the maintainer's author name. Most consumers of the library had no way to verify authenticity and pulled the malicious update. If commits had been signed and users had verified signatures (via go mod verify with Sigstore, or similar), the attack would have been caught: the attacker did not have the signing key, so their commits were unsigned or would have failed verification. Post-incident, the project adopted mandatory commit signing and enabled vigilant mode. Signing is not a theoretical concern — account-compromise attacks happen monthly in the open-source ecosystem.
Sigstore / Gitsign (Modern Keyless Signing)
The traditional model requires long-lived private keys. Sigstore introduces keyless signing: use short-lived certificates tied to your OIDC identity (GitHub, Google, etc.).
# gitsign (part of Sigstore) signs commits using short-lived OIDC-bound certs
brew install sigstore/tap/gitsign
git config --global gpg.format x509
git config --global gpg.x509.program gitsign
git config --global commit.gpgsign true
git commit -m "sigstore-signed commit"
# Opens a browser to authenticate with OIDC (GitHub / Google)
# Short-lived cert is issued, commit is signed, cert expires
# The signature is still verifiable forever via the public transparency log (Rekor)
Benefits:
- No long-lived private keys to manage.
- Identity-bound: the cert is tied to your GitHub/Google identity at sign time.
- Transparency log (Rekor): every signature is logged publicly, so tampering is detectable.
- Revocation is identity revocation (lose access to the OIDC provider → no more signatures).
Sigstore is becoming the default in many ecosystems (Kubernetes SIGs, various language package managers). For teams starting today, it is worth evaluating as an alternative to GPG.
Practical Setup for a New Team
Step 1: each person sets up signing
Document in README:
## Commit signing
We require signed commits on `main`. Set up SSH signing (5 minutes):
1. Ensure you have an SSH key: `ls ~/.ssh/id_ed25519*`
If not: `ssh-keygen -t ed25519`
2. Configure Git:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
3. Upload your SSH public key as a SIGNING key on GitHub:
Settings → SSH and GPG keys → New SSH key → Key type: Signing Key
Step 2: require signatures on main
GitHub branch protection:
- Require signed commits: ✓
Now unsigned pushes to main are rejected.
Step 3: verify in CI
CI script checks every commit in the PR is signed:
- name: Enforce signed commits
run: |
BAD=$(git log --format='%H %G?' origin/main..HEAD | awk '$2 != "G" { print $1 }')
if [ -n "$BAD" ]; then
echo "Unsigned commits: $BAD"
exit 1
fi
Step 4: sign release tags
Your release process (manual or automated):
git tag -s v1.2.3 -m "release 1.2.3"
git push origin v1.2.3
Step 5: document key rotation
Annual reminder to rotate keys. Update uploaded public keys. Re-verify recent signatures.
Troubleshooting
# "gpg failed to sign the data"
# Usually: GPG_TTY is unset or passphrase-agent is failing
export GPG_TTY=$(tty)
git commit -S ...
# "signing failed: No secret key"
# Check which key is configured
git config user.signingkey
# And that you have it
gpg --list-secret-keys
# Commit not showing as "Verified" on GitHub
# 1. Verify the email in your commit matches an email on your GitHub account
# (can be a private no-reply email)
git config user.email
# 2. Verify the public key is uploaded to GitHub as a signing key
# 3. Wait a minute (sometimes there's a propagation delay)
# 4. Refresh the page
# SSH signing: "cannot find allowed_signers file"
git config gpg.ssh.allowedSignersFile
# Create or point to the file:
echo "you@example.com namespaces=\"git\" $(cat ~/.ssh/id_ed25519.pub)" > ~/.ssh/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
Key Concepts Summary
- Author fields are forgeable. Without signing, Git trusts whatever you tell it.
- Signing adds a cryptographic signature (
gpgsigfield) over the commit content. - Two signing methods: GPG (traditional) and SSH (Git 2.34+, simpler). Pick SSH unless you already have GPG.
- Platforms verify signatures against keys uploaded to user accounts ("Verified" badge).
- Vigilant mode flags unsigned commits authored under your name.
- Branch protection can require signatures on specific branches.
- Signed tags for releases — industry standard for library / tool maintainers.
- CI enforcement checks
%G?in log format; requireGfor valid signatures. - Sigstore / gitsign offers keyless signing via OIDC identity + transparency log.
- Key rotation, hardware tokens, compromise response are core practices.
Common Mistakes
- Setting up signing but not uploading the public key to the platform. Commits are "signed" but platform shows no badge.
- Email in Git config does not match a verified email on the platform account. Signatures don't link to your identity.
- Signing with a key you do not keep safely. Private key on a shared machine = forgery opportunity.
- Using the same key for authentication and signing without role separation. Fine for most; problematic if you want to revoke one role.
- Long-expiration keys ("I set 20 years expiry so I never have to rotate"). Rotation is a feature, not a bug.
- Not signing release tags. The one thing end-users actually check.
- Failing to enforce signing on branches in platform settings. Signing is optional → many commits unsigned → audit value lost.
- Adopting signing without documenting the setup. New hires skip it; enforcement rejects their pushes; they hate the process.
- Ignoring Sigstore as an option. Modern, no-key-management alternative; worth evaluating.
- Trusting signed commits completely without considering author's device security. A compromised signer's machine signs bad commits with a valid key — signing proves origin, not intent.
Your team adopts commit signing via SSH keys. A developer sets up signing, commits, and pushes. Their commit shows as 'Unverified' on GitHub. Their git log locally says 'Good git signature for alice@example.com'. What is most likely wrong?