bin/yubikey-git (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
#!/bin/bash
# Opt this host in to git over ssh with the YubiKey git keys, 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 any one of the YubiKeys plugged in, one touch per fetch or push:
#
# git clone git@git.soarez.net:sz/<name>.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* copies of config/ssh/yubikey-git's keys, at
# mode 600, which ssh insists on and git
# cannot store. Each is a key's handle, not
# its private half: see that directory's
# config for why they are 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"
# One per YubiKey, each its own key; any of them will do.
keys=(id_ed25519_sk_git_main_nfc id_ed25519_sk_git_backup_nfc 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() {
local k key
need_fido_ssh
install -d -m 700 "${HOME}/.ssh"
for k in "${keys[@]}"; do
key="${HOME}/.ssh/${k}"
if [[ -e "${key}" ]] && ! cmp -s "${key}" "${src_dir}/${k}"; then
die "${key} exists and is a different key; move it aside first"
fi
install -m 600 "${src_dir}/${k}" "${key}"
install -m 644 "${src_dir}/${k}.pub" "${key}.pub"
done
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 one of the YubiKeys plugged in:"
echo " git clone git@git.soarez.net:sz/<name>.git"
}
disable() {
local k key
ours "${conf_link}" "${dir_link}/config" && rm "${conf_link}"
ours "${dir_link}" "${src_dir}" && rm "${dir_link}"
for k in "${keys[@]}"; do
key="${HOME}/.ssh/${k}"
if [[ -f "${key}" ]] && cmp -s "${key}" "${src_dir}/${k}"; then
rm -f "${key}" "${key}.pub"
fi
done
echo "${name}: disabled"
}
status() {
local on=y k key
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
for k in "${keys[@]}"; do
key="${HOME}/.ssh/${k}"
if [[ -f "${key}" ]] && cmp -s "${key}" "${src_dir}/${k}"; then echo "key handle: ${key}"; else echo "key handle: ${k} missing"; on=""; fi
done
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
|