Signed git pushes

Many people know that you can PGP-sign git objects — such as tags or commits themselves — but very few know of another attestation feature that git provides, which is signed git pushes.

Why sign git pushes? And how are they different from signed tags/commits?

Signed commits are great, but one thing they do not indicate is intent. For example, you could write some dangerous proof-of-concept code and push it into refs/heads/dangerous-do-not-use. You could even push it into some other fork hosted on a totally different server, just to make it clear that this is not production-ready code.

However, if your commits are PGP-signed, someone could take them and replay over any other branch in any other fork of your repository. To anyone checking the commit signatures, everything will look totally legitimate, as the actual commits are signed by you — never mind that they contain dangerous vulnerable code and were never intended to be pushed into something like refs/heads/next. At the very least, you will look reckless for pushing bad code, even though you were just messing around in a totally separate environment set up specifically for experimentation.

To help hedge against this problem, git provides developers a way to sign their actual pushes, as a means to attest “yes, I actually did intend to push these commits into this ref in this repository on this server, and here's my PGP signature to prove it.” When a push is signed, git will both check the signature it received against a trusted keyring and generate a “push certificate” that can be logged in something like a transparency log:

https://git.kernel.org/pub/scm/infra/transparency-logs/gitolite/git/1.git/plain/m?id=c06eebe4875d6103d580efcf8cd78cc9cc4b5192

Now, before you rush to enable signed pushes, please keep in mind that this functionality needs to first be enabled on the server side, and the vast majority of public git hosting forges do NOT have this turned on. Thankfully, git provides an if-asked setting, which will first check if the remote server supports signed pushes, and only generate the push certificate if the remote server accepts them. To enable this feature for yourself, simply add the following to your ~/.gitconfig:

[push]
    gpgSign = if-asked

Enabling on the server side

If you are running your own git server, then it is easy to enable this on the server side. Add the following either to each repository config file, or to /etc/gitconfig to enable it globally:

[receive]
    advertisePushOptions = true
    certNonceSeed = "<uniquerandomstring>"

You should set the certNonceSeed setting to some randomly generated long string that should be kept secret. It is combined with the timestamp to generate a one-time value (“nonce”) that the git client is required to sign and provides both a mechanism to prevent replay attacks and to offer proof that the certificate was generated for that specific server (though for others to verify this, they would need to know the value of the nonce seed).

Once you have this feature enabled, it is up to you what you do with the generated certificates. You can simply opt to record them, just like we do with our transparency log, or you can actually reject pushes that do not come with valid push certificates. I refer you to the git documentation and to our post-receive-activity-feed hook, which we use to generate the transparency log: