Skip to content

Instantly share code, notes, and snippets.

@andsens
Last active November 6, 2024 08:01
Show Gist options
  • Save andsens/2ebd7b46c9712ac205267136dc677ac1 to your computer and use it in GitHub Desktop.
Save andsens/2ebd7b46c9712ac205267136dc677ac1 to your computer and use it in GitHub Desktop.
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 "$@"
@mew1033
Copy link

mew1033 commented Feb 8, 2021

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

@alanivey
Copy link

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!

@alexdetsch
Copy link

In my Debian-WSL2, I can't access cmd.exe (and I didn't investigate in this further, as there seems to be a cleaner way), so the script isn't working. I replaced line 55 and 56 with this (you have to install wslu for this):

username="$(wslvar USERPROFILE)"
wslwinhome="$(wslpath $username)"
winhome="$(wslpath -w $wslwinhome)"

additionally, I had to add export PATH=$PATH:/sbin to my .bashrc as otherwise start-stop-daemon isn't found in path.

@matt-forster
Copy link

matt-forster commented Dec 16, 2021

@andsens I was having difficulty with this script and multiple shells (ie, a windows terminal and VSCode, which opens another terminal). It was failing because multiple PIDs were being included in the socket file, and kill was not super happy about that.

I'd end up with an error like;

gpg-agent-relay foreground
gpg-agent-relay: line 99: kill: 768
2650
8610
23065
26471
27892
30552: arguments must be process or job IDs
tail: cannot open '2650' for reading: No such file or directory
tail: cannot open '8610' for reading: No such file or directory
tail: cannot open '23065' for reading: No such file or directory
tail: cannot open '26471' for reading: No such file or directory
tail: cannot open '27892' for reading: No such file or directory
tail: cannot open '30552' for reading: No such file or directory
==> /dev/null <==

To correct it, I modified the killsocket function;

 killsocket() {
   local socketpath=$1
   if [[ -e $socketpath ]]; then
     # local socketpid
     lsof +E -taU -- "$socketpath" | awk 'length {print $1}' | xargs -rn1 kill
   fi
 }

This works and corrects my multiple-terminal issues, but the timeout is also removed. It could be added, but I am wondering if it is even necessary?

@demonbane
Copy link

I've been using this for a couple of years now with few enough headaches that I never tried to look for a better way. But with systemd support now in WSL I decided to see if I could simplify this approach and I found that I could do it all just using systemd and npiperelay. Turns out I could so I packaged it all up in wsl-gpg-systemd if anyone is interested in giving it a shot.

@matt-forster
Copy link

@demonbane I tried it out on a fresh container this evening, and it works great! Thanks for simplifying it tenfold.

For anyone coming at systemd new; the socket set up by wsl-gpg-systemd is /run/user/1000/gnupg/S.gpg-agent.ssh. I had to pause for a second to figure out where the socket was being opened.

@demonbane
Copy link

The installer takes care to always use gpgconfig to get any paths, and newer releases of gpg have been following the systemd spec and using /run for transient user session storage, like the sockets. And surprisingly, there doesn't appear to be an easy way to override that location. From the gpg-agent manpage:

       --use-standard-socket
       --no-use-standard-socket
       --use-standard-socket-p
              Since GnuPG 2.1 the standard socket is always used.  These options have no more  effect.   The  command
              gpg-agent --use-standard-socket-p will thus always return success.

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