From 1ef46e010e2dc017f60cddfabf6e669a0469ef3f Mon Sep 17 00:00:00 2001 From: sev Date: Thu, 26 Aug 2021 15:11:14 -0500 Subject: [PATCH] gpg: expand forwarding, add default-recipient-self - GPG forwarding has been expanded from simply overwriting the existing sockets to instead create a new environment for each connection. - GPG SSH agent handling has been reworked. - default-recipient-self was added to config. --- base/.zlogout | 21 +++++- base/.zshenv | 182 ++++++++++++++++++++++++++++++++++----------- base/.zshrc | 5 ++ bin/gpg-learn-keys | 1 + gpg/gpg.conf | 1 + ssh/config | 7 +- 6 files changed, 168 insertions(+), 49 deletions(-) diff --git a/base/.zlogout b/base/.zlogout index 9e0ed3f..3b09ce5 100644 --- a/base/.zlogout +++ b/base/.zlogout @@ -1,4 +1,23 @@ -echo logout +# gpg forward cleanup +if [[ $SHLVL == 1 && $GNUPGHOME =~ '.ssh_forward/\d+$' ]] { + x=$GNUPGHOME + # reset GNUPGHOME + [[ -o GLOB_ASSIGN ]]; y=$? + setopt GLOB_ASSIGN + GNUPGHOME=$GNUPGHOME/../..(:a) + [[ $y != 0 ]] && unsetopt GLOB_ASSIGN + # clean dir if it exists + if [[ -d $x ]] { + find $x -mindepth 1 -maxdepth 1 | while read -r y; do + unlink $y + done + rmdir -p $x 2>/dev/null + } + # clean up any remaining sockets from gpg forwarding + rm -f $_GNUPG_SOCK_DEST_BASE*(N=) +} + +[[ -o interactive ]] && echo logout clear ### load site-specific diff --git a/base/.zshenv b/base/.zshenv index 1f3e198..11cded7 100644 --- a/base/.zshenv +++ b/base/.zshenv @@ -1,3 +1,6 @@ +# WARN: not used in this repo, but gpgconf --list-dirs does not read +# gpg-agent.conf to get socket path, see dev.gnupg.org/T3108 + ### unset unwanted options that could be set in /etc/zshenv unsetopt SH_WORD_SPLIT KSH_ARRAYS @@ -13,8 +16,11 @@ if [[ -v _sev_reset_shell || $SHLVL == 1 ]] { export LC_CTYPE=$LANG ## path + # path and fpath should already be linked to PATH and FPATH + # we don't want duplicates so turn on unique mode if it isn't already typeset -U path fpath - if [[ $SHLVL == 1 ]] { + # do not run more than once per session, even if resetting shell + if [[ $SHLVL == 1 ]] { # take a backup before any customizations export _sev_sys_PATH=$PATH export _sev_sys_FPATH=$FPATH @@ -24,13 +30,14 @@ if [[ -v _sev_reset_shell || $SHLVL == 1 ]] { /usr/local/{s,}bin /usr/games) PATH=$PATH:$_sev_sys_PATH fpath=(${ZDOTDIR:-$HOME/.zsh}/functions/{*,Completions/*}(N)) - #fpath is not exported by default + # fpath is not exported by default export FPATH=$FPATH:$_sev_sys_FPATH # take another backup, explained in .zprofile typeset -U _backup_path _backup_path=("${path[@]}") ## xdg + # TODO: check for and merge existing XDG env vars export XDG_CONFIG_HOME=~/etc export XDG_CONFIG_DIRS=~/.config:/usr/pkg/etc/xdg:/usr/local/etc/xdg:/etc/xdg export XDG_DATA_HOME=~/share @@ -42,17 +49,124 @@ if [[ -v _sev_reset_shell || $SHLVL == 1 ]] { } ## create tmp link - t=${TMPDIR:-/tmp}/home-$LOGNAME + t=${TMPDIR:-/tmp}/.home-$LOGNAME if [[ ! -e $t ]] { - mkdir -m 700 $t >/dev/null 2>&1 - # TODO: check if dir exists after mkdir + mkdir -m 700 $t 2>/dev/null + if [[ ! -d $t ]] { + [[ -o interactive ]] && + print -P "%F{red}!!! Can't create temp folder $t%f" + [[ -h $XDG_RUNTIME_DIR ]] && unlink $XDG_RUNTIME_DIR 2>/dev/null + [[ ! -e $XDG_RUNTIME_DIR ]] && mkdir $XDG_RUNTIME_DIR 2>/dev/null + } } # allow opaque entries to override link creation - if [[ ! -e $XDG_RUNTIME_DIR ]] { - ln -sf $t $XDG_RUNTIME_DIR >/dev/null 2>&1 - } + [[ ! -e $XDG_RUNTIME_DIR ]] && ln -sf $t $XDG_RUNTIME_DIR 2>/dev/null unset t + ## gpg forwarding + # NOTE: while ssh automatically sets SSH_AUTH_SOCK with the ForwardSsh + # directive, GPG must be forwarded manually. to support this, we + # forward the restricted gpg-agent extra socket to the remote host + # with a RemoteForward rule in ~/.ssh/config that uses the + # _GNUPG_SOCK_* env vars. + # to avoid conflicts with other ssh sessions where the same user is + # connecting to the same host from different machines, gpg in each + # environment should utilize its own forwarded socket, rather than + # replace the sockets in GNUPGHOME which will be overridden on the + # next connection. previously, you could provide a path to the agent + # socket in GPG_AGENT_INFO, but that was deprecated in GPG v2.1. + # instead, we must clone GNUPGHOME and replace the agent sockets + # there with the forwarded one. + # HACK: without SendEnv, which is disabled by default in most sshd configs, + # there is no foolproof way to prevent race conditions or filename + # collisions, pass the forward path to the remote host environment, + # or even know if the forward path exists and is writable. we just + # have to guess this path is good on the desination host, and assume + # the newest matching socket is the correct one after connecting. in + # theory we could occlude the ssh binary on PATH with an alias or + # script that communicates with the remote host before opening a + # shell, but that would open up too many edge cases where it wouldn't + # work to make it worth the effort and extra overhead. + # do not run more than once per session, even if resetting shell + if [[ $SHLVL == 1 && -v commands[gpg] ]] { + export _GNUPG_SOCK_DEST_BASE=/tmp/.gpg-agent-forward + export _GNUPG_SOCK_DEST_EXT=$(date +%s).$RANDOM + export _GNUPG_SOCK_DEST=$_GNUPG_SOCK_DEST_BASE.$_GNUPG_SOCK_DEST_EXT + _sev_gpg_forward_dir=${GNUPGHOME:-~/.gnupg}/.ssh_forward + s=($_GNUPG_SOCK_DEST_BASE*(N=oc[1])) + # clean up forwards if its session is dead or we ask for it + if [[ -d $_sev_gpg_forward_dir ]] { + find $_sev_gpg_forward_dir -type d -mindepth 1 -maxdepth 1 | + while read -r x; do + # NOTE: the only way we can get here is if we are SHLVL 1. if + # our own pid already has a dir, it is most likely stale, + # or something is very broken—assume the former. + p=$(basename $x) + if [[ -v _sev_gpg_forward_clean || $$ == $p ]] || + ! kill -0 $p 2>/dev/null; then + find $x -mindepth 1 -maxdepth 1 | while read -r y; do + unlink $y + done + rmdir $x + fi + done + unset x p y + } + # create new forward dir + if [[ -n $s && -v SSH_CLIENT ]] { + export _sev_gpg_forwarded= + mkdir -pm700 $_sev_gpg_forward_dir + h=$_sev_gpg_forward_dir/$$ + mkdir -pm700 $h + # XXX: is it safe to link scdaemon socket? can its name be changed? + for x in S.scdaemon gpg.conf gpg-agent.conf sshcontrol \ + pubring.kbx trustdb.gpg private-keys-v1.d crls.d; do + ln -s ${GNUPGHOME:-~/.gnupg}/$x $h + done + export GNUPGHOME=$h + unset h + for x in $(gpgconf --list-dirs | grep 'agent-.*-\?socket:'); do + # dirs are prefixed and percent-encoded—strip and decode + # https://stackoverflow.com/a/64312099 + x=${${x/#agent-*socket:/}//(#b)%([[:xdigit:]](#c2))/${(#):-0x$match[1]}} + if [[ ! -v orig ]] { + mv $s $x + orig=$x + } else { + ln -s $orig $x + } + done + unset x orig + } + unset s + + # what we will forward if we start a new ssh connection + # NOTE: do this after setting up GNUPGHOME to pick up new socket path; + # if already connected over SSH, extra should be the remote one + export _GNUPG_SOCK_SRC=$(gpgconf --list-dirs agent-extra-socket) + } else { + # required for RemoteForward to not error out if the vars are unset + export _GNUPG_SOCK_SRC=/nonexistent + export _GNUPG_SOCK_DEST=/nonexistent + } + + ## gpg agent + # always try to start agent during setup + if [[ SHLVL == 1 ]] { + gpg-connect-agent /bye >/dev/null 2>&1 + [[ $? -ne 0 && -o interactive ]] && + print -P "%F{red}!!! Can't communicate with GPG agent%f" + } + # set up tty if it isn't, and we're interactive or in xorg & not forwarded + # do not run more than once per session, even if resetting shell + if [[ -v commands[gpg-connect-agent] && + ! -v _sev_gpg_forward && ! -v GPG_TTY && + ( -o interactive || -v DISPLAY ) ]] { + export GPG_TTY=$(tty) + export PINENTRY_USER_DATA=USE_TTY=$((!${+DISPLAY})) + gpg-connect-agent UPDATESTARTUPTTY /bye >/dev/null 2>&1 + } + ## ssh agents # NOTE: preferred order of agents to check: okcagent, gnupg, openssh # first block takes care of okcagent and openssh, second gnupg @@ -67,7 +181,7 @@ if [[ -v _sev_reset_shell || $SHLVL == 1 ]] { IFS=$'\0' read -r sock pid <$agentfile } if [[ -S $sock && $pid > 0 ]] && kill -0 $pid; then - [[ -o interactive ]] && echo "Reusing agent pid $pid" + [[ -o interactive ]] && echo "Reusing agent PID $pid" export SSH_AUTH_SOCK=$sock export SSH_AGENT_PID=$pid else @@ -83,45 +197,25 @@ if [[ -v _sev_reset_shell || $SHLVL == 1 ]] { echo -n $SSH_AUTH_SOCK$'\0'$SSH_AGENT_PID >!$agentfile fi unset okc agentfile sock pid - } elif [[ -v commands[gpg] && ! -S $_GNUPG_SOCK_DEST && \ - ( ! -v SSH_AUTH_SOCK || -v DISPLAY ) ]] { - export GPG_TTY=$(tty) - export PINENTRY_USER_DATA=USE_TTY=$((!${+DISPLAY})) - gpg-connect-agent UPDATESTARTUPTTY /bye >/dev/null 2>&1 - [[ -o interactive ]] && gpg-connect-agent /subst /serverpid \ - '/echo GPG agent pid ${get serverpid}' /bye - [[ ! -v SSH_AUTH_SOCK ]] && \ - export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket) - } else { - [[ -o interactive ]] && echo "Pre-existing or remote agent" - } - - ## gpg ssh forwarding - # ssh automatically tunnels SSH_AUTH_SOCK with the right config, but GPG - # doesn't—we use a RemoteForward rule in ~/.ssh/config that uses these env - # vars to push the gpg extra socket through when connecting via ssh - # HACK: this entire thing sucks but there is no other easy way that works - # out of the box with other systems - if [[ -v commands[gpgconf] ]] { - # if already connected over SSH, reuse forwarded socket for future - # connections; else use extra socket - sock=${SSH_CLIENT:+agent-socket} - export _GNUPG_SOCK_SRC=$(gpgconf --list-dirs ${sock:-agent-extra-socket}) - unset sock - # XXX: multiple SSH sessions to the same host will overwrite this - # socket, no way to send unique paths without configuring explicit - # SendEnv and AcceptEnv exclusions on client and host respectively - export _GNUPG_SOCK_DEST=/tmp/.gpg-agent-forward - # if socket exists already, we are on a RemoteForwarded client, so copy - # it over so that GPG sees it - # XXX: race condition if connecting multiple terminals at once - if [[ -S $_GNUPG_SOCK_DEST ]] { - unlink $_GNUPG_SOCK_SRC >/dev/null 2>&1 - mv $_GNUPG_SOCK_DEST $_GNUPG_SOCK_SRC >/dev/null + } elif [[ ! -v SSH_AUTH_SOCK && -v commands[gpg] ]] { + # since gpg agent was started above, we just have to export and notify + if [[ -o interactive ]] { + if [[ -v _sev_gpg_forwarded ]] { + echo 'Remote GPG agent' + } else { + gpg-connect-agent /subst /serverpid \ + '/echo GPG agent PID ${get serverpid}' /bye + } } + export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket) + } elif [[ -v SSH_AUTH_SOCK ]] { + [[ -o interactive ]] && echo "Preconfigured agent" + } else { + [[ -o interactive ]] && print -P "%F{red}No agent available" } } + ### load site-specific if [[ -f ~/.zshenv.local ]] { source ~/.zshenv.local } diff --git a/base/.zshrc b/base/.zshrc index 0da8429..7890dad 100644 --- a/base/.zshrc +++ b/base/.zshrc @@ -184,6 +184,11 @@ function sev_preexec { # save last exec time for bell # XXX: does not run for blank cmdline _sev_exectime=$SECONDS + # update gpg forward, to always have unique filename and avoid clashes + if [[ -v _GNUPG_SOCK_DEST_EXT ]] { + export _GNUPG_SOCK_DEST_EXT=$(date +%s).$RANDOM + export _GNUPG_SOCK_DEST=$_GNUPG_SOCK_DEST_BASE.$_GNUPG_SOCK_DEST_EXT + } } add-zsh-hook preexec sev_preexec function sev_precmd { diff --git a/bin/gpg-learn-keys b/bin/gpg-learn-keys index fc48cdb..54852b4 100755 --- a/bin/gpg-learn-keys +++ b/bin/gpg-learn-keys @@ -2,4 +2,5 @@ gpg-connect-agent <