diff --git a/functions/_sagt b/functions/_sagt index d5b4ed9..e064bce 100644 --- a/functions/_sagt +++ b/functions/_sagt @@ -1,3 +1,3 @@ #compdef sagt -compadd -- kill ls op piv reset \ No newline at end of file +compadd -- gpg gpg-pin kill ls op piv reset diff --git a/tools/sagent.sh b/tools/sagent.sh index bd91789..5e08a72 100755 --- a/tools/sagent.sh +++ b/tools/sagent.sh @@ -154,7 +154,7 @@ check_pinentry() fi } -use_gpg_agent() +prepare_gpg_agent() { command -v gpgconf > /dev/null 2>&1 || fmt_fatal "gpgconf not found" command -v gpg-connect-agent > /dev/null 2>&1 || fmt_fatal "gpg-connect-agent not found" @@ -167,6 +167,11 @@ use_gpg_agent() gpgconf --launch gpg-agent gpg-connect-agent updatestartuptty /bye > /dev/null +} + +use_gpg_agent() +{ + prepare_gpg_agent local agent_socket agent_socket=$(gpgconf --list-dirs agent-ssh-socket) @@ -176,10 +181,37 @@ use_gpg_agent() fmt_note "using gpg-agent: $agent_socket" echo unset SSH_AGENT_PID - printf 'export GPG_TTY=%q\n' "$current_tty" + printf 'export GPG_TTY=%q\n' "$GPG_TTY" printf 'export SSH_AUTH_SOCK=%q\n' "$agent_socket" } +cache_gpg_pin() +{ + [[ $# -le 1 ]] || fmt_fatal "usage: sagt gpg-pin [KEY]" + command -v gpg > /dev/null 2>&1 || fmt_fatal "gpg not found" + prepare_gpg_agent + + local signing_key="${1:-}" + local temp_dir + local signature_file + local status=0 + local gpg_args=(--detach-sign) + + temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/sagent-gpg-pin.XXXXXXXXXX") || \ + fmt_fatal "failed to create a temporary directory" + signature_file="$temp_dir/signature.gpg" + gpg_args+=(--output "$signature_file") + [[ -z "$signing_key" ]] || gpg_args+=(--local-user "$signing_key") + + fmt_note "performing a test signature; enter the GPG PIN and touch the token if prompted" + printf 'sagt gpg-pin\n' | gpg "${gpg_args[@]}" || status=$? + + rm -f -- "$signature_file" + rmdir -- "$temp_dir" + [[ $status -eq 0 ]] || fmt_fatal "test signature failed (gpg exit $status)" + fmt_note "test signature completed; the GPG PIN should remain cached until the card or agent session is reset" +} + read_agent_file() { local agent_file="$1" @@ -290,6 +322,9 @@ route() gpg) use_gpg_agent ;; + gpg-pin) + cache_gpg_pin "${@:2}" + ;; reset) reset ;; diff --git a/tools/test-sagent-gpg-pin.sh b/tools/test-sagent-gpg-pin.sh new file mode 100755 index 0000000..4c54d5c --- /dev/null +++ b/tools/test-sagent-gpg-pin.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +SAGENT="$THIS_DIR/sagent.sh" +TEST_DIR=$(mktemp -d /tmp/sagent-gpg-pin.XXXXXX) +trap 'rm -rf "$TEST_DIR"' EXIT + +MOCK_BIN="$TEST_DIR/bin" +MOCK_HOME="$TEST_DIR/home" +MOCK_TMP="$TEST_DIR/tmp" +MOCK_GPG_ARGS="$TEST_DIR/gpg-args" +MOCK_GPG_INPUT="$TEST_DIR/gpg-input" +MOCK_AGENT_LOG="$TEST_DIR/agent-log" +mkdir -p "$MOCK_BIN" "$MOCK_HOME/gnupg" "$MOCK_HOME/sysconf" "$MOCK_TMP" +export MOCK_BIN MOCK_HOME MOCK_GPG_ARGS MOCK_GPG_INPUT MOCK_AGENT_LOG + +cat > "$MOCK_BIN/gpgconf" <<'EOF' +#!/usr/bin/env bash +printf 'gpgconf %s\n' "$*" >> "$MOCK_AGENT_LOG" +case "$*" in + '--list-dirs homedir') printf '%s\n' "$MOCK_HOME/gnupg" ;; + '--list-dirs sysconfdir') printf '%s\n' "$MOCK_HOME/sysconf" ;; + '--list-dirs bindir') printf '%s\n' "$MOCK_BIN" ;; + '--launch gpg-agent') ;; + *) exit 1 ;; +esac +EOF + +cat > "$MOCK_BIN/gpg-connect-agent" <<'EOF' +#!/usr/bin/env bash +printf 'gpg-connect-agent %s\n' "$*" >> "$MOCK_AGENT_LOG" +EOF + +cat > "$MOCK_BIN/tty" <<'EOF' +#!/usr/bin/env bash +printf '/dev/pts/mock\n' +EOF + +cat > "$MOCK_BIN/gpg" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$@" > "$MOCK_GPG_ARGS" +output='' +while [[ $# -gt 0 ]]; do + if [[ "$1" == '--output' ]]; then + output=$2 + shift 2 + else + shift + fi +done +cat > "$MOCK_GPG_INPUT" +printf 'mock signature\n' > "$output" +EOF + +printf '#!/usr/bin/env bash\n' > "$MOCK_BIN/pinentry" +chmod +x "$MOCK_BIN/gpgconf" "$MOCK_BIN/gpg-connect-agent" "$MOCK_BIN/tty" \ + "$MOCK_BIN/gpg" "$MOCK_BIN/pinentry" + +output=$(HOME="$MOCK_HOME" TMPDIR="$MOCK_TMP" PATH="$MOCK_BIN:$PATH" "$SAGENT" gpg-pin TEST-KEY 2> "$TEST_DIR/stderr") +[[ -z "$output" ]] +grep -Fxq -- '--detach-sign' "$MOCK_GPG_ARGS" +grep -Fxq -- '--local-user' "$MOCK_GPG_ARGS" +grep -Fxq -- 'TEST-KEY' "$MOCK_GPG_ARGS" +grep -Fxq -- 'sagt gpg-pin' "$MOCK_GPG_INPUT" +grep -Fq -- 'gpgconf --launch gpg-agent' "$MOCK_AGENT_LOG" +grep -Fq -- 'gpg-connect-agent updatestartuptty /bye' "$MOCK_AGENT_LOG" +grep -Fq -- 'test signature completed' "$TEST_DIR/stderr" + +output=$(HOME="$MOCK_HOME" TMPDIR="$MOCK_TMP" PATH="$MOCK_BIN:$PATH" "$SAGENT" gpg-pin 2> "$TEST_DIR/stderr") +[[ -z "$output" ]] +! grep -Fxq -- '--local-user' "$MOCK_GPG_ARGS" +[[ -z $(find "$MOCK_TMP" -mindepth 1 -print -quit) ]] + +echo "sagent gpg-pin tests passed" diff --git a/tools/test.zsh b/tools/test.zsh index 6789281..63ea27b 100644 --- a/tools/test.zsh +++ b/tools/test.zsh @@ -35,6 +35,7 @@ doll dfs cd tools/test-getopts.sh tools/test-riot-gpg.sh +tools/test-sagent-gpg-pin.sh tools/common.sh get_os_name test $(echo y | tools/common.sh ask_for_yN "test") = "1" test $(echo n | tools/common.sh ask_for_yN "test") = "0"