Previously, I wrote about using bash (WSL) on Windows 10. I've changed some of my setup since then, but still use something similar for when I'm developing on a Windows machine. Until recently, I have been disappointed with the lack of gpg support in wsl. GnuPG itself works fine, but you can not use a hardware smartcard device like a Yubikey.

To get around this, I installed Keybase on my WSL instance, and set up a key just for wsl. It works fine for when I only need to encrypt something for myself or use Keybase's kbfs or git support. I can't use this method if I want to sign git commits or encrypt things using my regular gpg key (stored on a smartcard) though.

Recently, I learned that it is possible to forward gpg-agent from windows to wsl and even from wsl to another server via ssh. This guide should be enough to get this running for someone else as well.

Pre-reqs

Before getting started, the following is assumed:

  1. WSL (Windows Subsystem for Linux) set up and working
  2. GPG4Win installed, and preferably set up with a known-working config on Windows
  3. A Yubikey with appropriate subkeys on it already
  4. socat and gpg installed on WSL

Windows Setup

GPG4Win

On Windows, we'll be using GPG4Win. It can be downloaded from https://www.gpg4win.org/download.html. I used 3.1.10, but I'm assuming any recent version should work just as well.

Once it's installed, open up Kleopatra and go to Tools -> Manage Smartcards. It should look similar to this if the yubikey is detected:

Example Kleopatra Smartcard Management Screen

After checking that your yubikey is detected, go to Settings -> Configure Kleopatra -> GnuPG System. Ensure Enable ssh support and Enable putty support are both checked. Quit Kleopatra once it's configured. If you open up %APPDATA%\gnupg\gpg-agent.conf, it should contain this at the top of the file:

###+++--- GPGConf ---+++###
enable-ssh-support
enable-putty-support
###+++--- GPGConf ---+++###

Now we need to start the gpg-agent on Windows. One way to do this is to open a command prompt and run this:

"C:\Program Files (x86)\GnuPG\bin\gpg-connect-agent.exe" /bye

You can check that the agent is working on Windows by listing the keys with:

"C:\Program Files (x86)\GnuPG\bin\gpg-connect-agent.exe" "keyinfo --list" /bye

S KEYINFO 2DE54A65626EFC544C9D83A49CD38FFFEE31CE30 D - - - P - - -
OK

At this point, gpg and the yubikey should be working fine under Windows. "C:\Program Files (x86)\GnuPG\bin\gpg.exe" --card-status should also show details of the yubikey.

npiperelay

WSL does not currently support passing usb devices into it. There are several github issues open for it, and a UserVoice that can be voted up to add support for libusb. Because of this, you'll get an error like this if you try to run gpg --card-status from within WSL:

$ gpg --card-status
gpg: error getting version from 'scdaemon': No SmartCard daemon
gpg: OpenPGP card not available: No SmartCard daemon

Unfortunately gpg inside of WSL can't see the yubikey or any other smartcard. However, it does have access to the host Windows filesystem. gpg-agent on windows also creates a socket file at %APPDATA%/gnupg/S.gpg-agent. Unfortunately (again) Windows doesn't really support unix sockets, so we can't just use this socket file. The socket file it creates is actually just a plaintext file referencing a tcp port and nonce:

[justyns]$ file /c/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent
/c/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent: ISO-8859 text

[justyns]$ cat /c/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent
55099
;D-+U

[justyns]$                                                       

Trying to use gpg with that "socket file" in wsl will give you an error like this:

[justyns]$ gpg -K
connect to /c/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent port -2 failed: Bad file descriptor
connect to /c/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent port -2 failed: Bad file descriptor
connect to /c/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent port -2 failed: Bad file descriptor
connect to /c/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent port -2 failed: Bad file descriptor
gpg: can't connect to the agent: End of file

npiperelay is a tool that allows you to access Windows named pipes inside of WSL. For example, it can be used to access Docker for Windows from within WSL. At the time of writing, this tool does not actually support LibAssuan file sockets that GnuPG uses on Windows. There is a github issue open to add support: Support for LibAssuan file sockets

NZSmartie from the above issue has a PR open that adds the initial support we need. You'll need to either download the pre-compiled release from https://github.com/NZSmartie/npiperelay/releases or compile it yourself using that branch.

Note: There's actually a second PR which fixes some issues with the one mentioned above. I haven't tested that one yet, but it is probably the one that should be getting used until they're both merged into master.

Once you've either compiled or downloaded the npiperelay.exe, put it in a path accessible to both WSL and Windows. I put mine in /mnt/c/Users/justyns/bin/npiperelay.exe to make it easy to find later.

WSL

In a WSL shell, change the command below and run it:

socat UNIX-LISTEN:"$HOME/.gnupg/S.gpg-agent,fork" EXEC:'/mnt/c/Users/justyns/bin/npiperelay.exe -ei -ep -s -a "C:/Users/justyns/AppData/Roaming/gnupg/S.gpg-agent"',nofork

In a different WSL shell, check that there is S.gpg-agent in the $HOME/.gnupg/ directory and that you're able to see the yubikey now:

$ ls -lah ~/.gnupg/S.gpg-agent
srwxrwxrwx 1 justyns justyns 0 Jul 14 18:35 /home/justyns/.gnupg/S.gpg-agent

$ gpg --card-status
Reader ...........: Yubico Yubikey 4 OTP U2F CCID 0
Application ID ...: x
Version ..........: 2.1
Manufacturer .....: Yubico
Serial number ....: x
Name of cardholder: Justyn S
Language prefs ...: en
Sex ..............: unspecified
URL of public key : https://keybase.io/justyns/key.asc
Login data .......: justyns
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 0 0 0
PIN retry counter : 0 0 0
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]

If all went well, gpg --card-status should now work! gpg -K should also work to show you your private keys. Something to keep in mind though is that you need to import the public gpg keys into your WSL gpg keyring. Just forwarding the gpg-agent isn't enough.

Automating everything

Once you're sure the above solution is working, I'd recommend setting everything up so that gpg in WSL automatically works without having to do anything extra.

On Windows, the gpg-agent needs to be started when you log in. Either create a scheduled task to start it on logon, or create a shortcut in your Start Menu/Programs/Startup directory:

  1. Open File Explorer and navigate to %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
  2. Create a new shortcut to run "C:\Program Files (x86)\GnuPG\bin\gpg-connect-agent.exe" /bye
  3. Right click -> properties on the new shortcut and change it to run minimized

On WSL, we need to create a script that will automatically execute socat/npiperelay when we login:

  1. Create a new file at ~/bin/gpg-agent-relay.sh with these contents. Make sure you change the usernamd and paths if needed:

    #!/bin/bash
    # Launches socat+npiperelay to relay the gpg-agent socket file for use in WSL
    # See https://justyn.io/blog/using-a-yubikey-for-gpg-in-windows-10-wsl-windows-subsystem-for-linux/ for details
    
    GPGDIR="${HOME}/.gnupg"
    USERNAME=justyns
    # I use the same username for wsl and windows, but feel free to modify the paths below if that isn't the case
    WIN_GPGDIR="C:/Users/${USERNAME}/AppData/Roaming/gnupg"
    NPIPERELAY="/mnt/c/Users/${USERNAME}/bin/npiperelay.exe"
    PIDFILE="${GPGDIR}/.gpg-agent-relay.pid"
    OLDPID=$(cat "${PIDFILE}")
    
    # Launch socat+npiperelay for the regular gpg-agent
    if [ ! -z "${OLDPID}" ]; then
        ps -p "${OLDPID}" >/dev/null && \
        echo "gpg-agent-relay.sh already running with process id $(cat ${PIDFILE})" && \
        exit 0
    fi
    
    rm -f "${GPGDIR}/S.gpg-agent*"
    echo $$ > ${PIDFILE}
    
    # Relay the regular gpg-agent socket for gpg operations
    socat UNIX-LISTEN:"${GPGDIR}/S.gpg-agent,fork" EXEC:"${NPIPERELAY} -ep -ei -s -a '${WIN_GPGDIR}/S.gpg-agent'",nofork &
    AGENTPID=$!
    
    # Relay the gpg ssh-agent
    socat UNIX-LISTEN:"${GPGDIR}/S.gpg-agent.ssh,fork" EXEC:"${NPIPERELAY} -ep -ei -s -a '${WIN_GPGDIR}/S.gpg-agent.ssh'",nofork &
    SSHPID=$!
    
    wait ${AGENTPID}
    wait ${SSHPID}
    
  2. Make the script executable: chmod 750 ~/bin/gpg-agent-relay.sh

  3. Update ~/.bashrc, ~/.zshrc, or whichever file is run when a new shell is spawned. Add something like this to the bottom of it:

    # Launch the gpg-agent-relay.sh script in the background
    ~/bin/gpg-agent-relay.sh &
    

Now if you reboot your machine, you should be able to open WSL/bash and have gpg --card-status immediately work if you have a Yubikey plugged in.

Forwarding Yubikey and gpg-agent over SSH

While getting my Yubikey to work in WSL was my main goal, I also figured out how to forward my gpg-agent from Windows to WSL and then to a remote server over ssh. I previously had this working with git-bash, but I'm happy that it works just as easily through WSL.

In my ~/.ssh/config, I have a shell host configured like this:

Host shell
    HostName 1.2.3.4
    ForwardAgent yes
    SendEnv LANG LC_*
    RemoteForward /home/justyns/.gnupg/S.gpg-agent /home/justyns/.gnupg/S.gpg-agent
    RemoteForward /home/justyns/.gnupg/S.gpg-agent.ssh /home/justyns/.gnupg/S.gpg-agent.ssh

Now when I use ssh shell to connect to this machine, ssh automatically forwards the S.gpg-agent socket files above. It does depend on ~/.gnupg already existing, but otherwise gives me the ability to ssh to this server and then use gpg (with private keys still on the Yubikey) as normal. I wouldn't recommend doing this with any servers you do not explicitly control or trust, but can be very handy for local servers.

References

Below is a list of links that helped me get parts of this solution working, and may have additional information helpful to anyone else struggling with the same issues.

Common Issues

I'll try to keep this section up to date with any errors I run across while setting this up on new machines. Hopefully it will help someone else as well.

If you run into any issues getting this working, please feel free to reach out to me and I'd be happy to help if I can!

gpg --card-status doesn't detect any devices

gpg: selecting openpgp failed: No such device
gpg: OpenPGP card not available: No such device

I kept running into the above error when doing gpg --card-status from a windows command prompt or git-bash. The issue ended up being because git-bash installs its own version of gpg and that takes precedence in the PATH. You can either alter your PATH variable to prefer GPG4Win or use full paths to the binaries like I did above.

gpg-agent socket already exists

2019/07/14 19:22:03 socat[11585] E "/home/justyns/.gnupg/S.gpg-agent" exists

The above error usually happens if gpg-agent is already running in WSL. Just kill any existing gpg-agent processes and then try the gpg-agent-relay.sh script again.