Signing Git Commits on the Terminal

1. Introduction

git itself is cryptographically secure, but over the last few years it’s been trendy to sign your commits with GPG. By signing your commits, you colleagues can be slightly more confident that your commits were truly written by you.

Configuring git to allow you to sign your commits is pretty easy: just create a new private key in GPG (if you don’t already have one) and store its fingerprint in git’s user.signingkey config variable. If you then commit with git commit -S, GPG will prompt you for your password and you’re good to go.

pinentry-gui.png
Figure 1: An example PIN-entry GUI.

It takes very little effort to fold this into your development workflow. GPG even comes with gpg-agent: a daemon that temporarily caches your password. With gpg-agent running, you only need to re-enter your password a few times per day.

Once you’re comfortable with this workflow, you can tell git to sign your commits by default, by settings its commit.gpgsign config variable to true.

2. The Problem: PIN-entry on the Terminal

Signing your git commits with GPG works perfectly fine, until that fateful day when you need to commit some code in a non-X11 session. Most likely, you’ll encounter this when you’ve brought your laptop on the road, and need to SSH into your dev-machine at home and commit a few quick changes. Unfortunately for you, after entering your well-formatted commit message, git blocks for about 30 seconds then spits out the following error:

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

You were never prompted for your password and GPG eventually gave up waiting for it. Uh oh! Eventually you’ll probably give up and commit via git commit --no-gpg-sign, but what happened here?

This root-cause of this issue is the ever-helpful gpg-agent: that fellow that caches your password so you don’t need to re-enter it every time. When git asks GPG to sign your commit, it in turn asks gpg-agent for your credentials and it’s gpg-agent that displays the password-prompt. gpg-agent is a daemon, typically started when you last sat at your desk and logged into your X11 desktop environment. It’s attached your X11 session (so that it can show you the GUI prompts), but it has no idea whether you, the user, have access to that same X11 session! Regardless of where you are, it’s going to show the password-prompt on your screen at home. Eventually this GUI prompt will timeout, resulting in your terminal session printing out the above error.

3. The Solution: Smarter PIN-entry choice

Ultimately, we want to find a way for GPG to prompt us with a GUI, when we’re in an X11 session, and on the console when we’re not. If it only uses a GUI prompt, we can’t commit through SSH. If it only prompts us on the terminal then it won’t work if we want to commit via our IDE/visual-editor. We want both, dammit! And with a tiny bit of shell scripting, we can have `em.

3.1. GPG: Specify PIN-entry Mode

To better slot into a variety of desktop environments, GPG provides a general interface for prompting the user for their password. If you’re using KDE, you can use pinentry-qt; for Gnome: pinentry-gtk. If you’re totally wild, you can even have Emacs prompt you, via pinentry-emacs.

  • We’re really getting into the weeds here, with respect to delegation, but when you call git commit -S, git delegates to gpg2, which delegates to gpg-agent, which delegates to pinentry-gtk (for example), which finally displays the GUI prompt you can type your password into.

Fortunately for us, GPG provides a --pinentry-mode=loopback command-line argument. The option essentially means “get your own damn password!” It redirects password-entry back to gpg2 (skipping whatever third-party “pinentry” program you have installed). In gpg2’s case, it will then prompt you directly on the terminal.

In our case, we want to use gpg2 --pinentry-mode=loopback when not using X11, and gpg2 --pinentry-mode=default (or just gpg2) otherwise.

3.2. Git: Swap out the Signing App

Out of the box, git will sign our commits using gpg2 (aka: gpg2 --pinentry-mode=default), but like most things, we can configure it to use another application, by assigning a different path to the gpg.program config variable:

git config --global gpg.program "/usr/local/bin/my-gpg2"

For our purposes, there are a couple of problems with this:

  1. It doesn’t accept command line arguments. You can’t do:

    git config --global gpg.program "gpg2 --pinentry-mode=loopback"
    
  2. We want to use a different gpg.program depending on whether we’re in an X11 session or not.

3.2.1. Use a Script for gpg.program

It’s easy to solve our first problem. Just whip up a quick script that includes your preferred command-line arguments:

1: #!/bin/sh
2: exec gpg2 --pinentry-mode loopback "$@"

3.2.2. Configure git on the fly

Instead of permanently changing the gpg.program in your git config file, you can temporarily set it via a command-line argument:

# When in an SSH session:
git -c 'gpg.program=/usr/local/bin/gpg2-loopback' commit -S

# When in an X11 session (use the default gpg.program):
git commit -S

3.3. Automatically choose PIN-entry mode

It’s nice that we have some way forward: one method that works in X11 and one that works on the terminal. It certainly doesn’t “spark joy” however. I don’t know about you, but I’m simply not going to remember to do the git -c 'gpg.program=foobar' version. We already had to write a script for this, so let’s make it a tiny bit smarter:

1: #!/bin/sh
2: if [ -n "$DISPLAY" ]; then
3:   exec gpg2 "$@"
4: else
5:   exec gpg2 --pinentry-mode loopback "$@"
6: fi

This version takes advantage of X11’s DISPLAY environmental variable. This variable must be present during an X11 session, so that GUI clients know which X11 server to connect to. When we’re in a terminal-only session, this env-var shouldn’t be present, as our session doesn’t have access to an X11 server, even if one is running on the system.

Now that we have a single script that does everything we want, we can permanently set it in our git-config then commit in the same way, every time:

git config --global gpg.program "/usr/local/bin/gpg2-display-aware"
git commit -S # Boo-yah!