#!/bin/bash # Opt this host in to git over ssh with the YubiKey git key, or back out. # # yubikey-git enable set it up; safe to re-run # yubikey-git disable undo everything enable did # yubikey-git status what is in place # # Then, with the YubiKey plugged in, one touch per fetch or push: # # git clone git@git.soarez.net:sz/.git # # For a machine that has this repo and nothing else -- one that is not trusted # with a key of its own. Where a private layer is installed, its git config # usually sends that URL elsewhere, and enabling this changes nothing there. # # What enable does, all of it undone by disable: # # ~/.ssh/id_ed25519_sk_git a copy of config/ssh/yubikey-git's key, at # mode 600, which ssh insists on and git # cannot store. It is the key's handle, not # its private half: see that directory's # config for why it is safe in this repo. # ~/.config/ssh/yubikey-git a link to that directory: the ssh config # and git.soarez.net's pinned host key # ~/.config/ssh/enabled.d/yubikey-git.conf # a link to its config, which is what turns it # on: config/ssh/entry includes enabled.d/*.conf # # On macOS it also needs Homebrew's OpenSSH, and installs it if Homebrew is # there: Apple's ssh cannot talk to a security key. git runs whichever ssh is # first on PATH, and config/env puts Homebrew's ahead of /usr/bin. # # Runs under macOS's /bin/bash 3.2. set -euo pipefail name="yubikey-git" key="${HOME}/.ssh/id_ed25519_sk_git" # The same path config/ssh/entry includes from, not $XDG_CONFIG_HOME. cfg="${HOME}/.config/ssh" dir_link="${cfg}/${name}" conf_link="${cfg}/enabled.d/${name}.conf" die() { >&2 echo "${name}: $*"; exit 1; } # This script's real directory: ~/.bin/yubikey-git is a link into the repo, and # macOS's readlink has no -f before 13. resolve_repo() { local src="$0" dir while [[ -L "${src}" ]]; do dir="$(cd "$(dirname "${src}")" && pwd)" src="$(readlink "${src}")" [[ "${src}" == /* ]] || src="${dir}/${src}" done cd "$(dirname "${src}")/.." && pwd } src_dir="$(resolve_repo)/config/ssh/${name}" [[ -f "${src_dir}/config" ]] || die "no ${src_dir}/config -- is this the dotfiles checkout?" # A link we made points at exactly this target; anything else there is not ours. ours() { [[ -L "$1" && "$(readlink "$1")" == "$2" ]]; } need_fido_ssh() { local ssh_bin ssh_bin="$(command -v ssh || true)" if [[ "$(uname)" == Darwin ]]; then if [[ "${ssh_bin}" == /usr/bin/ssh || -z "${ssh_bin}" ]]; then if [[ ! -x /opt/homebrew/bin/ssh && ! -x /usr/local/bin/ssh ]]; then command -v brew > /dev/null || die "Apple's ssh cannot use a security key, and there is no Homebrew to install OpenSSH with" echo "installing Homebrew's OpenSSH: Apple's ssh cannot use a security key" brew install openssh fi ssh_bin="$(command -v ssh || true)" [[ "${ssh_bin}" != /usr/bin/ssh ]] || die "Homebrew's ssh is installed but /usr/bin/ssh comes first on PATH; open a new shell and re-run" fi else ssh -Q key 2> /dev/null | grep -qx 'sk-ssh-ed25519@openssh.com' || die "this ssh ($(ssh -V 2>&1)) has no security-key support" fi } enable() { need_fido_ssh install -d -m 700 "${HOME}/.ssh" if [[ -e "${key}" ]] && ! cmp -s "${key}" "${src_dir}/id_ed25519_sk_git"; then die "${key} exists and is a different key; move it aside first" fi install -m 600 "${src_dir}/id_ed25519_sk_git" "${key}" install -m 644 "${src_dir}/id_ed25519_sk_git.pub" "${key}.pub" mkdir -p "${cfg}/enabled.d" if [[ -e "${dir_link}" || -L "${dir_link}" ]] && ! ours "${dir_link}" "${src_dir}"; then die "${dir_link} exists and is not this repo's; move it aside first" fi ln -sfn "${src_dir}" "${dir_link}" if [[ -e "${conf_link}" || -L "${conf_link}" ]] && ! ours "${conf_link}" "${dir_link}/config"; then die "${conf_link} exists and is not this repo's; move it aside first" fi ln -sfn "${dir_link}/config" "${conf_link}" # Fail now, not at the first push: a config ssh cannot parse breaks every ssh. if ! ssh -G git.soarez.net 2>&1 | grep -qix "identityfile ~/.ssh/id_ed25519_sk_git"; then ssh -G git.soarez.net > /dev/null || true disable > /dev/null die "ssh did not pick the config up, so it is undone; is ~/.ssh/config this repo's config/ssh/entry?" fi echo "${name}: enabled. With the YubiKey plugged in:" echo " git clone git@git.soarez.net:sz/.git" } disable() { ours "${conf_link}" "${dir_link}/config" && rm "${conf_link}" ours "${dir_link}" "${src_dir}" && rm "${dir_link}" if [[ -f "${key}" ]] && cmp -s "${key}" "${src_dir}/id_ed25519_sk_git"; then rm -f "${key}" "${key}.pub" fi echo "${name}: disabled" } status() { local on=y if ours "${conf_link}" "${dir_link}/config"; then echo "ssh config: enabled (${conf_link})"; else echo "ssh config: not enabled"; on=""; fi if ours "${dir_link}" "${src_dir}"; then echo "config dir: ${dir_link}"; else echo "config dir: missing"; on=""; fi if [[ -f "${key}" ]] && cmp -s "${key}" "${src_dir}/id_ed25519_sk_git"; then echo "key handle: ${key}"; else echo "key handle: missing"; on=""; fi echo "ssh: $(command -v ssh) ($(ssh -V 2>&1))" [[ -n "${on}" ]] } case "${1:-}" in enable) enable ;; disable) disable ;; status) status ;; *) >&2 echo "usage: ${name} enable|disable|status"; exit 2 ;; esac