Signing Your Commits in 2023 on macOS

Let me preface this with a disclaimer: I am not a security expert, and I in fact know very little about either digital security or cryptography. This is just a little note on what I’ve learned about this topic so you don’t need to go scrounging around the internet like I did.

Edit: It has been one day since I posted this. I have now learned of the existence of Secretive and YubiKeys, and how keeping a private key in a file accessible to, say, malware, might not be the best idea.

If you have a capable Mac, I highly recommend playing around with Secretive! It and the Secure Enclave are very cool pieces of technology. Once again: let this be a lesson to not take security advice from some random blog post :)

I’ve left this post up for posterity’s sake, not because the approach here is generally advisable.

The bad old days

In the past, I used GPG to sign my commits. It sounded simple enough: just brew install gnupg, run a few commands to generate a key, paste the results into GitHub and I’d be on my way. Unfortunately, it wasn’t that easy. Maybe it’s because I’m completely clueless about this topic and I made some sort of silly mistake, but I constantly had problems.

I don’t want to bore you with my pains and give an excruciating recount of everything I can remember that went wrong with GPG commit signing. Instead, I have included one example which I think is illustrative. Feel free to skip to the next section if you’d like.

In the end, after one and a half years I gave up out of frustration and stopped signing commits entirely.

Initially, whenever I tried to sign a commit I was greeted by this error message:

$ git commit --gpg-sign
error: gpg failed to sign the data
fatal: failed to write commit object

Some googling revealed that the solution to this cryptic error is buried in the manpages (as usual). From man gpg-agent:

You should always add the following lines to your .bashrc or whatever initialization file is used for all shell invocations:

GPG_TTY=$(tty)
export GPG_TTY

It is important that this environment variable always reflects the output of the tty command. For W32 systems this option is not required.

Of course, I obliged and added this to my shell configuration.

The shiny new way

For a bit of context: in 2019 it became possible to use OpenSSH keys to sign anything, and in August of 2022 GitHub added support for signing commits with SSH keys.

I first found out about this because my university’s private GitLab instance does not allow saving HTTPS credentials; when I wanted to access a repo, I had to type my username and password in, every single time. The options here are to either store your credentials forever in plaintext, cache your credentials for fifteen minutes in memory, or set up an SSH key. None of those options sounded particularly appealing to me.

After much reluctance following my previous experience with GPG, I followed some instructions I found in GitLab’s documentation, and off I went!

Creating a key is easy: just run

$ ssh-keygen -t your-key-type-of-choice

(And yes, you don’t have to install anything because OpenSSH is pre-installed.)

The recommended key type these days is ed25519 (don’t quote me on that), so I ran

$ ssh-keygen -t ed25519
  1. Enter a filename (or just hit enter and accept the default of ~/.ssh/id_ed25519)
  2. Enter a passphrase and confirm it
  3. There is no third step!

ssh-keygen should have printed a big long string after you confirmed your passphrase. This is your key’s fingerprint. Paste this into GitLab. (Confusingly, to add an SSH key to GitHub you need to full public key, rather than just the fingerprint. You can find it in ~/.ssh/id_ed25519.pub, or whatever path you chose.)

Next, I configured Git to sign commits with my newly-generated key. Your global Git configuration is likely located in either ~/.gitconfig or $XDG_CONFIG_HOME/git/config. If you’re not sure, you can open it in your default editor with git config --global --edit.

[user]
name = Foo Bar
email = [email protected]
signingkey = /Users/foo/.ssh/id_ed25519

[gpg]
format = ssh

You can also tell Git to automatically sign every commit instead of having to use --gpg-sign every time:

[commit]
gpgsign = true

Unfortunately, this results in a prompt to enter your passphrase each and every time you make a commit. The solution to this is wonderfully simple: first, add your key to the macOS Keychain with

$ ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Then, add this single line to ~/.ssh/config:

UseKeychain yes

And now, after entering your passphrase one last time the next time you sign a commit, you will never have to type it again! If you have iCloud Keychain enabled this should also sync over iCloud to any other Macs you might have, though I don’t have another one to test this out.

Finally, you might like to confirm that everything’s worked. You can view your commit history, along with whether the commits have been signed with a valid signature, using

$ git log --show-signature

If you’ve followed everything correctly so far, this should yield something like the following output:

$ git log --show-signature
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
commit 366b70abf747b990fd4c6c239ce55bae012aabd1 (HEAD -> main)
No signature
Author: Luna Razzaghipour <[email protected]>
Date:   Thu Mar 2 20:12:18 2023 +1100

    Initialize repository

What this error message is trying to say is that you have to tell Git what email and key combinations you personally have reviewed to be correct (who you “trust”).

Let’s start by creating ~/.gitallowedsigners. If you prefer, you can use $XDG_CONFIG_HOME/git/allowed_signers or whatever path you like.

Here, I’ve given my email and key for example’s sake. Of course, you should fill in your own values instead. You can find your public key in ~/.ssh/id_ed25519.pub (or whatever path you chose). If you trust me, though, you can leave my email-key pair in there :P

[email protected] ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTxD1PsPaEPdLiwXOEdINDuwv8Pn3E+GvkeJ75LyV4qEjxjmnwRzUCbn8k497Sy3jbTuNR/c7SX5/w5S7tpl4o=
[email protected] public-key-here

Next, we need to tell Git about this file:

[gpg "ssh"]
allowedSignersFile = /Users/foo/.gitallowedsigners

If you now run that git log command from before, you should see something like this instead:

$ git log --show-signature
commit 366b70abf747b990fd4c6c239ce55bae012aabd1 (HEAD -> main)
Good "git" signature for [email protected] with ED25519 key SHA256:Kywsc1vHG9+wPIxhxODjWIPvSdz5cteDXqpkYCB1qDo
Author: Luna Razzaghipour <[email protected]>
Date:   Thu Mar 2 20:12:18 2023 +1100

    Initialize repository

My verdict

In summary, creating and managing keys with OpenSSH is such a breath of fresh air, even for someone who’s barely used GPG. I mean, I loved it so much that I was motivated enough to write a blog post. What more proof do you need?

I was elated to discover that keys are just text files. There is no database of keys, there isn’t even a directory the keys have to live in! To reuse the default key name from the example above: ~/.ssh/id_ed25519 contains the private key, and ~/.ssh/id_ed25519.pub contains the public key. I could just as well have entered any path on the filesystem, and everything would’ve worked perfectly.

Take fingerprints, for example: there exists a notion of a fingerprint, which is essentially a shortened, transformed version of an SSH public key. To generate this, simply run

$ ssh-keygen -l

and it’ll prompt you for a path. This path can be either the path to a public key or a private key; the same exact fingerprint can be derived from either. Alternatively, you can use

$ ssh-keygen -lf ~/.ssh/id_ed25519

to avoid the interactive prompt.

Delightfully, during key generation ssh-keygen prints not only the fingerprint, but also an ASCII pictoral representation of it (“randomart”). You can view it again after you’ve generated your key with

$ ssh-keygen -lvf ~/.ssh/id_ed25519

For example, here is the output from the key I use for signing commits:

$ ssh-keygen -lvf ~/.ssh/github
256 SHA256:lmMzeq1AyvNz2MiIWA/tPnteHkr8zyHsD2x9gSZaBbk no comment (ECDSA)
+---[ECDSA 256]---+
|        ..       |
|        ..       |
|         ..      |
|        Eo .     |
|   .  . S o .    |
|  o..+ O O   .   |
| o =+o*+X + .    |
|. . =+*Xo* o     |
|   .o=+o=o+      |
+----[SHA256]-----+

You can recreate this for yourself with the following command. Seriously, try it! It fetches my public key from GitHub, and then runs it through ssh-keygen:

$ curl https://github.com/lunacookies.keys \
	| ssh-keygen -lvf /dev/stdin

Now, since I plan on signing every commit and tag I create, I’ve enabled GitHub’s vigilant mode. This way each of my commits has either a “Verified” or “Unverified” badge next to it. It is my hope that, with SSH keys being so easy to work with and with GitHub and GitLab both supporting them, more developers will start signing their commits.

Luna Razzaghipour
2 March 2023