simon-dreher.de

git is not only totally awesome at storing your source code, it also has many features (just look at git help -a). Today I want to show you some of them, which use cryptographic signing for various things.

Signing commits

The first feature is signed commits, which aren't really common today, but get used increasingly. They can be easily used, without changing the workflow and provide authentication that this commit has been authored by the signer.

Important to note here is the difference between (cryptographically) signing a commit and signing off a commit. Signing off is done using the command line switch -s (lower-case s) and justs add a line containing the committer at the end of the commit message. It is typically used for legal reasons (see git documentation for more details).

For cryptographically signing a commit you have to use the command line switch -S (upper-case s) and either specify the key to use or you have beforehand configured your key in the git config.

Config

The official documentation has a nice introduction on how to use signed commits. Basically you only have to find the ID of your key and add it to the config (can also be locally configured or passed on the command line):

gpg --list-secret-keys
git config --global user.signingkey ${KEY_ID}

You can verify a specific commit with git verify-commit. To show for each commit if it is signed you can view the log with this:

git log --show-signature

or include %G? into your custom pretty log format.

Benefits

By signing commits you can cryptographically securely prove that a commit has been authored or at least approved by you. The regular author of a commit can freely be chosen, so anyone could forge it. Mike Gerwitz spun an amusing git horror story out of this scenario and shows how signed commits, together with a security policy, can prevent this.

Another plus side is, that if you add your gpg public key in your github profile it shows a verified badge on your signed commits.

In principle signing the last commit is enough to implicitly sign all the previous commits, since the commits form kind of a merkle tree (also see the later section on SHA-1).

Signing is not restricted to commits, you can also sign tags or merges (with --verify-signatures on merge you can even verify that the commit to be merged is correctly signed). Since version 2.2.0 you can even sign your pushes!

Signed push

The signing of whole pushes seems to be a much less commonly used feature. Nearly all the search results I could find were different versions of the git man page.

The protocol between server (where it's pushed to) and client (the pusher) works roughly as follows (according to the original commit introducing push signing):

  1. The server generates a text file containing the commiter, the old and new refs, a timestamp etc. and a nonce (computed as HMAC(path, timestamp, secret seed)).
  2. The client then has to sign this file (using GPG) and sends the resulting certificate.
  3. The server (by default) does nothing with this signatue, but it can be used in receive-hooks. For this several GIT_PUSH_CERT* variables are set and can be used in pre- and post-receive-hook.

Such an certificate could look as follows:

certificate version 0.1
pusher B528DAC8C4CE9F1DD40FCEA498528C52F33E51D7 1503779574 +0200
pushee /home/simon/test-gitbare
nonce 1503779574-6bc9f1573f6b509aa93b

64a8f3381bebb5d66c8380ea7a0afb879cca0c30 ab229000201ca35016fdd619d55f7153d3ac9fab refs/heads/master
-----BEGIN PGP SIGNATURE-----

iQEzBAABCAAdFiEEtSjayMTOnx3UD86kmFKMUvM+UdcFAlmh2vYACgkQmFKMUvM+
UddONAf/dVsPei9QZ9JpZb6T4yRKMSMlZyjL/xNr7GwAMrvXmUQFGNtuM8ihTk/I
zextMCeZ3Wpkzns1j1HG+/3zZjH8ULFIg31XOO9ek8J0Onl2BH0QwdyGndBW3gUt
axFH97848S1eBYlUPcEl8u2O30rvLCWvEBJ5TsT9LFI3Ujzh+6kp69wBhdzv/Zb9
aJX2Sf05l5n4nkEOuhyU4jVAXpK6Xg44cdaZdKYgrD/ayTnsC/6CIw605zsP8YAR
ZSdC+Beth/cgR1N5e63a6gswnN4wSWiamvaE+D9BjNEEFK98tzbe1pYj2AY11mR9
7Nf3F6WeuQlsXh6xjwYq09KNedrAxg==
=nrlt
-----END PGP SIGNATURE-----

For this whole exchange to work the server has to offer signed pushes, and for this the certNonceSeed has to be configured on the server:

git config receive.certNonceSeed whateverRandomValue

And then you can push with

git push --signed origin master

What happens with the signed push is totally up to the server. The receive-hooks could either be used to only accept pushes of specific authors or accept all pushes, but store them into an audit trail.

git cat-file blob ${GIT_PUSH_CERT} in a receive-hook yields the certificate with accompanying signature. Storing them somewhere you could create a chain of certificates, where for each ref update the signer is cryptographically authenticated. Further tools for creating or auditing such a chain I don't know of, so if you're interested you have to invent something by yourself.

Inspect the certificate

A problem I had with the certificate was that I couldn't directly get gpg to verify this combined format. I had to either split it by hand and verify or use the following commands:

csplit certificate '/^-----BEGIN PGP SIGNATURE-----$/' '{*}' --prefix='signature'
gpg --verify signature01 signature00

Benefits

If you use only signed commits but unsigned pushes, this allows an attacker to reset the head back to an old commit, unless you prevent it server side with settings which disallow force pushes etc.

Even if you do prevent this you can't be sure, to which branch name the signer assigned the commit to. So an attacker could move the current state of a development branch to master. For this scenario it's neccessary that he needs some kind of write access, but should actually not be allowed to write to some special branches (such as master).

In summary you can realize a kind of access control system with signed pushes. Normally git relies for the access control on ssh or classical file attributes. The committer and author of a git commit are easy to forge and therefore can't be used for (secure) access control. With cryptographical signing the identity of the pusher is authenticated and the receive-hook can safely decide based on this information.

For now different write access to different branches is solved by separate repos and pull request (e.g. as done on github). With some signed push based access control it could be even more fine grained (you are only limited to what can be done in a git hook).

This push certificates don't allow to make sure that the repo is on the most recent state in case of a malicious server. Such a server could just ignore new pushes and serve an old state.

Security implications of SHA-1

Since SHA-1 is the hash function used in git and lately there have been several attacks on SHA-1 there remains the question if this has implications on the security of the functions shown above.

As Linus Torvalds himself explained, originally git didn't rely on the cryptographic collision resistance of SHA-1. In the signing functions above the signed data is based on SHA-1 hashes. So if you could find a collision for a commit hash you could get someone to sign one commit content and then change it to the other content, without invalidating the signature.

For now (as Linus rightly mentions) the SHA-1 attacks aren't practical for forging commits. For one the method described in SHAttered can be detected (which git does) and secondly it relies on a large block of not displayed binary data (unused jpeg image) in a pdf file. If your file format isn't that forgiving, it is much more difficult to create colliding documents which don't look like garbage. Git has few places where something like this could be hidden and the developers implemented protection against the mentioned attack.

So for now it isn't practically exploitable, but moving to a better hash function would be beneficial and is already in development.

Conclusion

Signing your commits is backwards compatible and proves the authorship of a commit, so I see nearly no reasons not to use it, at least for tags and commits of a release it's very beneficial.

For signing pushes I haven't found much practical applications, but there are cases where it's extremely useful. For example I'm currently building a kind of configuration management system and for this it's not enough to secure that some commit is signed by me, otherwise the config of a machine could be reset to an old state. With signed pushes however I can ensure that the newest commit is used as long as my server isn't malicous (in which case I'm lost anyways).