Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Script taken from https://blog.nimamoh.net/yubi-key-gpg-wsl2/ and improved
#!/usr/bin/env bash
# Inspired by https://blog.nimamoh.net/yubi-key-gpg-wsl2/
# Guide:
# Install GPG on windows & Unix
# Add "enable-putty-support" to gpg-agent.conf
# Download wsl-ssh-pageant and npiperelay and place the executables in "C:\Users\[USER]\AppData\Roaming\" under wsl-ssh-pageant & npiperelay
# https://github.com/benpye/wsl-ssh-pageant/releases/tag/20190513.14
# https://github.com/NZSmartie/npiperelay/releases/tag/v0.1
# Adjust relay() below if you alter those paths
# Place this script in WSL at ~/.local/bin/gpg-agent-relay
# Start it on login by calling it from your .bashrc: "$HOME/.local/bin/gpg-agent-relay start"
GNUPGHOME="$HOME/.gnupg"
PIDFILE="$GNUPGHOME/gpg-agent-relay.pid"
die() {
# shellcheck disable=SC2059
printf "$1\n" >&2
exit 1
}
main() {
checkdeps socat start-stop-daemon lsof timeout
case $1 in
start)
if ! start-stop-daemon --pidfile "$PIDFILE" --background --notify-await --notify-timeout 5 --make-pidfile --exec "$0" --start -- foreground; then
# shellcheck disable=SC2016
die 'Failed to start. Run `gpg-agent-relay foreground` to see output.'
fi
;;
stop)
start-stop-daemon --pidfile "$PIDFILE" --remove-pidfile --stop ;;
status)
start-stop-daemon --pidfile "$PIDFILE" --status
local result=$?
case $result in
0) printf "gpg-agent-relay is running\n" ;;
1 | 3) printf "gpg-agent-relay is not running\n" ;;
4) printf "unable to determine status\n" ;;
esac
return $result
;;
foreground)
relay ;;
*)
die "Usage:\n gpg-agent-relay start\n gpg-agent-relay stop\n gpg-agent-relay status\n gpg-agent-relay foreground" ;;
esac
}
relay() {
set -e
local winhome
local wslwinhome
winhome=$(cmd.exe /c "<nul set /p=%UserProfile%" 2>/dev/null || true)
wslwinhome="$(wslpath -u "$winhome")"
local npiperelay="$wslwinhome/AppData/Roaming/npiperelay/npiperelay.exe"
local wslsshpageant="$wslwinhome/AppData/Roaming/wsl-ssh-pageant/wsl-ssh-pageant-amd64-gui.exe"
local gpgconnectagent="/mnt/c/Program Files (x86)/GnuPG/bin/gpg-connect-agent.exe"
local gpgagentsocket="$GNUPGHOME/S.gpg-agent"
local sshagentsocket="$GNUPGHOME/S.gpg-agent.ssh"
# backslash escaping in socat EXEC doesn't seem to work very well, use forward slashes instead
# windows/npiperelay handle that just fine
local wingpgagentpath="${winhome//\\/\/}/AppData/Roaming/gnupg/S.gpg-agent"
killsocket "$gpgagentsocket"
killsocket "$sshagentsocket"
"$gpgconnectagent" /bye
"$wslsshpageant" --systray --winssh ssh-pageant 2>/dev/null &
# shellcheck disable=SC2034
WSPPID=$!
socat UNIX-LISTEN:"$gpgagentsocket,unlink-close,fork,umask=177" EXEC:"$npiperelay -ep -ei -s -a '$wingpgagentpath'",nofork &
GNUPID=$!
# shellcheck disable=SC2064
trap "kill -TERM $GNUPID" EXIT
socat UNIX-LISTEN:"$sshagentsocket,unlink-close,fork,umask=177" EXEC:"$npiperelay /\/\./\pipe/\ssh-pageant" &
SSHPID=$!
set +e
# shellcheck disable=SC2064
trap "kill -TERM $GNUPID; kill -TERM $SSHPID" EXIT
systemd-notify --ready 2>/dev/null
wait $GNUPID $SSHPID
trap - EXIT
}
killsocket() {
local socketpath=$1
if [[ -e $socketpath ]]; then
local socketpid
if socketpid=$(lsof +E -taU -- "$socketpath"); then
timeout .5s tail --pid=$socketpid -f /dev/null &
local timeoutpid=$!
kill "$socketpid"
if ! wait $timeoutpid; then
die "Timed out waiting for pid $socketpid listening at $socketpath"
fi
else
rm "$socketpath"
fi
fi
}
checkdeps() {
local deps=("$@")
local dep
local out
local ret=0
for dep in "${deps[@]}"; do
if ! out=$(type "$dep" 2>&1); then
printf -- "Dependency %s not found:\n%s\n" "$dep" "$out"
ret=1
fi
done
return $ret
}
main "$@"
@Nimamoh

This comment has been minimized.

Copy link

@Nimamoh Nimamoh commented Jul 2, 2020

Great work, thank you!

I had two issues trying your script, first are the options --notify-await --notify-timeout 2 on the start command. It is relatively recent options to rely on, do you have an opinion on how to adapt it on older system?

Second if the systemd-notify which will not work unless started as a systemd service, what would you think about a || true ?

Lastly, I noticed it did not really support consecutive start or kill -9 then restarts. Here is what I did to adapt your work, I am no bash guru, it's mainly food for thoughts:

#!/usr/bin/env bash
# Inspired by https://blog.nimamoh.net/yubi-key-gpg-wsl2/

# Guide:
# Install GPG on windows & Unix
# Add "enable-putty-support" to gpg-agent.conf
# Download wsl-ssh-pageant and npiperelay. I suggest placing the executables in "C:\Users\[USER]\AppData\Roaming\" under wsl-ssh-pageant & npiperelay
# https://github.com/benpye/wsl-ssh-pageant/releases/tag/20190513.14
# https://github.com/NZSmartie/npiperelay/releases/tag/v0.1
# Autostart pageant and wsl-ssh-pageant by adding shortcuts in "C:\Users\[USER]\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"
# "C:\Program Files (x86)\GnuPG\bin\gpg-connect-agent.exe" /bye
# "C:\Users\[USER]\AppData\Roaming\wsl-ssh-pageant\wsl-ssh-pageant-amd64-gui.exe" --systray --winssh ssh-pageant
# Place this script in WSL at ~/.local/bin/gpg-agent-relay
# Start it on login by calling it from your .bashrc: "$HOME/.local/bin/gpg-agent-relay start"

GNUPGHOME="$HOME/.gnupg"
PIDFILE="$GNUPGHOME/gpg-agent-relay.pid"

die() {
  printf "$1\n" >&2
  exit 1
}

main() {
  case $1 in
  start)
    $0 stop &> /dev/null
    if ! start-stop-daemon --pidfile "$PIDFILE" --background --make-pidfile --exec "$0" --start -- foreground; then
      die 'Failed to start. Run `gpg-agent-relay foreground` to see output.'
    fi
    ;;
  stop)
    start-stop-daemon --pidfile "$PIDFILE" --remove-pidfile --stop ;;
  status)
    start-stop-daemon --pidfile "$PIDFILE" --status
    local result=$?
    case $result in
      0) printf "gpg-agent-relay is running\n" ;;
      1 | 3) printf "gpg-agent-relay is not running\n" ;;
      4) printf "unable to determine status\n" ;;
    esac
    return $result
    ;;
  foreground)
    relay ;;
  *)
    die "Usage:\n  gpg-agent-relay start\n  gpg-agent-relay stop\n  gpg-agent-relay status\n  gpg-agent-relay foreground" ;;
  esac
}

relay() {
  set -e
  local winuser
  winuser=$(/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe '$env:UserName')
  winuser=${winuser//$'\r'}
  local winhome="/mnt/c/Users/$winuser"
  local wingnupghome="C:/Users/$winuser/AppData/Roaming/gnupg"
  local npiperelay="$winhome/AppData/Roaming/npiperelay/npiperelay.exe"

  local wslsgpgagent="$GNUPGHOME/S.gpg-agent"
  local wslssshagent="$GNUPGHOME/S.gpg-agent.ssh"

  if [[ -S "$wslsgpgagent" ]]; then
      rm "$wslsgpgagent"
  fi
  if [[ -S "$wslssshagent" ]]; then
      rm "$wslssshagent"
  fi

  socat UNIX-LISTEN:"$wslsgpgagent,unlink-close,fork,umask=177" EXEC:"$npiperelay -ep -ei -s -a '${wingnupghome}/S.gpg-agent'",nofork &
  GNUPID=$!
  trap "kill -TERM $GNUPID" EXIT

  socat UNIX-LISTEN:"$wslssshagent,unlink-close,fork,umask=177" EXEC:"$npiperelay /\/\./\pipe/\ssh-pageant" &
  SSHPID=$!
  trap "set +e; kill -TERM $GNUPID; kill -TERM $SSHPID" EXIT

  systemd-notify --pid=$$ --ready || true
  wait $GNUPID $SSHPID
  trap - EXIT
}

main "$@"
@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Jul 5, 2020

I had two issues trying your script, first are the options --notify-await --notify-timeout 2 on the start command. It is relatively recent options to rely on, do you have an opinion on how to adapt it on older system?

Not without a big chunk of workaround-code. Until recently there has not really been a way of notifying the parent process about service readiness in a clean manner

Second if the systemd-notify which will not work unless started as a systemd service, what would you think about a || true ?

It will! :-) My system is running vanilla Ubuntu 20.04 with no systemd as pid 1 and it works just fine. I think it's because it simply uses sockets to do the notification, so the service manager is not really necessary.

Lastly, I noticed it did not really support consecutive start or kill -9 then restarts. Here is what I did to adapt your work, I am no bash guru, it's mainly food for thoughts:

Yup. I removed that initially because I didn't want to hide any errors while developing and forgot to add it again. However, I'm posting an update that uses lsof to figure out the listening PID of the orphaned socket and kill it first.

@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Jul 5, 2020

@Nimamoh
Updated. I went with your suggestion of the || true on systemd-notify so that a manual call to foreground doesn't fail. I would simply remove the entire notify part if you want to run it on older systems.
Notable changes:

  • gpg-agent & wsl-ssh-pageant are now started from the script as well (but not terminated). This makes installation a lot easier (assuming the paths match)
  • When removing orphaned sockets, the matching listener processes are terminated as well.
  • A rudimentary dependency-check is performed to make sure lsof, socat, and start-stop-daemon are available.
@Nimamoh

This comment has been minimized.

Copy link

@Nimamoh Nimamoh commented Jul 5, 2020

Really nice, I tried your script and it works really well. Are you okay if I update my blog post by referencing your work?

@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Jul 5, 2020

@Nimamoh, of course, go right ahead (assuming attribution of course).

@Nimamoh

This comment has been minimized.

Copy link

@Nimamoh Nimamoh commented Jul 6, 2020

@andsens
Thank you again, updated the article, feel free to give feedback.

@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Jul 7, 2020

@Nimamoh
Awesome stuff, excellent guide. You don't actually need the enable-ssh-support in gpg-agent.conf, since we're only use the putty named pipe for forwarding.
Some errors I plan on fixing:

  • When starting up the first time the timeout of 2 sec can actually be too short, resulting in start-stop-daemon: timed out waiting for a notification
  • When killing orphaned socket processes, wait will not working, because the processes are not children of the current one.. So I need to find an alternative way of waiting for shutdown on those.
@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Jul 8, 2020

@Nimamoh, updated. All known bugs are fixed :-)

@Nimamoh

This comment has been minimized.

Copy link

@Nimamoh Nimamoh commented Jul 8, 2020

Well done! I updated the blog post accordingly and used the last version of your script, so far it's good ^^

@strangnet

This comment has been minimized.

Copy link

@strangnet strangnet commented Aug 2, 2020

I've tried the guide by @Nimamoh with this script, and I can't seem to get past the start-stop-daemon: timed out waiting for a notification error when running gpg-agent-relay start and running with foreground gives no output at all.

My environment is Pengwin (Debian based, with fish shell) in WSL2, up-to-date Windows 10 (fresh install).

Kleopatra has found my Yubikey properly and seems to be configured correctly. Also, the systray icon for wsl-ssh-pageant pops up as it should. Looking at the logs in Kleopatra, it seems some kind of handshake is happening and it looks fine to my untrained eyes:

gpg-agent[23720]: DBG: chan_0x000003b0 -> OK Pleased to meet you
gpg-agent[23720]: DBG: chan_0x000003b0 <- RESET
gpg-agent[23720]: DBG: chan_0x000003b0 -> OK
gpg-agent[23720]: DBG: chan_0x000003b0 <- [eof]
gpg-agent[23720]: DBG: chan_0x0000039c -> OK Pleased to meet you
gpg-agent[23720]: DBG: chan_0x0000039c <- GETINFO pid
gpg-agent[23720]: DBG: chan_0x0000039c -> D 23720
gpg-agent[23720]: DBG: chan_0x0000039c -> OK
gpg-agent[23720]: DBG: chan_0x0000039c <- BYE
gpg-agent[23720]: DBG: chan_0x0000039c -> OK closing connection

Any suggestions?

@strangnet

This comment has been minimized.

Copy link

@strangnet strangnet commented Aug 2, 2020

Apparently I should have checked further. ssh-add -L actually shows my ssh key on the card so I guess it worked as it should anyway?

@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Aug 2, 2020

@strangnet try running bash -x gpg-agent-relay.sh foreground and link to the output in a new gist. bash -x just shows all commands being run in the script, so maybe you can also figure it out yourself :-)

One possibility, things just take longer than I anticipated. I have set the timeout to 5 seconds (line 27), try adjusting that.

@strangnet

This comment has been minimized.

Copy link

@strangnet strangnet commented Aug 2, 2020

If I do let the agent relay start, it outputs the timeout error, and setting the timeout higher makes no difference. It does succeed, though. If I run foreground as you suggested the output is nothing, but if I don't run start on login, it starts the gpg-agent successfully.

My solution now, is to accept that it works, i.e. I get the result I'm after, and just send the output of start to /dev/null.

@lekv

This comment has been minimized.

Copy link

@lekv lekv commented Oct 11, 2020

This script is fantastic, thanks for putting it together!

I ran into some issue b/c winuser is detected as the user inside WSL (and they're different for me). However, changing that part to these lines works for me:

relay() {
  set -e
  local winhome="$(cmd.exe /c "<nul set /p=%UserProfile%" 2>/dev/null)"
  local wslhome="$(wslpath $winhome)"
  local wingnupghome="$winhome/AppData/Roaming/gnupg"
  local npiperelay="$wslhome/AppData/Roaming/npiperelay/npiperelay.exe"
  local wslsshpageant="$wslhome/AppData/Roaming/wsl-ssh-pageant/wsl-ssh-pageant-amd64-gui.exe"
  ...
@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Oct 17, 2020

@lekv thanks for the pointer. I have adjusted the script, I really like how your solution makes no assumption about the home dir location whatsoever :-)
There was an issue with the backslashes in the path returned by %UserProfile% though. I think socat or some other part of that chain re-interpretes the gpg-agent socket path argument and thereby makes it invalid. Instead of double escaping the backslashes I simply replaced them with forward slashes (local wingpgagentpath="${winhome//\\/\/}/AppData/Roaming/gnupg/S.gpg-agent").

@dannyverp

This comment has been minimized.

Copy link

@dannyverp dannyverp commented Nov 2, 2020

Hi @andsens, thank you for the awesome script. I'm having a bit of trouble getting it to work. I've reached a stage where I had to slightly modify the script because my username contains a space and the script doesn't seem to be able to handle that. Now I just get a whole bunch of npiperelay.exe help messages. Here's my output of the foreground task when ran with bash -x

+ PIDFILE=/home/dannyverpoort/.gnupg/gpg-agent-relay.pid
+ main foreground
+ checkdeps socat start-stop-daemon lsof timeout
+ deps=("$@")
+ local deps
+ local dep
+ local out
+ local ret=0
+ for dep in "${deps[@]}"
++ type socat
+ out='socat is /usr/bin/socat'
+ for dep in "${deps[@]}"
++ type start-stop-daemon
+ out='start-stop-daemon is /usr/sbin/start-stop-daemon'
+ for dep in "${deps[@]}"
++ type lsof
+ out='lsof is /usr/bin/lsof'
+ for dep in "${deps[@]}"
++ type timeout
+ out='timeout is /usr/bin/timeout'
+ return 0
+ case $1 in
+ relay
+ set -e
+ local winhome
+ local wslwinhome
++ cmd.exe /c '<nul set /p=%UserProfile%'
++ true
+ winhome='C:\Users\Danny Verpoort'
++ wslpath -u 'C:\Users\Danny Verpoort'
+ wslwinhome='/mnt/c/Users/Danny Verpoort'
+ local npiperelay=/mnt/c/Users/Local/bin/npiperelay/npiperelay.exe
+ local wslsshpageant=/mnt/c/Users/Local/bin/wsl-ssh-pageant/wsl-ssh-pageant-amd64-gui.exe
+ local 'gpgconnectagent=/mnt/c/Program Files (x86)/GnuPG/bin/gpg-connect-agent.exe'
+ local gpgagentsocket=/home/dannyverpoort/.gnupg/S.gpg-agent
+ local sshagentsocket=/home/dannyverpoort/.gnupg/S.gpg-agent.ssh
+ local 'wingpgagentpath=C:/Users/Danny Verpoort/AppData/Roaming/gnupg/S.gpg-agent'
+ killsocket /home/dannyverpoort/.gnupg/S.gpg-agent
+ local socketpath=/home/dannyverpoort/.gnupg/S.gpg-agent
+ [[ -e /home/dannyverpoort/.gnupg/S.gpg-agent ]]
+ killsocket /home/dannyverpoort/.gnupg/S.gpg-agent.ssh
+ local socketpath=/home/dannyverpoort/.gnupg/S.gpg-agent.ssh
+ [[ -e /home/dannyverpoort/.gnupg/S.gpg-agent.ssh ]]
+ '/mnt/c/Program Files (x86)/GnuPG/bin/gpg-connect-agent.exe' /bye
+ WSPPID=4746
+ /mnt/c/Users/Local/bin/wsl-ssh-pageant/wsl-ssh-pageant-amd64-gui.exe --systray --winssh ssh-pageant
+ GNUPID=4747
+ trap 'kill -TERM 4747' EXIT
+ socat UNIX-LISTEN:/home/dannyverpoort/.gnupg/S.gpg-agent,unlink-close,fork,umask=177 'EXEC:/mnt/c/Users/Local/bin/npiperelay/npiperelay.exe -ep -ei -s -a '\''C:/Users/Danny Verpoort/AppData/Roaming/gnupg/S.gpg-agent'\'',nofork'
+ SSHPID=4748
+ socat UNIX-LISTEN:/home/dannyverpoort/.gnupg/S.gpg-agent.ssh,unlink-close,fork,umask=177 'EXEC:/mnt/c/Users/Local/bin/npiperelay/npiperelay.exe /\/\./\pipe/\ssh-pageant'
+ set +e
+ trap 'kill -TERM 4747; kill -TERM 4748' EXIT
+ systemd-notify --ready
+ wait 4747 4748

The agent-relay foreground command is silent until I try to use gpg --card-status in a second WSL window. Then the repeated help message shows up.

I'm running Ubuntu 20.04.1 on WSL, windows 10 as host and I'm trying to use a yubikey 5. If I try the card status in the power shell I can actually see the card. I have all the perquisites installed. Any suggestions on what's going on?

image

@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Nov 7, 2020

@dannyverp, socat EXEC is garbage at escaping the arguments for its invocations. I suggest fiddling around with different escaping mechanism and see what sticks (i.e. just run the socat command seperately).
The big question mark here is whether some of the argument parsing happens on the windows side of things when invoking .exe files, I have no clue about that.
To start off with try double-escaping wingpgagentpath like so wingpgagentpath=$(printf -- '%q' "$wingpgagentpath")

@jmigual

This comment has been minimized.

Copy link

@jmigual jmigual commented Dec 12, 2020

Hey, so I've found out that somehow sometimes a socket is open by multiple processes and thus the killsocket function fails as the socketpid variable is a multiline string. I think replacing it with something like echo "$socketpid" | xargs kill should do the trick.

@andsens

This comment has been minimized.

Copy link
Owner Author

@andsens andsens commented Dec 13, 2020

@jmigual huh, curious. Of course, only on process can be listening on that socket. So the others must be clients. I'm not sure killing them is the best way to go, it would be better to have lsof filter properly.

@mew1033

This comment has been minimized.

Copy link

@mew1033 mew1033 commented Feb 8, 2021

This isn't working for me as is. I moved a few files around, but other than that I'm running the script exactly as you have here. The only thing that fixed gpg for me was to symlink /mnt/c/Program Files (x86)/GnuPG/bin/gpg.exe to /usr/local/sbin/gpg. (Found here: https://blog.nimamoh.net/yubi-key-gpg-wsl2/#isso-4)

Is there any way I can help figure out why it doesn't work with stock gpg?

P.S. I'm also going to work on modifying the script to only setup the GPG relay. I want to use the built in Windows OpenSSH client with its ssh-agent.

@alanivey

This comment has been minimized.

Copy link

@alanivey alanivey commented Feb 8, 2021

I'm also not able to get it working. When I run any gpg commands in WSL2 (Ubuntu 20.10 fwiw), it doesn't seem able to use the existing sockets. I added extra-socket /dev/null and browser-socket /dev/null to ~/.gnupg/gpg-agent.conf to keep it from creating any new sockets, and then any gpg commands return: gpg: can't connect to the agent: End of file

@mew1033

This comment has been minimized.

Copy link

@mew1033 mew1033 commented Feb 8, 2021

@alanivey Did you try the symlink method? That was the only thing that worked for me.

@alanivey

This comment has been minimized.

Copy link

@alanivey alanivey commented Feb 9, 2021

@mew1033 yes, I symlinked the Gpg4Win exe in WSL2 Linux as $HOME/.local/bin/gpg with ln -sv /mnt/c/Program\ Files\ \(x86\)/GnuPG/bin/gpg.exe ~/.local/bin/gpg (in my WSL2 Linux environment, I have this directory at the front of $PATH). This binary does not attempt to use $HOME/.gnupg/S.gpg-agent when running gpg commands, nor does it use $HOME/.gnupg for its database, so it's not necessary to do any GPG setup in the WSL2 Linux environment. B/c I'm not looking to use the SSH agent integration, I ultimately find this easier; I maintain a single GPG database in Windows and can use it in multiple WSL2 Linux environments without replicating.

I'll caveat that I have not used this configuration for longer than a day so I might end up being wrong!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment