Skip to content

Signed Git Commits: Authenticity and Verification

Lesson 4 of 9Intermediate13 min readGit Security & DevSecOps · Repository SecurityVerified: git 2.43.0; GitHub commit signature verification and vigilant mode, September 2026

Pillar 2’s signed commits lesson covers setup: generating a key, configuring Git, getting the green Verified badge.

This page is about what that badge means — and, more usefully, what it does not.

The single most important sentence in this lesson:

A valid signature proves something about the relationship between a signing key and an identity. It does not prove the code itself is safe.

Signing attaches a cryptographic signature to a commit or tag object. Verification checks that signature against a public key, and GitHub additionally checks that the key is registered to an account.

That chain answers exactly one question: was this object produced by someone holding the private key associated with this identity?

It answers nothing about whether the change is correct, safe, reviewed or intentional. A developer whose laptop is compromised signs malicious commits with their own key, and every signature verifies perfectly.

Signing is worth doing because Git’s author field is otherwise an unauthenticated free-text field that anyone can set to anything. It closes impersonation. It does not close anything else.

Git has no central identity provider. When you commit, Git records user.name and user.email from your configuration — values you chose, on your machine, with no verification step anywhere.

Terminal window
git -c user.name="Example Maintainer" \
-c user.email="maintainer@example.com" \
commit --allow-empty -m "Demonstration commit"

What it doesCreates a commit attributed to a different name and email address without changing any stored configuration.

Why we run itIt demonstrates that the author field is input, not evidence. Run it in a disposable repository.

Expected resultA commit whose git log output shows the supplied name and address.

Push that to GitHub with an email address matching a real account, and the web interface shows that account’s avatar next to the commit. Nothing has been broken; the field simply never carried a guarantee.

This is not a defect. Git is a distributed system in which any clone must be able to create commits offline. Authorship is metadata by design, and signing is the layer that turns metadata into evidence.

A Git commit object is text: tree hash, parent hashes, author, committer, message. Signing computes a signature over that content and stores it in the object.

Two consequences follow directly from that:

The signature covers the whole commit, including its parents. Changing anything — the message, the tree, the author, the parent — produces a different object with a different hash, and the old signature does not apply to it.

It says nothing about the tree’s contents beyond their hash. The signature commits you to this exact snapshot. It expresses no opinion on whether the snapshot is good.

There is one more property worth internalising: because a commit includes its parents’ hashes, a signature on a commit transitively fixes the entire history reachable from it. A signed release tag is therefore a strong statement — it pins the whole history, not just the tip.

GitHub supports GPG, SSH and S/MIME for commit signature verification.

SSHGPG / OpenPGPS/MIME
Setup effortLow — you may already have a keyModerate — key generation, expiry, subkeysOrganisational — certificate issued by a CA
Key you uploadA signing key entry on your accountA public key on your accountHandled through the certificate chain
ExpiryNo built-in expiryBuilt in, and worth usingFrom the certificate
RevocationRemove the key from the accountRevocation certificate plus removalCA revocation
Web-of-trust or CANeitherOptional web of trustCA-backed identity
Typical fitIndividuals and most teamsProjects with an existing OpenPGP cultureLarger organisations with PKI

SSH signing is the pragmatic default for most people. You very likely already have an SSH key, Git has supported SSH signing for several releases, and it removes the largest practical obstacle to adoption, which was never cryptography — it was GPG’s user experience.

GPG is not obsolete or worse. It has genuine advantages: built-in expiry, a revocation mechanism designed for the purpose, subkeys so the signing key need not be the master key, and an ecosystem of existing tooling. If your project already uses it, there is no security argument for migrating. GPG / OpenPGP signing covers it properly.

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

Note the fourth line. Tag signing is configured separately from commit signing and is frequently missed, which produces repositories full of signed commits and unsigned release tags — precisely backwards, since the tag is what consumers pin to.

GitHub’s badge is convenient and it is not the only check available. Verifying locally matters because it is the version that works when you do not trust the platform’s rendering.

Terminal window
git log --show-signature -3

What it doesShows each commit's signature validation result alongside the log entry.

Why we run itIt is the check that does not depend on a web interface, and the one to reach for during an incident.

Expected resultA Good signature line per signed commit, or a note that no signature was found.

Terminal window
git verify-commit HEAD

What it doesVerifies a single commit's signature and exits non-zero if it does not validate.

Why we run itIt is scriptable, which makes it usable in a check rather than in a habit.

Expected resultExit status 0 for a good signature; a diagnostic and non-zero status otherwise.

For SSH signatures, local verification needs an allowed signers file mapping identities to public keys — Git has no equivalent of GitHub’s account registry:

Terminal window
echo "engineer@example.com ssh-ed25519 AAAA_EXAMPLE_PUBLIC_KEY_MATERIAL" \
>> ~/.config/git/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers

Without that file, Git can confirm a signature is internally well-formed but cannot say whose it is — reported as No principal matched. That state is easy to mistake for verification.

Some projects commit an allowed_signers file to the repository and point Git at it, which makes “who may sign for this project” a reviewable file rather than local configuration. It is a good pattern, with one caveat: whoever can merge a change to that file can add a key.

GitHub does something Git alone cannot: it checks the signature and checks that the key belongs to an account, then relates the signer to the commit’s author and committer fields.

The statuses:

StatusMeaning
VerifiedSigned, and the signature verified against a key registered to an account
Partially verifiedSigned and verified, but the author is a different person who has enabled vigilant mode
UnverifiedSigned, but the signature could not be verified
(no badge)Unsigned

That last row is the gap most people miss. By default an unsigned commit shows nothing — not a warning, not a red mark. A repository where most commits are signed and one is not looks, at a glance, entirely normal.

Vigilant mode changes the default. With it enabled on your account, commits attributed to you that are not signed are displayed as Unverified rather than as unmarked.

That is the setting that makes signing an actual anti-impersonation control. Without it, an attacker who forges your name simply produces an unsigned commit, which renders identically to every other unsigned commit in the repository. With it, the forgery is visibly marked.

Partially verified exists because Git separates author from committer. If Alice writes a commit and Bob rebases it, Bob signs it — the signature is Bob’s, and it says nothing about whether Alice consented to the final form. When the author has vigilant mode enabled, GitHub reports that distinction rather than flattening it to Verified.

A ruleset or branch protection rule can require signed commits on a protected ref. Every commit reaching that branch must carry a verifiable signature.

This is where a real and widely-encountered problem appears.

Commits made through the GitHub web interface — editing a file in the browser, accepting a suggestion in review — are created by GitHub and signed with its web-flow key, whose public half is published so anyone can verify it. That is why a repository requiring signatures does not block web edits.

It is also worth understanding for what it is: those commits are signed by GitHub, attesting that GitHub made the change on the authenticated user’s behalf. That is a different claim from a commit signed on a developer’s machine, and it is exactly as strong as your trust in the platform.

Signing introduces a failure mode that unsigned repositories do not have: what happens when the key is somebody else’s.

The uncomfortable property is that signatures do not expire retroactively in a useful way. A signature made yesterday with a key you revoke today still verifies against the key. Git records no timestamp authority, so “was this signed before or after the compromise?” has no cryptographically reliable answer from the signature alone.

Practically, the response is:

  1. Remove the key from your GitHub account immediately. New commits signed with it stop showing as Verified.

  2. Generate and register a replacement, so you can keep working.

  3. For GPG, publish a revocation certificate — the mechanism designed for this, which is why you generate it at key creation time rather than when you need it.

  4. Identify what was signed with it. Search the repository’s history for commits signed by the key and establish which are legitimate.

  5. Consider re-signing a known-good point. A signed tag on a commit you have verified by other means gives consumers a reference that does not depend on the compromised key.

  6. Tell downstream consumers. Anyone who has been verifying your signatures needs to know which key to stop trusting, and from when.

Step 6 is the one organisations skip. If nobody was verifying, it costs nothing; if somebody was, its absence means they are still trusting a key you know is compromised.

It is worth writing this out, because “we sign our commits” is often used to answer questions signing does not address.

Threat. A commit is attributed to a trusted maintainer without that maintainer having produced it — either to slip a change past reviewers who trust the name, or to muddy attribution after an incident.

Attack surface. Anyone who can push to any branch of the repository, or open a pull request from a fork, can set the author field to anything. That includes external contributors on a public repository.

Impact. Modest on its own, and significant in combination. A reviewer skimming a diff from a trusted colleague reads it differently from the same diff from a stranger, and that difference is exactly what the forgery is buying.

Control. Signing plus vigilant mode makes an unsigned commit attributed to a signing user visibly unverified. A ruleset requiring signed commits makes it un-mergeable.

Verification. Push an unsigned commit with a colleague’s name to a test repository and look at how it renders, before and after enabling vigilant mode. This takes two minutes and is more convincing than any explanation.

Note what is absent from that model: nothing about malicious code, nothing about compromised machines, nothing about review quality. Those threats are real and they belong to other controls.

Enforcement through a ruleset is the strong version, and there are situations where you want an independent check — verifying a dependency’s release tag, or confirming what landed on a branch you consume.

- name: Verify the release tag is signed by a known maintainer
run: |
git verify-tag "${TAG}"
env:
TAG: ${{ github.event.release.tag_name }}

Two things to get right for this to mean anything.

Fetch enough history and the tag object itself. A shallow clone may not have the annotated tag, and git verify-tag on a missing object is a confusing error rather than a failure.

Supply the trust anchor. For SSH signatures the job needs an allowed_signers file, and where that file comes from is the security decision. Committed to the repository, it is protected by whatever protects the repository. Fetched at run time, it is protected by whatever protects that fetch.

The failure mode to avoid is a verification step that passes because it verified nothing — no principal matched, no signature present, wrong object. Test it against a deliberately unsigned tag and confirm the job fails.

Because signing is cryptographic, it attracts more confidence than its scope supports. Four things it is regularly asked to do and does not:

It is not review. A signature says who; review says whether.

It is not access control. Anyone with a registered key and write access can sign and push. The signature does not consult a permission model.

It is not provenance for artifacts. A signed commit says something about source. The binary somebody downloads was produced by a build, and connecting the two is build provenance and artifact attestations — a separate mechanism with a separate chain of evidence.

It is not non-repudiation in any legal sense. Git records no trusted timestamp, keys are held on ordinary laptops, and revocation does not reach backwards. Treat a signature as strong operational evidence, not as proof of intent.

Requiring signatures across an organisation is a cultural change more than a technical one, and it fails in a predictable way: mandate first, tooling second, and a month of blocked merges.

An order that works:

  1. Start with the people who publish. Maintainers who cut releases are a small set, and release tags are the highest-value thing to sign.
  2. Sign tags before commits. Fewer objects, higher value, less disruption.
  3. Enable vigilant mode on individual accounts. It is free, it is per-account, and it makes impersonation visible without blocking anyone.
  4. Run the ruleset in Evaluate mode to see who and what would break. Bots and integrations are usually the surprise.
  5. Fix automation. Anything that commits from CI needs a signing identity, which usually means a GitHub App or a dedicated key held as a secret. This is the step that takes real work.
  6. Enforce, once steps 4 and 5 are clean.

Believing a signature vouches for the code. It vouches for the key. A compromised developer machine produces perfectly verified malicious commits.

Signing commits and not tags. commit.gpgsign and tag.gpgsign are separate settings, and the tag is what consumers pin to.

Uploading an SSH key for authentication and expecting signatures to verify. The signing key entry is separate.

Requiring signed commits while allowing rebase and merge. These conflict, and the failure appears at merge time.

Treating absence of a badge as safe. Without vigilant mode, unsigned commits are unmarked, not flagged.

Storing a signing key in CI without thinking about what it means. A key in a repository secret is a key that any workflow — including one added in a pull request — can potentially reach. Prefer a GitHub App identity or attestations over shipping a private key into a runner.

A signature is a statement: “the holder of this key produced exactly these bytes.” Verification checks the statement. Everything else — whether the key’s holder is who you think, whether they meant it, whether the bytes are any good — is a separate question with a separate answer.

  • Git’s author and committer fields are unauthenticated configuration; signing is what makes authorship verifiable
  • A signature covers the whole commit object, and transitively pins the history reachable from it
  • GitHub supports GPG, SSH and S/MIME; SSH signing is the lowest-friction default for most teams
  • An SSH key must be registered specifically as a signing key, separately from authentication
  • Unsigned commits show no badge by default; vigilant mode is what makes impersonation visible
  • Partially verified reflects Git’s author/committer split, not a weaker signature
  • Rebase and merge adds commits without signature verification and conflicts with a signing requirement
  • Revoking a key does not invalidate signatures already made with it
  • Signing proves origin and says nothing about whether the change is safe

Use a disposable repository.

  1. Configure SSH signing and make a signed commit. Run git log --show-signature and read the output.

  2. Register the key on GitHub as an authentication key only. Push. Predict: what status does GitHub show? Then add it as a signing key and refresh.

  3. Make a commit with -c user.name and -c user.email set to a colleague’s details, unsigned. Push it. Predict: does anything visually distinguish it?

  4. Enable vigilant mode on your account and look again.

  5. Create a signed tag with git tag -s v1.0.0 -m "Release". Verify it with git verify-tag v1.0.0. Then check whether your ordinary commits were being signed and the tag was not.

  6. Add a ruleset requiring signed commits. Push an unsigned commit and read the rejection.

  7. With the rule active, open a pull request and try each merge method your repository allows. Predict: which succeed?

  8. Delete the repository.

Check your understanding

3 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.

A commit shows GitHub's 'Verified' badge. What has been established?
Show answer

That the holder of a key registered to that account produced exactly these bytes — A signature vouches for the key, not the code. A compromised developer machine produces perfectly verified malicious commits. Verification answers one question and nothing else.

You uploaded your SSH public key to GitHub as an authentication key and signed a commit with it. Does it verify?
Show answer

No — a signing key must be registered separately as a signing key — GitHub keeps authentication keys and signing keys as separate entries. The same key can be added twice, but it verifies signatures only when added as a signing key.

A repository requires signed commits and allows the 'rebase and merge' button. What happens at merge time?
Show answer

The merge fails — rebasing creates new commits that GitHub cannot sign as the author — Rebase-and-merge rewrites commits, and the rewritten commits are no longer signed by the author. The lesson lists this combination as one that conflicts, with the failure appearing only at merge time.

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.