More fun with Yubikey: Signed Git commits and GPG agent forwarding
I’ve been on a “What other neat things can I do with my Yubikey” kick after my last post, and it turns out one of those neat things is to cryptographically sign Git commits. This allows you to prove that the owner of a particular GPG key is actually the person who committed the code.
Setting up signed Git commits locally is very easy, run git config --global user.signingkey "<ID of your GPG signing subkey>"
(mine is C65E91ED24C34F59
as shown in the screenshot below), then run your Git commit normally but with the added flag -S
to sign it.
Bitbucket Cloud doesn’t currently support displaying signed Git commits in the UI, but you can do it on GitHub and you get a shiny little “Verified” badge next to each one and this message when you click on it:
You can also show it locally with git log --show-signature
.
This is all well and good, but what if you want to sign something on a remote server that you’re connected to via SSH? Enter GPG agent forwarding!
Just like you can do SSH agent forwarding to have your private SSH key securely forwarded to a machine you’re connecting to, you can do the same with the GPG agent that stores your GPG keys and allow it to access your signing subkey. Setting up GPG agent forwarding is broadly straightforward, but make a note of which versions of GNUPG you’re running at each end. The “modern” version is 2.1 and higher, I’m running 2.2.x on my Macs but my Linode runs CentOS 7 which only comes with GPUPG 2.0.x and I wasn’t able to fully get agent forwarding working between it and 2.2.x on my Macs. I tested the latest Debian with 2.1 and that worked.
I followed this guide, but one extremely important note is that you can’t use a relative path for the local or remote sockets, they have to be the full absolute path. This becomes a pain when you’re connecting to and from different OSes or machines where your username differs. Thankfully, SSH has a Match exec
option where you can run a command to match different hosts and use different host definitions (and thus put in different paths for the sockets) depending on your local and remote machines.
Mine looks like this :
# Source machine is a personal Mac, connecting to another personal Mac on my local network; the local network all uses the .core domain internally
Match exec "hostname | grep -F .core" Host *.core
RemoteForward /Users/virtualwolf/.gnupg/S.gpg-agent /Users/virtualwolf/.gnupg/S.gpg-agent.extra
# Source machine is a personal Mac, connecting to my Linux box
Match exec "hostname | grep -F .core" Host {name of the Host block for my Linode}
RemoteForward /home/virtualwolf/.gnupg/S.gpg-agent /Users/virtualwolf/.gnupg/S.gpg-agent.extra
# Source machine is my work Mac, connecting to my Linux box
Match exec "hostname | grep -F {work machine hostname}" Host {name of the Host block for my Linode}
RemoteForward /home/virtualwolf/.gnupg/S.gpg-agent /Users/{work username}/.gnupg/S.gpg-agent.extra
(Yes, technically this doesn’t work as I mentioned at the start due to my Linode being on CentOS 7 and having GNUPG 2.0, but the socket forwarding bit works, just not when I actually want to do anything with it. :P)