#!/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 <&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"