A Git Horror Story: Repository Integrity With Signed Commits

By Mike Gerwitz

What if we had a way to ensure that a commit by someone named "Mike Gerwitz" with my e-mail address is actually a commit from myself, much like we can assert that a tag signed with my private key was actually tagged by myself? Well, who are we trying to prove this to? If you are only proving your identity to a project author/maintainer, then you can identify yourself in any reasonable manner. For example, if you work within the same internal network, perhaps you can trust that pushes from the internal IP are secure. If sending via e-mail, you can sign the patch using your GPG key. Unfortunately, these only extend this level of trust to the author/maintainer, not other users! If I were to clone your repository and look at the history, how do I know that a commit from “Foo Bar” is truly a commit from Foo Bar, especially if the repository frequently accepts patches and merge requests from many users?

$ git commit -S -m 'Fixed security vulnerability CVE-123'
# ^ GPG-sign commit

Notice the -S flag above, instructing Git to sign the commit using your GPG key (please note the difference between -s and -S). If you followed this practice for each of your commits---with no exceptions---then you (or anyone else, for that matter) could say with relative certainty that the commit was indeed authored by yourself. In the case of our story, you could then defend yourself, stating that if the backdoor commit truly were yours, it would have been signed. (Of course, one could argue that you simply did not sign that commit in order to use that excuse. We’ll get into addressing such an issue in a bit.)

In order to set up your signing key, you first need to get your key id using gpg --list-secret-keys:

$ gpg --list-secret-keys | grep ^sec
sec 4096R/8EE30EAB 2011-06-16 [expires: 2014-04-18]
# ^^^^^^^^

You are interested in the hexadecimal value immediately following the forward slash in the above output (your output may vary drastically; do not worry if your key does not contain 4096R as above). If you have multiple secret keys, select the one you wish to use for signing your commits. This value will be assigned to the Git configuration value user.signingkey:

# remove --global to use this key only on the current repository
$ git config --global user.signingkey 8EE30EAB
# ^ replace with your key id

Given the above, let’s give commit signing a shot. To do so, we will create a test repository and work through that for the remainder of this article.

$ mkdir tmp && cd tmp
$ git init .
$ echo foo > foo
$ git add foo
$ git commit -S -m 'Test commit of foo' You need a passphrase to unlock the secret key for
user: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"
4096-bit RSA key, ID 8EE30EAB, created 2011-06-16 [master (root-commit) cf43808] Test commit of foo 1 file changed, 1 insertion(+) create mode 100644 foo

The only thing that has been done differently between this commit and an unsigned commit is the addition of the -S flag, indicating that we want to GPG-sign the commit. If everything has been set up properly, you should be prompted for the password to your secret key (unless you have gpg-agent running), after which the commit will continue as you would expect, resulting in something similar to the above output (your GPG details and SHA-1 hash will differ).

By default (at least in Git v1.7.9), git log will not list or validate signatures. In order to display the signature for our commit, we may use the --show-signature option, as shown below:

$ git log --show-signature
commit cf43808e85399467885c444d2a37e609b7d9e99d
gpg: Signature made Fri 20 Apr 2012 11:59:01 PM EDT using RSA key ID 8EE30EAB
gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"
Author: Mike Gerwitz <mike@mikegerwitz.com>
Date: Fri Apr 20 23:59:01 2012 -0400 Test commit of foo

There is an important distinction to be made here---the commit author and the signature attached to the commit may represent two different people. In other words: the commit signature is similar in concept to the -s option, which adds a Signed-off line to the commit---it verifies that you have signed off on the commit, but does not necessarily imply that you authored it. To demonstrate this, consider that we have received a patch from “John Doe” that we wish to apply. The policy for our repository is that every commit must be signed by a trusted individual; all other commits will be rejected by the project maintainers. To demonstrate without going through the hassle of applying an actual patch, we will simply do the following:

$ echo patch from John Doe >> foo
$ git commit -S --author="John Doe <john@doe.name>" -am 'Added feature X' You need a passphrase to unlock the secret key for
user: "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"
4096-bit RSA key, ID 8EE30EAB, created 2011-06-16 [master 16ddd46] Added feature X Author: John Doe <john@doe.name> 1 file changed, 1 insertion(+)
$ git log --show-signature
commit 16ddd46b0c191b0e130d0d7d34c7fc7af03f2d3e
gpg: Signature made Sat 21 Apr 2012 12:14:38 AM EDT using RSA key ID 8EE30EAB
gpg: Good signature from "Mike Gerwitz (Free Software Developer) <mike@mikegerwitz.com>"
Author: John Doe <john@doe.name>
Date: Sat Apr 21 00:14:38 2012 -0400 Added feature X
# [...]

This then raises the question---what is to be done about those who decide to sign their commit with their own GPG key? There are a couple options here. First, consider the issue from a maintainer’s perspective---do we necessary care about the identity of a 3rd party contributor, so long as the provided code is acceptable? That depends. From a legal standpoint, we may, but not every user has a GPG key. Given that, someone creating a key for the sole purpose of signing a few commits without some means of identity verification, only to discard the key later (or forget that it exists) does little to verify one’s identity. (Indeed, the whole concept behind PGP is to create a web of trust by being able to verify that the person who signed using their key is actually who they say they are, so such a scenario defeats the purpose.) Therefore, adopting a strict signing policy for everyone who contributes a patch is likely to be unsuccessful. Linux and Git satisfy this legal requirement with a “Signed-off-by” line in the commit, signifying that the author agrees to the Developer’s Certificate of Origin; this essentially states that the author has the legal rights to the code contained within the commit. When accepting patches from 3rd parties who are outside of your web of trust to begin with, this is the next best thing.

To adopt this policy for patches, require that authors do the following and request that they do not GPG-sign their commits:

$ git commit -asm 'Signed off'
# ^ -s flag adds Signed-off-by line
$ git log
commit ca05f0c2e79c5cd712050df6a343a5b707e764a9
Author: Mike Gerwitz <mike@mikegerwitz.com>
Date: Sat Apr 21 15:46:05 2012 -0400 Signed off Signed-off-by: Mike Gerwitz <mike@mikegerwitz.com>
# [...]

Then, when you receive the patch, you can apply it with the -S (capital, not lowercase) to GPG-sign the commit; this will preserve the Signed-off-by line as well. In the case of a pull request, you can sign the commit by amending it (git commit -S --amend). Note, however, that the SHA-1 hash of the commit will change when you do so.

What if you want to preserve the signature of whomever sent the pull request? You cannot amend the commit, as that would alter the commit and invalidate their signature, so dual-signing it is not an option (if Git were to even support that option). Instead, you may consider signing the merge commit, which will be discussed in the following section.