Sign git commits on GitHub with GPG in macOS

gitgithubdotfilesgpgsecurity

When you are pushing commits to your repositories you are authenticating with either HTTPS or SSH. Telling GitHub that it is really you. That only ensures that it's the correct user doing the pushing. You could also add an extra level of verification by signing the actual commits before pushing.

This will give you a cool badge on each commit as in the screenshot below and also other users can trust that they are actually coming from you.

A commit that is verified

There is a proprietary tool called GPG Suite that contains some tools that will help you manage your gpg keys in a GUI and integrate with Apple Mail for encrypting email. For now we'll be focusing on doing it manually with the standalone tools to avoid bloating our system.

Generating GPG keys

I assume you don't already have GPG installed but if you do you probably know what you are doing and can skip ahead. The first step is to install GnuPG, a free implementation of the OpenPGP standard.

brew install gnupg

GnuPG has multiple dependencies and when all are installed we can generate a new key pair.

gpg --full-generate-key
  • Pick RSA and RSA
  • Enter a key size of 4096
  • No expiration (unless you want it to be invalid after a certain time of course)
  • Enter your real name or your GitHub user name
  • Enter your commit email address (find out with git config --global user.email). If you aren't using a private address you really should.
  • You can enter GitHub as comment.

It may take a while (or not if you have a fast computer) and when done it will be saved under ~/.gnupg. Both the private key and the public key are saved here. The private key should never be sent to others or be lost.

Add the key to GitHub

To connect you (with your private key) to GitHub we can upload the public key to your profile. First we need to find out which key to copy, you might have multiple.

gpg --list-secret-keys --keyid-format LONG

This will output something similar like this:

/Users/name/.gnupg/pubring.kbx
------------------------------------
sec   rsa4096/EE05XXXXXXXXXXXX 2021-01-14 [SC]
      215556521B98225XXXXXXXXXXXXXXXXXXXXXXXXX
uid                 [ultimate] Name Nameson (GitHub) <1234567+user@users.noreply.github.com>
ssb   rsa4096/A226A4XXXXXXXXXX 2021-01-14 [E]

What we want here is the GPG key ID, it's the part after sec rsa4096/, so in this case it's EE05XXXXXXXXXXXX. And to copy the public key we do that with this command.

gpg --armor --export EE05XXXXXXXXXXXX | pbcopy

This will copy the long public key to your clipboard. If you rather want it to be printed in the terminal you can omit the | pbcopy part and then copy it manually. It will be a long string inside a block, the whole content should be copied including start and end parts.

-----BEGIN PGP PUBLIC KEY BLOCK-----

SUPER+LONG+STRING+IS+HERE+ABC123+ETC
-----END PGP PUBLIC KEY BLOCK-----

Now we head over to GitHub and navigate to Settings -> SSH and GPG keys and click on New GPG key.

Add a new GPG key on GitHub

Paste the public key and then save it. You'll probably need to enter you password.

Sign commits with your key

Next we need to tell git to use your newly created key when signing the commits. That is easily done by adding a property to your git config. Change the key to match your ID.

git config --global user.signingkey EE05XXXXXXXXXXXX

This is saved in ~/.gitconfig if you want to manually organize it. Please keep in mind that if you are using dotfiles synced across multiple devices this will also require the same key to be synced. You can skip the --global flag to add it to a specific repository instead. Using the same keys across multiple devices is described at the end.

When using gpg there is a deamon (gpg-agent) running in the background managing the keys regardless of what protocol you are using, but we need to add an environmental variable to let it know the current shell. Add this to your ~/.zshrc, .bashrc or whatever shell you are using.

export GPG_TTY=$(tty)

After restarting your terminal you should be able to sign your commits by adding -S or --gpg-sign to the commit command, git commit -S. gpg-agent should now show you a prompt to enter your private key password. If entered correctly and you push this commit, GitHub will validate the signing with the provided public key and then mark it as a verified one, congratulations 🥳

A verified commit on GitHub

This is all you have to do to make commit signing work. If you want to boost the process a bit more you can continue reading.

Sign commits automatically

To avoid entering the -S flag on each commit you can make signing the default behavior by adding the property to your git config.

git config --global commit.gpgsign true

If you commit often it will probably be a bit tiring after a while to enter your password every time. This can be solved by saving the password in macOS keychain, like we easily can do with ssh key passwords with the ´ssh-agent´. Unfortunately gpg-agent has no built-in support for this so we need add a separate tool.

The tool mainly used for this is called pinentry-mac which is now part of the GPG Suite mentioned in the beginning. Luckily for us it is still open-source and recently updated (compared to the initial one that was last updated 2015).

You can install the tool with brew:

brew install pinentry-mac

If not existing already create the file ~/.gnupg/gpg-agent.conf and add the following line:

pinentry-program /usr/local/bin/pinentry-mac

Then just stop the currently running gpg-agent and next time you try to sign a commit you'll be prompted to save it to the macOS Keychain.

gpgconf --kill gpg-agent

Use signing with keys on multiple devices

In short you have two choices here, either you generate a new key on each of your devices or you use the same key across all of them. If you generate new ones you also need to add them to GitHub but if you lose one device it's easy to just remove the public one on GitHub. So it's safer but also adds a bit more work and currently GitHub doesn't distinguish keys in a good way in my opinion. The latter is easier but if one device is compromised so are all of the other private keys you use, because they are the same.

When it comes to ssh keys I always generate one for each device but I don't do it for my GPG keys, even though it is a bit more secure. The reason is with an example for encrypting/decprypting email. If I receive an email that has been encrypted with my public key I need the private key to be able to decrypt and read the email. If I do this locally I need the same private key connected to the public key, different keys won't work. So I organize my gpg keys by what they are used for instead. One for each GitHub account and one for each email address.

Export your keys

We export the key the same way as we did when adding it to GitHub but we also add a flag for saving it to a file. We use the same ID as in the above example.

gpg --output my_public_key.gpg --armor --export EE05XXXXXXXXXXXX

And for the private key it's another command where you also need to enter your password.

gpg --output my_private_key.gpg --armor --export-secret-key EE05XXXXXXXXXXXX

They will be saved in your current folder when just writing the file names like this.

Transfer and import your keys on another device

Now we need to transfer them to the new device. It's very important to do this in a secure way when you include the private key, this means no email. You can use a USB flash drive or transfer them with scp for example. On the new device you need to install everything mentioned above and then you can import the keys.

gpg --import my_public_key.gpg

And another command for the private key when importing as well, combined with the password.

gpg --allow-secret-key-import --import my_private_key.gpg

And last but not least, remember to clean up all the exported keys from USB flash drives and other places. It can also be a good idea to backup your keys in a safe or password manager.