Back to signing git commits with GPG

Decided to move back to GPG for commit signing from SSH. Here are the steps I took and why I took them.

Photo of a picnic. Woman is sitting on a blanket, a water bottle rests on the ground, she's writing a signature on a phone.
Photo by DocuSign on Unsplash

A while ago I wrote about signing commits with an SSH key. My main reasoning then was that I can remove the GPG suite from the computer, and ssh-agent is easier to deal with.

Signing commits with SSH however has a few drawbacks compared to GPG when it comes to GitHub. From their documentation:

SSH signatures are the simplest to generate. You can even upload your existing authentication key to GitHub to also use as a signing key. Generating a GPG signing key is more involved than generating an SSH key, but GPG has features that SSH does not. A GPG key can expire or be revoked when no longer used. GitHub shows commits that were signed with such a key as "Verified" unless the key was marked as compromised. SSH keys don't have this capability.

I love simple things, but what I love even more is the correctness that comes from properly dealing with verifications.

During the time I’ve been using SSH keys to sign commits, I’ve bumped into a few instances where I had to mark my signing SSH key as expired or otherwise no longer in use. The effect of that was that every single commit I’ve signed with that SSH key suddenly became Unverified, which is just not a good look. Had I used GPG keys, even though they’re more fiddly to set up, they would still be verified, unless I mark that key as compromised.

Back to GPG keys then

Step one: create a new GPG key and tell GitHub about it

I’m on a mac and following GitHub’s documentation on adding a new GPG key, so here’s what I did:

  1. Checked for existing ~/.gnupg directory. There was one, so I deleted everything from within that folder, except for the two .conf files: gpg-agent.conf and gpg.conf
  2. Installed GPG again. I’m on a mac, so brew install gpg did that for me. Current latest version as of writing this post is 2.4.5. That installation also pulled in pinentry as a dependency.
  3. Checked the correct path of pinentry with which pinentry. I copy that path into the ~/.gnupg/gpg-agent.conf file and make sure that the pinentry-program setting in that file has the correct path.
  4. Double checked that the permissions of the ~/.gnupg folder and its contents are sufficiently locked down. It should be 600 for the files inside, and 700 for the directory itself. See this GitHub gist for more info on the correct commands to set them up.
  5. Started generating a key by following the GitHub documentation with gpg --full-generate-key.
  6. At the prompt hit enter to accept the default key type to sign and encrypt things with.
  7. Hit enter to accept the default elliptic curve to use: 25519.
  8. When choosing expiry, you need to take context into account. I’m on a personal mac, so no expiry is fine, however on a corporate laptop I would make sure the expiry complies with whatever documentation I am bound by. It’s probably going to be 3 months or something similar. Check your paperwork for this. GPG will double check when the key is going to expire. If you’re happy with it, select y, and hit enter.
  9. Now comes the user info time. You should add your real name, an email address that you’ve also added, or you plan to add, to GitHub, and a comment. The comment will be important to figure out which one to revoke or expire when choosing from a long list of GPG keys. You probably don’t have more than a handful, but add something meaningful. I chose Created on 2024-05-27 on personal M1 mac for comment so I can differentiate from the others later. You’ll get to a point where GPG will summarize the info and ask you if it’s okay. I do not proceed yet, the next step is going to ask for a passphrase.
  10. Let’s generate that passphrase. I used 1password, but really any password manager will do as long as you don’t need to remember what it is. Once I had the passphrase copied onto my clipboard, I go back to the terminal.
  11. I select “Okay” to confirm my personal details are correct, and pinentry asks for a passphrase. You can choose to not have one, however I always recommend it. Once I pasted the passphrase into the first input, pinentry told me they don’t match. Advance with <tab>, paste again, they match, advance to the OK button, and close. I now have a new GPG key!
  12. Let’s list that, as that’s the next step. The command is gpg --list-secret-keys --keyid-format=long. Note: if the permissions on the directory are whack, GPG will tell you here. Refer back to point [4] on how to fix that!
  13. From there you’ll need to copy the key ID from the sec section after the /. Refer to the GitHub documentation for an example.
  14. Export, or print the ASCII version of the key by using gpg --armor --export 3AA5C34371567BD2 where the 3AA... bit is the key ID you copied in [13]. This is using the example from GitHub.
  15. Copy all of that between the --—BEGIN PGP PUBLIC KEY BLOCK----- and --—END PGP PUBLIC KEY BLOCK-----, including those lines, and add a new GPG key in your GitHub settings/keys page.

We’re done with this part. The next is to actually use it!

Step two: use the key locally

For this we need to do two different things: first we need to tell the local git command to use that GPG key to sign commits instead of the SSH key it’s been using, and second we need to make sure that we don’t need to input the passphrase every single time we do anything with the key. I hope that pinentry here should help us.

Let’s start with the second option. For that to work, I’ve added the following to my .zshrc file. If you use a different shell, add it to your relevant .rc file:

export GPG_TTY=$(tty)
alias testgpg="echo \"test\" | gpg --clearsign"
alias killgpg="gpgconf --kill gpg-agent"

The first line is required per the gpg documentation. Check that with man gpg-agent. The other two are convenience aliases. The first one, testgpg will try to sign a message. Ideally it will ask for a passphrase the very first time, but then never again because it should store that in your keychain.

Sadly with the current state of affairs it will ask every time the gpg-agent restarts, whether that’s because of a computer restart, or you manually terminate the running instance with the killgpg command. This might be good enough, but what are the alternatives?

Pinentry-mac

On a mac this is the canonical answer. Installing it is easy, you need to use brew:

$ brew install pinentry-mac

And then modify your ~/.gnupg/gpg-agent.conf file and set the value of the pinentry-program key to wherever brew installed pinentry-mac. On my device that setting looks like this:

pinentry-program /opt/homebrew/bin/pinentry-mac

If you restart your GPG agent now, and try to sign a commit, you’ll be greeted by a mac window rather than a terminal input window.

There’s an option in the bottom right corner there that asks whether you want to save the passphrase in your keychain. You should select yes to that.

Screenshot of the pinentry-mac dialogue window.

Pinentry-touchid

That one is supposed to ask for your fingerprint when it needs to access a key, but I couldn’t get this set up to actually work on my computer that does have a touchid sensor.

If you’d like to give it a go, here’s the repository: https://github.com/jorgelbg/pinentry-touchid, and pinentry-mac is a prerequisite for it. It’s not something I want to spend any more time on.

With this setting up consistent and stable local signing of messages using a passphrase protected GPG key is done, on to telling git about it!

I’m following GitHub’s relevant documentation page: telling git about my signing key. Here are my steps:

  1. Because I’ve been using an SSH key to sign commits with, I need to unset the gpg.format option, so the default will be used. That’s the git config --global --unset gpg.format command.
  2. Much like when we were generating the GPG key, I need to list the keys I have on the system so I can grab its key ID: gpg --list-secret-keys --keyid-format=long. From there I can copy the relevant section, refer to the linked GitHub page to see what you’ll need if you’re following along.
  3. Let’s actually set that key to be the one to sign stuff with: git config --global user.signingkey <the key ID>.
  4. The next step wants to set commit.gpgsign = true, however I already have that as I’ve been signing my commits with SSH. I don’t need to run this.
  5. Step 7 in the GitHub doc essentially adds the export GPG_TTY=$(tty) line to your .rc file, which you should have already done by now as I touched on this earlier in this post.
  6. And lastly GitHub tells you you should install pinentry-mac if you’re on a mac. Way ahead of them 😅.

Time to test this out!

I switched to a new branch on one of my projects, did some modifications, added those to a chunk to be committed, and actually committed it, and git did not complain. But was it actually signed?

Luckily I wrote about this not too long ago when I set up the SSH version that I’m now moving away from, but the command you want is the following with the expected output as well:

$ git verify-commit <sha>
gpg: Signature made Tue 28 May 01:11:03 2024 BST
gpg:                using EDDSA key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
gpg: Good signature from "Gabor Javorszky (Created 2024-05-27 on personal M1 mac) <xxxxx@xxxxxx>" [ultimate]

Pushing the branch up to GitHub also correctly recognises that that commit was signed by my GPG key even though the immediately preceding commit was signed using an SSH key.

All is good, we can go home. This process used to be so much more confusing with so many more footguns! Luckily it’s no longer the case, though I can’t tell whether the availability of good documentation helps, the organisation of the tools, or I got more experienced since the last time I tried to set this up.

Hopefully this was useful to you. If you tried to do this yourself but ran into a problem, find me on mastodon and ping me, I’ll do my best to help!