Git - Signing your Work

How do you prove that the last commit was really made by you? How to audit if your contributors are really themselves and not somebody with stolen account data? And as a user, how can you validate that code is coming from a trusted source, when it is just downloaded from the internet?

Git - Signing your Work
Photo by Pixabay

How do you prove that the last commit was really made by you? How to audit if your contributors are really themselves and not somebody with stolen account data? And as a user, how can you validate that code is coming from a trusted source, when it is just downloaded from the internet?

Let's dig into commit-signing with Git. A GPG supported way to show that you are really you and that the work is coming from your machine.

Git

Git is a free and Open Source distributed source code management system. It can handle basically everything from small personal projects to large scale development like the Linux kernel.

© Jason Long, CC BY 3.0

Working with Git makes it easy to contribute to Open Source projects, but also in corporate organizations, small teams and large initiatives. Keeping track of who worked on what and when something changed, makes Git a valid source for change management, changelogs, release messages and more.

In case you never worked with Git, or you are just getting started, please check out the beginner articles "Git - Getting Started" and "Git - Next Steps" beforehand.

Work Signing

You may have heard of messages like: "Some bad code was found in repository XYZ." or "Website XYZ shares a compromised version of tool ABC. Honestly, some of these issues must be fixed later in the so-called supply chain. Starting with the first lines of code will ensure that only the right people are contributing code.

Even if somebody was able to log in to your servers, you want to avoid installing malware on your customer's environment or introduce exploitable bugs.

Git brings capabilities to sign commits and tags, but also verify the same. In the best case, each of your contributors has a GPG key pair and is able to sign commits. And how do you do this? Well, it's easier than you think.

GPG keys

Signing commits and tags is done with GPG, in Git. GPG or GNUPG (GNU Privacy Guard) is an Open Source implementation of the OpenPGP standard is refined in RFC4880. Practically, you will end up with a key bundle, made out of secret and public keys. These keys can be used to sign or even encrypt something, but also validate that it was signed from the correct person or unencrypted without knowledge of the secret key.

In case, you never worked with GPG and you want to get started right now, I strongly recommend reading the below articles, first.

GPG - Getting Started 1/2
The world is full of signing and encryption methods. There is one, that is especially useful in Open Source software. GPG can be used to sign mails, Git commits, container image encryption and much more. Let’s set it up!
GPG - Getting Started 2/2
In the last part of this article, we installed GPG and created our first keys. For this part, let’s have a look at sub-keys and renews. Afterward, you should be set up to use GPG for upcoming tasks like container image encryption, Git commit signing or E-Mail signatures.

If you really don't have time, and you just want to test this feature, you can create a GPG key bundle with the following commands.

# List keys (just to see if you already have some
$ gpg --list-keys

# Generate a new key
$ gpg --gen-key 

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: Foo Bar
Email address: foobar@example.com
You selected this USER-ID:
    "Foo Bar <foobar@example.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? o

# List keys (to review)
$ gpg --list-keys --keyid-format long
------------------------------------
pub   ed25519/E76EA4FB7D30E7D3 2024-02-07 [SC] [expires: 2027-02-06]
      9F155BCD338D2F47E10FAEABE76EA4FB7D30E7D3
uid                 [ultimate] Foo Bar <foobar@example.com>
sub   cv25519/E111AF22283B7636 2024-02-07 [E] [expires: 2027-02-06]

Configure Git

To configure Git, with your desired key, and review the existing configuration, you just need to perform a couple of commands.

# List global Git configuration
$ git config --global --list

# Configure your name (if not done already)
$ git config --global user.name=Foo Bar

# Configure your mail (if not done already, should match the gpg mail)
$ git config --global user.email=foobar@example.com

# Configure your signing key (the desired key ID is fetched from gpg --list-keys --keyid-format long)
$ git config --global user.signingkey=E76EA4FB7D30E7D3

Now, you are ready to sign your work. Git can use your key to sign commits and tags, if you tell it to do so. Let's investigate how this works.

Signing commits

Signing your commits will validate that really you were the one who worked on the code. This ensures that the commit was really made by you, and not just somebody who configured Git the same way.

To sign a commit, the manual way, you can use the already known git commit command. You just need to add -S to it.

# Stage the code you want to commit
$ git add <file>

# Sign a fresh commit
$ git commit -S -m "Signed commit"

In case, you want to sign every commit, you can configure Git in this way.

# Configure Git to sign all commits in all repositories
$ git config --global commit.gpgsign true

# Configure Git to sign all commits in the current repository
$ git config --local commit.gpgsign true

Verifying commits

Now that we know how to sign a commit, we also want to verify that a commit was made by someone. To do so, we need to import the public key from the other person. Afterward, or if it is our own commit, we can check if the commit was made by the real person (aka the well known signing key).

# Show git log
$ git log --show-signature

commit c387dd569c6f2b8a560fca6935896eaef8692295 (HEAD -> main)
gpg: Signature made Mi 07 Feb 2024 11:23:17 CET
gpg:                using EDDSA key 9F155BCD338D2F47E10FAEABE76EA4FB7D30E7D3
gpg: Good signature from "Foo Bar <foobar@example.com>" [ultimate]
Author: Daniel Schier <dschier@while-true-do.io>
Date:   Wed Feb 7 11:23:17 2024 +0100

    Signed commit

There you go. The "Good signature" indicates that we know the public key. With the flag --show-signature, Git will hand down the key ID to GPG, where it will get verified.

But, this looks a bit, let's say, fragmented. Let's say, we want to have better view on the same. Maybe something like:

# Git log, but prettier
$ git log --pretty="format:%h %G? %aN  %s"

c387dd5 G Daniel Schier  Signed commit
bb029a2 G Daniel Schier  Signed commit

Now, let's explain the output quickly.

  • %h is the short commit hash
  • %G? is a variable that carries the GPG status
    • "G" for a good signature
    • "B" for a bad signature
    • "U" for a good signature with unknown validity
    • "X" for a good signature that has expired
    • "Y" for a good signature made by an expired key
    • "R" for a good signature made by a revoked key
    • "E" if the signature cannot be checked
    • "N" for no signature
  • %aN author name
  • %s Subject/Title of the commit (aka the first line)

You can find even more format options via git log --help.

Signing tags

So far, we have learned how to set up Git for signing, signing our commits and reviewing/validating the same. Now, we may want to create a tag, for our next release or so.

In general, I recommend using annotated tags. This allows to add descriptions to tags and makes it easier to identify why something was created. This is also a nice place to add your release notes.

# Create a tag
$ git tag <tag>

# Create an annotated tag
$ git tag -a <tag>

In addition, we may want to add a signature. Interestingly, Git forces us to use annotations, if we want to sign tags. Perfect (at least for my taste). Now, you might have guessed the command already. Have you?

# Create a signed tag
$ git tag -s <tag>

You will be requested to add the message for your annotation.

You might want to enforce signatures for tags. This can be configured in Git with the following command.

# Configure to sign all tags for all repositories
$ git config --global tag.gpgSign true

# Configure to sign tags for the local repository
$ git config --local tag.gpgSign true

Verifying tags

Now that you signed a couple of tags, you might want to review them or verify/validate that the tagger and its key are valid. Let's do this.

# Show details of a tag
$ git show <tag>

tag 0.1.0
Tagger: Daniel Schier <dschier@while-true-do.io>
Date:   Wed Feb 7 14:57:25 2024 +0100

Just my first signed tag
-----BEGIN PGP SIGNATURE-----

LOTSOFLETTERSHERE

-----END PGP SIGNATURE-----

commit c387dd569c6f2b8a560fca6935896eaef8692295 (HEAD -> main, tag: 0.1.0)
Author: Daniel Schier <dschier@while-true-do.io>
Date:   Wed Feb 7 11:23:17 2024 +0100

    Signed commit

You can also validate, if the key is ok. Similar to the commit verification, you need to ensure that you have the public key of the tagger imported, beforehand.

$ git tag -v 0.1.0
object c387dd569c6f2b8a560fca6935896eaef8692295
type commit
tag 0.1.0
tagger Daniel Schier <dschier@while-true-do.io> 1707314245 +0100

Just my first signed tag
gpg: Signature made Mi 07 Feb 2024 14:57:36 CET
gpg:                using EDDSA key 9F155BCD338D2F47E10FAEABE76EA4FB7D30E7D3
gpg: Good signature from "Foo Bar <foobar@example.com>" [ultimate]

As you can see, the last line states gpg: Good signature. For an invalid tag, or if you don't have the public key of the tagger, you might see something like error: could not verify the tag.

Merges

Almost done. There is one more thing, we need to talk about. Let's assume all of your contributors properly signed their commits, and you want to merge a branch. In Git 1.8.3 and later, git merge and git pull can be told to inspect and reject when merging a commit that does not carry a trusted GPG signature with the --verify-signatures option.

# merge a branch and ensure only signed commits are in
$ git merge --verify-signatures <branch>

# Example
$ git merge --verify-signatures feature/signing 
Commit 43478f8 has a good GPG signature by Foo Bar <foobar@example.com>
Updating c387dd5..43478f8
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)

In case the branch contains unverified commits, this will look something like this:

# Try to merge an unverified branch
$ git merge --verify-signatures <branch>
fatal: Commit ab0123456 does not have a GPG signature.

And if you want to merge with a merge-commit and verify the result commit, this works like so:

# Merge with signing the resulting merge-commit
$ git merge --verify-signatures -S <branch>

Lastly, you can also ensure the above behavior locally or globally, so you don't need to remember the --verify-signature option.

# Configure to verify signatures on merge globally
$ git config --global merge.verifySignatures true

# Configure to verify signatures for the current repository only
$ git config --local merge.verifySignatures true

Now, you should be settled to push your supply chain from "this may come from" to "this is verified and coming from".

Integrations

Working with Git on the command line might be fun and cool, but showing off is also something, you need to consider. Many tools are coming with integrations for signed commits and tags.

This allows users to verify if the upstream repository is well and healthy, but also ensures some visual representation of the supply chain validity. Below, you can find links for some platforms, I have used with signed commits.

Signed commits | GitLab
GitLab product documentation.
Signing commits - GitHub Docs
You can sign commits locally using GPG, SSH, or S/MIME.

And finally, some more links, in case you want to continue your journey with GPG and Git.

Git - Signing Your Work
Git - git-merge Documentation
The GNU Privacy Guard

Conclusion

Phew, another long article. I really need to make them shorter in the future. Anyway, this topic is pretty important, mostly for corporate teams, but also for open source projects with a strong focus on software quality. In a perfect world, we want to track down the entire supply chain, from coding to packaging and hosting a service. Signatures for code are only one part of this journey.

Have you ever used signed commits? Do you enforce them? How do you ensure that only the right people are allowed to work on code and validate that they are really themselves?

I would love to get your opinion on the same.