How to use GPG with YubiKey (bonus: WSL)

Recently I spent a week investigating the use of YubiKeys to increase the security of our company. In the process, I've read many different tutorials on using GPG (GNU Privacy Guard), some more up to date, some less, and had to piece together information on getting GPG + YubiKey working from WSL. To simplify this process for the next person (and to have something I can link people to when they ask), I've decided to write everything down[1].

This post assumes that you already know what GPG is, and why you want to use it, but you don't have your own set of keys yet. If you are further along (e.g. you already have your own set of keys), you can skip those parts and use your already existing keys instead of generating new ones.

It also assumes that your YubiKey can hold 4096 bits RSA keys. As far as I know, this is true for all of the 5th generation Yubikeys, but it is not true for the YubiKey 4 NFC. If your YubiKey can only hold 2048 bits RSA keys, you will need to generate smaller subkeys in the appropriate step (the master key should still be kept at 4096 bits).

Step 1: Install and set up GPG

If you are using Windows, you will need gpg4win. When I was writing this post, the latest version was 3.1.5.

If you are on Linux, you likely already have gpg installed, but you should check it's version -- e.g. on Ubuntu 16.04 LTS, gpg is GPG in version 1.4.20. I strongly recommend getting GPG in version 2.x.x.

If you want to use gpg from within WSL together with YubiKey, you have to install gpg in version 2.x.x inside WSL and install gpg4win on the side of Windows.

Settings

On Windows, GPG (and related) settings are in AppData/Roaming/gnupg. On Linux, the settings can be found in ~/.gnupg/. The settings files themselves are gpg.conf for the gpg binary, scdaemon.conf for the SmartCard daemon and gpg-agent.conf for the gpg-agent.

These will become important later, but if you are on Windows I recommend placing charset utf-8 into gpg.conf straight away.

Step 2: Generate a new set of keys

After the previous step, you should have GPG set up and ready to generate keys. In my case, the executable name ended up being gpg2, so I will use that in examples through this post. We will need to generate 3-4 keys, or rather 1 key and 2-3 subkeys. They will be

  1. A master key that should be backed up and kept strictly offline,
  2. An Encryption key, a subkey of the master key that is used for encryption
  3. A Signing key, a subkey of the master key that is used for signing
  4. An (Optional) Authentication key, a subkey of the master key that can be used for SSH or similar

The master key is used for issuing/revoking subkeys and confirming other people's identities. This makes it essentially a person's online identity and thus should be kept securely backed up on offline medium and removed from the PC where it was generated afterwards.

The Encryption and Signing keys are the keys used during everyday activity, and because they are bound to the master key, if they are ever compromised they can be easily revoked, at least as long as you retain control over your master key. The Authentication key is a bit different in that some people think it is pointless (or even that it shouldn't be used), while other people are using it regularly. This post assumes that you will want to use it as well, but you can always just skip those steps.

Generating the master key

Because GPG (at least in the version I have) still defaults to 2048 bits RSA keys and we want to generate 4096 bits RSA keys in the interest of future-proofing[2], we will have to run GPG with the --full-key-gen option so we can customize properties of the generated key. GPG will then ask you about various properties of your new key, as you can see below, where gpg X> means that GPG is asking you about X:

$ gpg2 --full-gen-key
gpg keytype> 1 (RSA and RSA)
gpg keysize> 4096
gpg expiry> 3y
gpg correct> y
gpg real name> ${your name}
gpg email addr> ${your email}
gpg comment>
gpg okay> O
gpg passphrase> ${password to protect this key}

You should always have your key expire eventually -- as long as you have access to it, you can extend its expiration date when it becomes relevant -- and you should also always have a passphrase on your master key. Real name and email address are hopefully self-explanatory, but comments are controversial. Some people hold the opinion that comments are a mistake and should not be used[3], while other people see comments as OK, as long as you avoid pointless redundancy (e.g. repeating your email address in the comment). Personally, I don't really care if your user-id looks like this

Sir Mix-A-Lot (I like big butts, and I cannot lie) mixalot@yahoo.com

but I would prefer if it didn't look like this

Jan Novák (jan.novak@gmail.com) jan.novak@gmail.com


If the key generation was successful, you should see something like this:

gpg: key 6129F208 marked as ultimately trusted
gpg: directory '/home/xarn/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/xarn/.gnupg/openpgp-revocs.d/1356ED7D349B649687E5D1ECA8F90C096129F208.rev'
public and secret key created and signed.

gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: PGP
gpg: depth: 0  valid:   2  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: next trustdb check due at 2021-11-04
pub   rsa4096/6129F208 2018-11-09 [S] [expires: 2021-11-08]
      Key fingerprint = 1356 ED7D 349B 6496 87E5  D1EC A8F9 0C09 6129 F208
uid         [ultimate] Jan Novák <email@example.com>
sub   rsa4096/BF36D4AC 2018-11-09 [] [expires: 2021-11-08]

This tells you that 2 keys were created, a master key with id 6129F208 and an encryption sub key with id BF36D4AC. For now, the master key id is the important one, the subkey id is not. Also note that both of these ids are in so-called "short" (32 bits) format, which is generally considered insecure, and either the long (64 bits) key id, or the full key fingerprint should be used instead. To get the long key id, you can pass --keyid-format long flag to gpg, e.g.:

$ gpg2 --list-keys --keyid-format long
/home/xarn/.gnupg/pubring.kbx
-----------------------------
pub   rsa4096/A8F90C096129F208 2018-11-09 [SC] [expires: 2021-11-08]
uid                 [ultimate] Jan Novák <email@example.com>
sub   rsa4096/72FBD8C2BF36D4AC 2018-11-09 [E] [expires: 2021-11-08]

This means we actually want to use A8F90C096129F208 as the master key id during the next steps.

Because we are using newer gpg, something called revocation certificate has also been generated -- a revocation certificate can be uploaded to key servers if you lose control of a key to mark the key as invalid. Obviously, you should back up the revocation certificate somewhere.

Adding more user ids

You might want to have more than one user identity (userid) in your master key. This is primarily used to either connect an internet screen name with a real-world name or to add associate more email addresses with your identity. In either case, you can do that by editing the master key:

$ gpg2 --edit-key A8F90C096129F208
gpg> adduid
Real name:
Email address:
Comment:

Generating subkeys

We already have the encryption subkey, now we also have to add the signing and authentication subkeys. This is done by editing the master key in expert mode (note that without --expert we can't set the key type by ourselves) and using the addkey command:

$ gpg2 --expert --edit-key 6129F208
gpg> addkey
gpg key-kind> 8 (RSA, own capabilities)

this will open up a menu where you can select what capabilities the new key should have. When using it, keep in mind that "toggle" does mean toggle and that the key starts with the S(ign) and E(ncryption) bits enabled. After you select the right set of capabilities (for this tutorial it means the key has only S or only A capability), you will get to the dialogue for creating keys -- set the size of the key to 4096 bits, expiration date to something reasonable and pick a passphrase again.

After creating both S(ign) and A(uthentication) keys, you should end the editing session and check that your keys were created properly:

gpg> save
xarn@DESKTOP-B2A3CNC:~ :) gpg2 --list-keys --keyid-format long

You should see something like this:

/home/xarn/.gnupg/pubring.kbx
-----------------------------
pub   rsa4096/A8F90C096129F208 2018-11-09 [SC] [expires: 2021-11-08]
uid                 [ultimate] Jan Novák <email@example.com>
sub   rsa4096/72FBD8C2BF36D4AC 2018-11-09 [E] [expires: 2021-11-08]
sub   rsa4096/94D8AB7C17FCE986 2018-11-09 [S] [expires: 2021-11-08]
sub   rsa4096/03F0A89596D8D340 2018-11-09 [A] [expires: 2021-11-08]

i.e. 4 keys, 3 of which are subkeys (marked with sub) and each of the subkeys has only one of the A/E/S capabilities.

Publishing and backing up the master key

Now that we have our keys ready, it is time to

  1. Publish the public part of the key
  2. Backup and securely store the private parts of the master key

Publishing is easy enough, as long as you find a keyserver that accepts uploads. I had some trouble finding one, but as of the time of writing, fks.pgpkeys.edu worked:

$ gpg2 --keyserver fks.pgpkeys.edu --send-key A8F90C096129F208

If this succeeds, people can download your key by its id from the public key server pools.

Backing the key up is also relatively simple, the first step is to export it. This is usually done in a format called ASCII armor, because cating a binary file into your terminal is no fun:

$ gpg2 --armor --export-secret-key A8F90C096129F208 > secret-key.asc

The second step is to securely back up secret-key.asc -- the usual recommendation is to use 1 or more encrypted USB cards. You should also delete the master key from the computer, but doing so right now would prevent you from moving the subkeys to the YubiKey.

Step 3: Setting up the YubiKey

If you used gpg inside WSL to generate your keys, you will have to first set up a bridge between gpg-agent inside WSL and gpg-agent inside Windows. See "Extras: gpg-agent bridge" for details.

First, we need to check that gpg can see the YubiKey when it is plugged in -- If it does not, check section "Extras: gpg does not detect YubiKey" for help.

$ gpg2 --card-status

Reader ...........: Yubico YubiKey OTP FIDO CCID 0
Application ID ...: D2760001240102010006090200580000
Version ..........: 2.1
Manufacturer .....: Yubico
   <snip>

Moving subkeys to the YubiKey

The option to move keys to the YubiKey is once again under --edit-key:

$ gpg2 --edit-key A8F90C096129F208
gpg> key 1
gpg> keytocard
gpg> <pick the right slot>
gpg> <repeat for the other keys>
gpg> save

keytocard is a destructive operation and removes the private subkey from the local key store. Now that the subkeys are stored on the YubiKey, you should delete the master key. To do that, you need to know its keygrip:

gpg2 --list-secret-keys --with-keygrip
/home/xarn/.gnupg/pubring.kbx
-----------------------------
sec   rsa4096/6129F208 2018-11-09 [SC] [expires: 2021-11-08]
      Keygrip = 5436620CA40373692E45B41A7831BEC2ACE624AB
uid         [ultimate] aslkdjfs (sjsj)
ssb>   rsa4096/BF36D4AC 2018-11-09 [E] [expires: 2021-11-08]
      Keygrip = D75AA532535A5E93C90353A3F273C0391FE25516
ssb>   rsa4096/17FCE986 2018-11-09 [S] [expires: 2021-11-08]
      Keygrip = B14D4AE1729E43DD1E1304C6CA083DA1CA8C6059
ssb>   rsa4096/96D8D340 2018-11-09 [A] [expires: 2021-11-08]
      Keygrip = 2F35594B4CFBA552BD73E4542065E7988BDE1564

from the listing above, the keygrip of the master key is 5436620CA40373692E45B41A7831BEC2ACE624AB and it can be deleted via

$ gpg-connect-agent "DELETE_KEY 5436620CA40373692E45B41A7831BEC2ACE624AB" /bye

You can verify that it has been deleted by listing the private keys again -- the master key should have a # next to it to signify that it cannot be used (the > next to the subkeys means that they are on the YubiKey).

Change YubiKey's PIN

All YubiKeys share the same factory PIN, 123456, and the same admin PIN, 12345678. Because PIN is what the YubiKey asks for to use a key, you need to change it. You can either do this via Yubico's management utility or via gpg:

$ gpg2 --change-pin
gpg> 1 (change PIN)
gpg> 3 (change admin PIN)
gpg> q

Enable touch protection for GPG keys

I also recommend enabling touch protection for GPG keys on the YubiKey. This means that to use any of the GPG keys on the YubiKey, you need to do 2 things:

  1. Enter the PIN (this is usually cached for a couple of hours)
  2. Touch the YubiKey's touch sensor

The upside is that even in the case a piece of malware manages to get onto your machine and intercepts your PIN, it still will not be able to use the GPG keys on your YubiKey. The downside is that you will be made painfully aware of every single usage of your GPG keys, which can sometimes be annoying[4].

To enable touch protection, you will need the Yubikey Manager utility. Once you have it installed, you can enable the touch protection for each key slot separately:

$ ykman openpgp touch sig on
$ ykman openpgp touch aut on
$ ykman openpgp touch enc on

And that is it, now you have your GPG subkeys on the YubiKey, the YubiKey is set up correctly and you should be able to just use it with gpg.


Extras:

git config

Signing git commits seems to be the most common reason for using GPG, so here are the necessary configuration steps.

  1. Tell git to use the right version of gpg. If you are using Git for Windows, it will likely try to use the wrong gpg binary. Similarly, if you had to install gnupg2 package to get modern gpg, you need to configure git to use gpg2 instead of gpg binary.
# Windows
git config --global gpg.program "C:\Program Files (x86)\GnuPG\bin\gpg.exe"
# Linux
git config --global gpg.program gpg2
  1. Tell git which key to use
git config --global user.signingkey <signing-subkey-id>
  1. Tell git to sign each commit
# Add --global if you want to sign every commit of every git tree
# Keep it like this to only enable signing for this specific tree
git config commit.gpgsign true

SSH Authentication via GPG key on YubiKey

This section does not apply to using YubiKey for SSH auth inside WSL.

To use your Auth subkey for SSH auth, you need to enable ssh support in gpg-agent.
To do so, you need to add enable-ssh-support to gpg-agent.conf, restart the gpg-agent and set it up to run on login (so that it is available when SSH asks for keys). You also need to set environment variable SSH_AUTH_SOCK to ~/.gnupg/S.gpg-agent.ssh.

You can check that everything works with ssh-add -L -> you should see the auth key from YubiKey in SSH format.

Note that keys in Auth slot on the YubiKey are given to SSH even if they are not in the sshcontrol file.

Troubleshooting -- GPG does not see the YubiKey

The most common reason for GPG not to see the YubiKey is that there are multiple SmartCard readers in the system. This is caused by the fact that if there is more than one SmartCard reader in the system, scdaemon just defaults to checking the first one and if that is not a GPG compatible smart card (in our case the YubiKey), it does not try the other ones.

To solve this, you will need to add reader-port <port id or device name> to scdaemon.conf. You can find the proper name from scdaemon logs, because it enumerates all readers, even though it only picks one:

# scdaemon.conf
debug-level guru
log-file <path>

Afterwards, you need to find lines saying "detected reader", specifically the one talking about YubiKey.

# scdaemon.log:
2018-11-06 18:11:14 scdaemon[11056] detected reader 'Alcor Micro USB Smart Card Reader 0'
2018-11-06 18:11:14 scdaemon[11056] detected reader 'Yubico YubiKey OTP+FIDO+CCID 0'
2018-11-06 18:11:14 scdaemon[11056] reader slot 0: not connected

Going by this log you should set reader-port to Yubico YubiKey OTP+FIDO+CCID 0.

WSL GPG bridge

Because the only devices visible from WSL are drives, which the YubiKey is not, gpg inside WSL cannot use the YubiKey directly. Luckily, we can work around that by redirecting requests to gpg-agent[5] under WSL to the gpg-agent running under Windows.

This can be done by combining the npiperelay utility on the Windows side with socat on the Linux side.

Getting npiperelay.exe

There are two ways to get the npiperelay.exe binary

  1. You can download it from the GitHub releases
  2. Build it yourself

The second option has a small problem in that if you install an older version of go (e.g. 1.6.2 from apt on Ubuntu 16.04), it will compile fine, but it will fail at runtime, and that the Readme in the linked repo is not updated to reflect the fork's address.

Setting things up

You will need the Windows-side gpg-agent to run right after startup. The easiest way of doing that is to add a shortcut to "C:\Program Files (x86)\GnuPG\bin\gpg-connect-agent.exe" /bye in the %AppData%\Microsoft\Windows\Start Menu\Programs\Startup folder. You should also set the shortcut to run minimized, to avoid pointless cmd pop-up on login.

On the WSL side, you should add this to ~/.profile or similar:

#####
## Autorun for the gpg-relay bridge
##
SOCAT_PID_FILE=$HOME/.misc/socat-gpg.pid

if [[ -f $SOCAT_PID_FILE ]] && kill -0 $(cat $SOCAT_PID_FILE); then
   : # already running
else
    rm -f "$HOME/.gnupg/S.gpg-agent"
    (trap "rm $SOCAT_PID_FILE" EXIT; socat UNIX-LISTEN:"$HOME/.gnupg/S.gpg-agent,fork" EXEC:'/mnt/c/PATH_TO_NPIPERELAY/npiperelay.exe -ei -ep -s -a "C:/Users/WINDOWS_USERNAME/AppData/Roaming/gnupg/S.gpg-agent"',nofork </dev/null &>/dev/null) &
    echo $! >$SOCAT_PID_FILE
fi

with paths modified accordingly.

WSL SSH bridge

I was unable to create a bridge between the WSL gpg-agent and Windows gpg-agent that would use gpg-agent's ssh support[6], but I managed to get it to work with gpg-agent's PuTTY support thanks to WSL-SSH-Pageant, and here are the steps:

  1. Enable PuTTY support in Windows-side gpg-agent by adding enable-putty-support to gpg-agent.conf, and restarting the gpg-agent.
  2. Get a wsl-ssh-pageant.exe, either from the GitHub Releases page or by compiling it yourself. Once you have it, you need to pick a path where it and a socket file will live -- I picked c:\ubuntu\wsl-ssh-pageant\, so the path to the executable is c:\ubuntu\wsl-ssh-pageant\wsl-ssh-pageant.exe and to the socket is
    c:\ubuntu\wsl-ssh-pageant\ssh-agent.sock.
  3. Set WSL environment variable SSH_AUTH_SOCK to /mnt/c/ubuntu/wsl-ssh-pageant/ssh-agent.sock (the path to the socket).
  4. From the Windows side, run C:\ubuntu\wsl-ssh-pageant\wsl-ssh-pageant.exe --wsl c:\ubuntu\wsl-ssh-pageant\ssh-agent.sock to start the bridge.

If everything worked correctly, you can now call ssh-add -L from WSL and see the GPG Auth key on YubiKey in SSH format. If it works, it is time to set up autorun.

Autorun

Because running wsl-ssh-pageant blocks the terminal for as long as it is running, if we just set up an autorun shortcut the terminal will remain open until you log off. To avoid this, we will write a trivial Visual Basic script that will run wsl-ssh-pageant in a hidden window, and place it in the autorun folder:

Set objShell = WScript.CreateObject("WScript.Shell")
objShell.Run("C:\ubuntu\wsl-ssh-pageant\wsl-ssh-pageant.exe --wsl c:\ubuntu\wsl-ssh-pageant\ssh-agent.sock"), 0, True

Troubleshooting

wsl-ssh-pageant will silently fail if you give it path into a folder that does not exist. This means that you should double check the paths you pass to it.


  1. Yes, I am aware of the irony inherent in complaining about obsolete tutorials, while writing down my own tutorial that will also be obsoleted soon. ↩︎

  2. In fact, the NIST standard explicitly disallows 2048 bit RSA keys from 2030 onwards. ↩︎

  3. The most common argument against comments is that, unlike your name and email, they are not verifiable, and thus introduce confusion as to what part of your key is signed. My position is that I am likely ignoring the comment anyway, as it is unlikely to provide factual information (it is, after all, a comment), so I don't care. ↩︎

  4. There are 2 cases where I found having to touch the YubiKey every time annoying. The first was quickly SSHing into different remote machines. The second was when using git, because git likes to implement a lot of things as a commit, so if you set it to sign every commit, you will end up having to touch the YubiKey because you used git stash... you will also have to touch your YubiKey for every single temporary commit during rebasing. ↩︎

  5. Because gpg-agent has been introduced in gpg v2, you cannot use this trick with gpg v1. ↩︎

  6. I got as far as seeing SSH auth requests in the Windows gpg-agent log, but they never succeeded. Some googling around suggests that the SSH support is just plain broken on Windows. ↩︎