Files
dotfiles/bin/load-yubikey-piv-ssh
2026-05-19 10:07:40 +02:00

259 lines
6.7 KiB
Bash
Executable File

#!/bin/zsh
# Load YubiKey PIV SSH keys into the macOS launchd-managed ssh-agent.
# The default health check verifies the PIV AUTH key with ssh-add -T so
# listed-but-stale PKCS#11 keys are reloaded instead of being trusted.
set -u
PKCS11_LIB="${PKCS11_LIB:-/usr/local/lib/opensc-pkcs11.so}"
PIV_KEY_PATTERN="${PIV_KEY_PATTERN:-PIV AUTH pubkey|SIGN pubkey|KEY MAN pubkey|CARD AUTH pubkey}"
PIV_AUTH_PATTERN="${PIV_AUTH_PATTERN:-PIV AUTH pubkey}"
VERIFY_MODE="auth"
TMP_DIRS=()
SCRIPT_NAME="${0:t}"
usage() {
# Print the small CLI surface; normal operation intentionally has few knobs.
cat <<EOF
Usage: ${SCRIPT_NAME} [--no-verify] [--help]
Safely load YubiKey PIV keys into the macOS launchd ssh-agent.
Options:
--no-verify Only check whether PIV keys are listed in the agent.
--help Show this help.
Environment:
PKCS11_LIB PKCS#11 provider path. Default: /usr/local/lib/opensc-pkcs11.so
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--no-verify)
VERIFY_MODE="none"
;;
--help|-h)
usage
exit 0
;;
*)
print -u2 -- "load-yubikey-piv-ssh: unknown option: $1"
usage >&2
exit 2
;;
esac
shift
done
cleanup() {
# Remove temporary public-key files created for ssh-add -T verification.
local dir
for dir in "${TMP_DIRS[@]}"; do
[[ -n "${dir}" && -d "${dir}" ]] && rm -rf -- "${dir}"
done
}
trap cleanup EXIT INT TERM
die() {
# Report a fatal script error with a stable prefix for shell output.
print -u2 -- "load-yubikey-piv-ssh: $*"
exit 1
}
note() {
# Emit status messages to stderr so stdout stays clean for callers.
print -u2 -- "load-yubikey-piv-ssh: $*"
}
agent_socket() {
# Return the launchd-published ssh-agent socket if it exists.
local sock
sock="$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null || true)"
[[ -n "${sock}" && -S "${sock}" ]] || return 1
print -r -- "${sock}"
}
use_launchd_agent() {
# Force this shell to use Apple's launchd-managed agent socket and ignore
# stale SSH_AGENT_PID values from manually started agents.
local sock
sock="$(agent_socket)" || die "could not find launchd SSH_AUTH_SOCK"
export SSH_AUTH_SOCK="${sock}"
unset SSH_AGENT_PID
}
agent_has_piv_keys() {
# Check for any listed YubiKey PIV identities in the active agent.
ssh-add -l 2>/dev/null | egrep -q "${PIV_KEY_PATTERN}"
}
loaded_piv_public_keys() {
# Select loaded public keys for either health verification or no-verify mode.
local pattern
case "${VERIFY_MODE}" in
auth) pattern="${PIV_AUTH_PATTERN}" ;;
none) pattern="${PIV_KEY_PATTERN}" ;;
*) die "internal error: unknown verify mode: ${VERIFY_MODE}" ;;
esac
ssh-add -L 2>/dev/null | egrep "${pattern}" || true
}
verify_loaded_piv_keys() {
# Export the loaded PIV AUTH public key to a temp file and ask ssh-add to
# perform a local sign-and-verify operation through the agent.
[[ "${VERIFY_MODE}" == "none" ]] && return 0
local tmp key_file line count
local -a key_files
tmp="$(mktemp -d "${TMPDIR:-/tmp}/load-yubikey-piv-ssh.XXXXXX")" ||
die "failed to create temporary directory"
TMP_DIRS+=("${tmp}")
count=0
while IFS= read -r line; do
[[ -n "${line}" ]] || continue
count=$((count + 1))
key_file="${tmp}/piv-${count}.pub"
print -r -- "${line}" >| "${key_file}" ||
die "failed to write temporary public key"
chmod 600 "${key_file}" || die "failed to chmod temporary public key"
key_files+=("${key_file}")
done < <(loaded_piv_public_keys)
if [[ "${count}" -eq 0 ]]; then
note "no matching PIV AUTH public key found for verification"
return 1
fi
SSH_ASKPASS_REQUIRE=never ssh-add -T "${key_files[@]}" >/dev/null 2>&1
}
agent_has_healthy_piv_keys() {
# A healthy state requires both listed PIV keys and a successful sign test.
if ! agent_has_piv_keys; then
return 1
fi
if verify_loaded_piv_keys; then
return 0
fi
return 1
}
agent_responds() {
# Treat "no identities" as a working agent, but reject broken sockets.
ssh-add -l >/dev/null 2>&1
case "$?" in
0|1) return 0 ;; # 1 means a reachable agent with no identities.
*) return 1 ;;
esac
}
restart_launchd_agent() {
# Restart only the launchd-managed ssh-agent. macOS may block kickstart under
# SIP, so fall back to terminating the exact PID reported by launchctl.
local pid
local i
note "restarting launchd ssh-agent"
if launchctl kickstart -k "gui/${UID}/com.openssh.ssh-agent" >/dev/null 2>&1; then
sleep 1
use_launchd_agent
return 0
fi
pid="$(launchctl print "gui/${UID}/com.openssh.ssh-agent" 2>/dev/null |
awk '/pid = / { print $3; exit }')"
if [[ -n "${pid}" ]]; then
note "launchctl kickstart failed; terminating launchd ssh-agent pid ${pid}"
kill -TERM "${pid}" >/dev/null 2>&1 ||
die "failed to terminate launchd ssh-agent pid ${pid}"
for i in {1..10}; do
kill -0 "${pid}" >/dev/null 2>&1 || break
sleep 0.2
done
if kill -0 "${pid}" >/dev/null 2>&1; then
note "launchd ssh-agent pid ${pid} did not exit after TERM; sending KILL"
kill -KILL "${pid}" >/dev/null 2>&1 ||
die "failed to kill launchd ssh-agent pid ${pid}"
fi
else
note "launchctl kickstart failed and no running launchd ssh-agent pid was found"
fi
use_launchd_agent
if ! agent_responds; then
die "failed to restart launchd ssh-agent"
fi
}
load_provider() {
# Load the OpenSC PKCS#11 provider and force PIN entry on the terminal.
SSH_ASKPASS_REQUIRE=never ssh-add -s "${PKCS11_LIB}"
}
verify_or_report_stale() {
# Treat listed PIV keys as healthy only after verification succeeds.
if agent_has_healthy_piv_keys; then
case "${VERIFY_MODE}" in
none) note "PIV keys are already loaded" ;;
auth) note "PIV AUTH key is already loaded and verified" ;;
esac
return 0
fi
if agent_has_piv_keys; then
note "PIV keys are listed but failed verification; treating agent as stale"
fi
return 1
}
[[ -r "${PKCS11_LIB}" ]] || die "PKCS#11 library is not readable: ${PKCS11_LIB}"
use_launchd_agent
if verify_or_report_stale; then
exit 0
fi
if agent_has_piv_keys; then
restart_launchd_agent
elif ! agent_responds; then
restart_launchd_agent
fi
if load_provider && agent_has_healthy_piv_keys; then
case "${VERIFY_MODE}" in
none) note "PIV keys loaded" ;;
auth) note "PIV keys loaded and PIV AUTH key verified" ;;
esac
exit 0
fi
note "initial load failed; checking agent state"
if verify_or_report_stale; then
exit 0
fi
restart_launchd_agent
if load_provider && agent_has_healthy_piv_keys; then
case "${VERIFY_MODE}" in
none) note "PIV keys loaded after agent restart" ;;
auth) note "PIV keys loaded after agent restart and PIV AUTH key verified" ;;
esac
exit 0
fi
die "failed to load and verify PIV keys"