mirror of
https://github.com/DictXiong/dotfiles.git
synced 2026-08-26 00:46:52 +08:00
feat(riot): add GPG agent forwarding
Signed-off-by: Dict Xiong <me@beardic.cn>
This commit is contained in:
parent
6fa601c458
commit
5e7c3293ae
139
scripts/riot
139
scripts/riot
@ -5,6 +5,7 @@ source "$THIS_DIR/../tools/common.sh"
|
|||||||
RIOT_TRUST_CLIENT=${RIOT_TRUST_CLIENT:-${DFS_TRUST:-0}}
|
RIOT_TRUST_CLIENT=${RIOT_TRUST_CLIENT:-${DFS_TRUST:-0}}
|
||||||
RIOT_TRUST_SERVER=${RIOT_TRUST_SERVER:-0}
|
RIOT_TRUST_SERVER=${RIOT_TRUST_SERVER:-0}
|
||||||
EXTRA_SSH_OPTIONS=()
|
EXTRA_SSH_OPTIONS=()
|
||||||
|
GPG_FORWARD=0
|
||||||
|
|
||||||
# config
|
# config
|
||||||
RIOT_CONFIG_FILES=(
|
RIOT_CONFIG_FILES=(
|
||||||
@ -135,6 +136,129 @@ parse_remote() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_local_gpg_agent() {
|
||||||
|
LOCAL_GPG_EXTRA_SOCKET=""
|
||||||
|
GPG_FORWARD_ERROR=""
|
||||||
|
|
||||||
|
if ! command -v gpgconf > /dev/null 2>&1 || ! command -v gpg-connect-agent > /dev/null 2>&1; then
|
||||||
|
GPG_FORWARD_ERROR="gpgconf or gpg-connect-agent is not available"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local agent_info
|
||||||
|
agent_info=$(gpg-connect-agent --no-autostart 'GETINFO pid' /bye 2>/dev/null || true)
|
||||||
|
if ! grep -qE '^D [1-9][0-9]*$' <<< "$agent_info"; then
|
||||||
|
GPG_FORWARD_ERROR="local gpg-agent is not running"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOCAL_GPG_EXTRA_SOCKET=$(gpgconf --list-dirs agent-extra-socket 2>/dev/null || true)
|
||||||
|
if [[ -z "$LOCAL_GPG_EXTRA_SOCKET" || "$LOCAL_GPG_EXTRA_SOCKET" != /* \
|
||||||
|
|| "$LOCAL_GPG_EXTRA_SOCKET" == *:* || "$LOCAL_GPG_EXTRA_SOCKET" == *$'\r'* \
|
||||||
|
|| "$LOCAL_GPG_EXTRA_SOCKET" == *$'\n'* || ! -S "$LOCAL_GPG_EXTRA_SOCKET" ]]; then
|
||||||
|
GPG_FORWARD_ERROR="local gpg-agent extra socket is unavailable"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local extra_info
|
||||||
|
extra_info=$(gpg-connect-agent --raw-socket "$LOCAL_GPG_EXTRA_SOCKET" 'GETINFO version' /bye 2>/dev/null || true)
|
||||||
|
if ! grep -qE '^D [^[:space:]]+' <<< "$extra_info"; then
|
||||||
|
GPG_FORWARD_ERROR="local gpg-agent extra socket exists but is not accepting connections"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
probe_remote_gpg_socket() {
|
||||||
|
REMOTE_GPG_SOCKET=""
|
||||||
|
|
||||||
|
# The probe must be an independent connection: reusing or creating a
|
||||||
|
# multiplex master here races with the immediately following login.
|
||||||
|
local query_cmd=(ssh "-S" "none" "-o" "ClearAllForwardings=yes" "-o" "RequestTTY=no")
|
||||||
|
if [[ -n "$PORT" ]]; then
|
||||||
|
query_cmd+=("-p" "$PORT")
|
||||||
|
fi
|
||||||
|
query_cmd+=(
|
||||||
|
"${SSH_OPTIONS[@]}"
|
||||||
|
"${EXTRA_SSH_OPTIONS[@]}"
|
||||||
|
"-T"
|
||||||
|
"$USERNAME${USERNAME:+@}$SERVER"
|
||||||
|
'socket=$(gpgconf --list-dirs agent-socket 2>/dev/null) || exit 10
|
||||||
|
case "$socket" in /*/S.gpg-agent) ;; *) exit 11;; esac
|
||||||
|
case "$socket" in *:*) exit 11;; esac
|
||||||
|
case "$socket" in *"
|
||||||
|
"*) exit 11;; esac
|
||||||
|
carriage_return=$(printf "\r")
|
||||||
|
case "$socket" in *"$carriage_return"*) exit 11;; esac
|
||||||
|
if [ -e "$socket" ] && [ ! -S "$socket" ]; then exit 12; fi
|
||||||
|
if [ -S "$socket" ] && command -v gpg-connect-agent >/dev/null 2>&1; then
|
||||||
|
agent_mode=$(gpg-connect-agent --raw-socket "$socket" "GETINFO restricted" /bye 2>/dev/null || true)
|
||||||
|
case "$agent_mode" in *"D 1"*) exit 13;; esac
|
||||||
|
fi
|
||||||
|
systemd_socket=0
|
||||||
|
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active --quiet gpg-agent.socket >/dev/null 2>&1; then
|
||||||
|
systemd_socket=1
|
||||||
|
fi
|
||||||
|
gpgconf --kill gpg-agent >/dev/null 2>&1 || exit 14
|
||||||
|
rm -f "$socket" || exit 15
|
||||||
|
printf "%s\n%s\n" "$socket" "$systemd_socket"'
|
||||||
|
)
|
||||||
|
|
||||||
|
local output status
|
||||||
|
if output=$("${query_cmd[@]}"); then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
case "$status" in
|
||||||
|
0) ;;
|
||||||
|
10) GPG_FORWARD_ERROR="gpgconf is unavailable on the remote host" ;;
|
||||||
|
11) GPG_FORWARD_ERROR="remote gpgconf returned an invalid agent socket" ;;
|
||||||
|
12) GPG_FORWARD_ERROR="refusing to remove the non-socket remote gpg-agent path" ;;
|
||||||
|
13) GPG_FORWARD_ERROR="another forwarded gpg-agent is already using the remote socket; close that session first" ;;
|
||||||
|
14) GPG_FORWARD_ERROR="failed to stop the remote gpg-agent (the socket may belong to another forwarding session)" ;;
|
||||||
|
15) GPG_FORWARD_ERROR="failed to remove the stale remote gpg-agent socket" ;;
|
||||||
|
*) GPG_FORWARD_ERROR="failed to query or clean the remote gpg-agent socket (ssh status $status)" ;;
|
||||||
|
esac
|
||||||
|
if [[ "$status" != "0" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local remote_systemd_socket
|
||||||
|
REMOTE_GPG_SOCKET=${output%%$'\n'*}
|
||||||
|
remote_systemd_socket=${output#*$'\n'}
|
||||||
|
if [[ -z "$REMOTE_GPG_SOCKET" || "$REMOTE_GPG_SOCKET" == *$'\r'* || "$REMOTE_GPG_SOCKET" == *$'\n'* \
|
||||||
|
|| "$REMOTE_GPG_SOCKET" != /*/S.gpg-agent || "$REMOTE_GPG_SOCKET" == *:* \
|
||||||
|
|| "$remote_systemd_socket" != "0" && "$remote_systemd_socket" != "1" ]]; then
|
||||||
|
GPG_FORWARD_ERROR="remote gpgconf returned an invalid agent socket: $output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$remote_systemd_socket" == "1" ]]; then
|
||||||
|
fmt_warning "remote gpg-agent.socket is active and may race with GPG forwarding; consider disabling its socket activation"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_gpg_forwarding() {
|
||||||
|
if [[ "$DFS_DRY_RUN" == "1" ]]; then
|
||||||
|
REMOTE_GPG_SOCKET="<remote-gpg-agent-socket>"
|
||||||
|
LOCAL_GPG_EXTRA_SOCKET="<local-gpg-agent-extra-socket>"
|
||||||
|
else
|
||||||
|
if ! check_local_gpg_agent; then
|
||||||
|
fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR"
|
||||||
|
fi
|
||||||
|
if ! probe_remote_gpg_socket; then
|
||||||
|
fmt_fatal "cannot forward gpg-agent: $GPG_FORWARD_ERROR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_OPTIONS+=(
|
||||||
|
# The probe already removed the old socket. Do not let the main SSH
|
||||||
|
# connection unlink a path recreated during the gap between them.
|
||||||
|
"-o" "StreamLocalBindUnlink=no"
|
||||||
|
"-o" "ExitOnForwardFailure=yes"
|
||||||
|
"-R" "$REMOTE_GPG_SOCKET:$LOCAL_GPG_EXTRA_SOCKET"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
print_cmd() {
|
print_cmd() {
|
||||||
local output=""
|
local output=""
|
||||||
for s in "${CMD[@]}"; do
|
for s in "${CMD[@]}"; do
|
||||||
@ -303,8 +427,10 @@ remove_hostkey() {
|
|||||||
# main
|
# main
|
||||||
print_help()
|
print_help()
|
||||||
{
|
{
|
||||||
fmt_info "usage: $0 [-Ddhlqt] [--dry-run] [--dev] [--help] [--lite] [--quite] [--trust] [--tmux] [--password] [[-o ssh-option]...] remote [command] [--] [command-args]"
|
fmt_info "usage: $0 [-Ddghlqt] [--dry-run] [--dev] [--gpg] [--help] [--lite] [--quite] [--trust] [--tmux] [--password] [[-o ssh-option]...] remote [command] [--] [command-args]"
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
options:
|
||||||
|
- -g, --gpg: forward the local GPG agent during an interactive SSH login (trusted remotes only)
|
||||||
available commands:
|
available commands:
|
||||||
- ssh [ssh-command-args] (default)
|
- ssh [ssh-command-args] (default)
|
||||||
- tmux [ssh-command-args] (run ssh in multiple tmux windows)
|
- tmux [ssh-command-args] (run ssh in multiple tmux windows)
|
||||||
@ -330,6 +456,9 @@ router() {
|
|||||||
-t|--trust )
|
-t|--trust )
|
||||||
RIOT_TRUST_SERVER=1
|
RIOT_TRUST_SERVER=1
|
||||||
;;
|
;;
|
||||||
|
-g|--gpg )
|
||||||
|
GPG_FORWARD=1
|
||||||
|
;;
|
||||||
--tmux )
|
--tmux )
|
||||||
USE_TMUX=1
|
USE_TMUX=1
|
||||||
;;
|
;;
|
||||||
@ -369,6 +498,11 @@ router() {
|
|||||||
print_help
|
print_help
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ "$GPG_FORWARD" == "1" && ( \
|
||||||
|
( "${positional[1]}" != "" && "${positional[1]}" != "ssh" && "${positional[1]}" != "tmux" ) \
|
||||||
|
|| "${#positional[@]}" -gt 2 ) ]]; then
|
||||||
|
fmt_fatal "gpg-agent forwarding is only supported for interactive SSH login"
|
||||||
|
fi
|
||||||
for i in ${!remotes[@]}; do
|
for i in ${!remotes[@]}; do
|
||||||
remote="${remotes[i]}"
|
remote="${remotes[i]}"
|
||||||
local batch_func="${remote}.batch"
|
local batch_func="${remote}.batch"
|
||||||
@ -380,6 +514,9 @@ router() {
|
|||||||
case "${positional[1]}" in
|
case "${positional[1]}" in
|
||||||
ssh|tmux|"" )
|
ssh|tmux|"" )
|
||||||
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1
|
[[ "${positional[1]}" == tmux ]] && USE_TMUX=1
|
||||||
|
if [[ "$GPG_FORWARD" == "1" ]]; then
|
||||||
|
prepare_gpg_forwarding
|
||||||
|
fi
|
||||||
run_ssh ssh "${positional[@]:2}"
|
run_ssh ssh "${positional[@]:2}"
|
||||||
;;
|
;;
|
||||||
git )
|
git )
|
||||||
|
|||||||
130
tools/test-riot-gpg.sh
Executable file
130
tools/test-riot-gpg.sh
Executable file
@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
RIOT="$THIS_DIR/../scripts/riot"
|
||||||
|
TEST_DIR=$(mktemp -d /tmp/riot-gpg.XXXXXX)
|
||||||
|
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||||
|
|
||||||
|
MOCK_BIN="$TEST_DIR/bin"
|
||||||
|
MOCK_HOME="$TEST_DIR/home"
|
||||||
|
MOCK_LOCAL_SOCKET="$TEST_DIR/local/S.gpg-agent.extra"
|
||||||
|
MOCK_REMOTE_SOCKET="$TEST_DIR/remote/S.gpg-agent"
|
||||||
|
MOCK_GPG_LOG="$TEST_DIR/gpg.log"
|
||||||
|
MOCK_SSH_LOG="$TEST_DIR/ssh.log"
|
||||||
|
mkdir -p "$MOCK_BIN" "$MOCK_HOME" "${MOCK_LOCAL_SOCKET%/*}" "${MOCK_REMOTE_SOCKET%/*}"
|
||||||
|
export MOCK_LOCAL_SOCKET MOCK_REMOTE_SOCKET MOCK_GPG_LOG MOCK_SSH_LOG
|
||||||
|
|
||||||
|
make_stale_socket() {
|
||||||
|
rm -f "$1"
|
||||||
|
python3 - "$1" <<'PY'
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_UNIX)
|
||||||
|
sock.bind(sys.argv[1])
|
||||||
|
sock.close()
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
cat > "$MOCK_BIN/gpgconf" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'gpgconf %s\n' "$*" >> "$MOCK_GPG_LOG"
|
||||||
|
case "$*" in
|
||||||
|
'--list-dirs agent-extra-socket') printf '%s\n' "$MOCK_LOCAL_SOCKET" ;;
|
||||||
|
'--list-dirs agent-socket') printf '%s\n' "$MOCK_REMOTE_SOCKET" ;;
|
||||||
|
'--kill gpg-agent') exit "${MOCK_KILL_STATUS:-0}" ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_BIN/gpg-connect-agent" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'gpg-connect-agent %s\n' "$*" >> "$MOCK_GPG_LOG"
|
||||||
|
case "$*" in
|
||||||
|
*'GETINFO pid'*) printf 'D 123\nOK\n' ;;
|
||||||
|
*'GETINFO version'*)
|
||||||
|
[[ "${MOCK_LOCAL_LIVE:-1}" == 1 ]] || exit 1
|
||||||
|
printf 'D 2.4.0\nOK\n'
|
||||||
|
;;
|
||||||
|
*'GETINFO restricted'*) printf 'D %s\nOK\n' "${MOCK_REMOTE_RESTRICTED:-0}" ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_BIN/systemctl" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
[[ "${MOCK_SYSTEMD_ACTIVE:-0}" == 1 ]]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_BIN/ssh" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
{
|
||||||
|
printf 'CALL\n'
|
||||||
|
printf 'ARG=%s\n' "$@"
|
||||||
|
} >> "$MOCK_SSH_LOG"
|
||||||
|
|
||||||
|
is_probe=0
|
||||||
|
last_arg=''
|
||||||
|
for arg in "$@"; do
|
||||||
|
[[ "$arg" == '-T' ]] && is_probe=1
|
||||||
|
last_arg=$arg
|
||||||
|
done
|
||||||
|
if [[ "$is_probe" == 1 ]]; then
|
||||||
|
sh -c "$last_arg"
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "$MOCK_BIN/gpgconf" "$MOCK_BIN/gpg-connect-agent" "$MOCK_BIN/systemctl" "$MOCK_BIN/ssh"
|
||||||
|
|
||||||
|
run_riot() {
|
||||||
|
HOME="$MOCK_HOME" RIOT_TRUST_CLIENT=0 PATH="$MOCK_BIN:$PATH" "$RIOT" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_failure() {
|
||||||
|
local expected=$1
|
||||||
|
shift
|
||||||
|
if run_riot "$@" > "$TEST_DIR/out" 2> "$TEST_DIR/err"; then
|
||||||
|
echo "expected riot to fail: $*" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -Fq "$expected" "$TEST_DIR/err"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dry-run must not inspect or mutate either host.
|
||||||
|
: > "$MOCK_GPG_LOG"
|
||||||
|
: > "$MOCK_SSH_LOG"
|
||||||
|
DFS_DRY_RUN=1 run_riot -g example.test > "$TEST_DIR/out" 2> "$TEST_DIR/err"
|
||||||
|
grep -Fq '<remote-gpg-agent-socket>:<local-gpg-agent-extra-socket>' "$TEST_DIR/out"
|
||||||
|
[[ ! -s "$MOCK_GPG_LOG" && ! -s "$MOCK_SSH_LOG" ]]
|
||||||
|
|
||||||
|
# A socket inode without a listener must be rejected locally.
|
||||||
|
make_stale_socket "$MOCK_LOCAL_SOCKET"
|
||||||
|
MOCK_LOCAL_LIVE=0 expect_failure 'extra socket exists but is not accepting connections' -g example.test
|
||||||
|
|
||||||
|
# -g is valid only for an interactive SSH login.
|
||||||
|
expect_failure 'only supported for interactive SSH login' -g example.test scp ./a ./b
|
||||||
|
expect_failure 'only supported for interactive SSH login' -g example.test ssh -- true
|
||||||
|
|
||||||
|
# Never remove a regular file merely because it has the expected basename.
|
||||||
|
MOCK_LOCAL_LIVE=1
|
||||||
|
printf 'keep me\n' > "$MOCK_REMOTE_SOCKET"
|
||||||
|
expect_failure 'refusing to remove the non-socket remote gpg-agent path' -g example.test
|
||||||
|
grep -Fqx 'keep me' "$MOCK_REMOTE_SOCKET"
|
||||||
|
|
||||||
|
# A restricted agent at the remote socket represents another forwarding session.
|
||||||
|
make_stale_socket "$MOCK_REMOTE_SOCKET"
|
||||||
|
MOCK_REMOTE_RESTRICTED=1 expect_failure 'another forwarded gpg-agent is already using the remote socket' -g example.test
|
||||||
|
|
||||||
|
# A normal remote agent can be cleaned up; systemd activation is reported.
|
||||||
|
make_stale_socket "$MOCK_REMOTE_SOCKET"
|
||||||
|
: > "$MOCK_SSH_LOG"
|
||||||
|
MOCK_REMOTE_RESTRICTED=0 MOCK_SYSTEMD_ACTIVE=1 run_riot -g example.test > "$TEST_DIR/out" 2> "$TEST_DIR/err"
|
||||||
|
grep -Fq 'remote gpg-agent.socket is active' "$TEST_DIR/err"
|
||||||
|
grep -Fq 'ARG=none' "$MOCK_SSH_LOG"
|
||||||
|
grep -Fq 'ARG=ClearAllForwardings=yes' "$MOCK_SSH_LOG"
|
||||||
|
grep -Fq 'ARG=StreamLocalBindUnlink=no' "$MOCK_SSH_LOG"
|
||||||
|
grep -Fq "ARG=$MOCK_REMOTE_SOCKET:$MOCK_LOCAL_SOCKET" "$MOCK_SSH_LOG"
|
||||||
|
[[ ! -e "$MOCK_REMOTE_SOCKET" ]]
|
||||||
|
|
||||||
|
echo 'riot gpg forwarding tests passed'
|
||||||
@ -34,6 +34,7 @@ dogo
|
|||||||
doll
|
doll
|
||||||
dfs cd
|
dfs cd
|
||||||
tools/test-getopts.sh
|
tools/test-getopts.sh
|
||||||
|
tools/test-riot-gpg.sh
|
||||||
tools/common.sh get_os_name
|
tools/common.sh get_os_name
|
||||||
test $(echo y | tools/common.sh ask_for_yN "test") = "1"
|
test $(echo y | tools/common.sh ask_for_yN "test") = "1"
|
||||||
test $(echo n | tools/common.sh ask_for_yN "test") = "0"
|
test $(echo n | tools/common.sh ask_for_yN "test") = "0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user