diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-25 17:56:18 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-25 17:56:18 +0200 |
| commit | 4f40b8ea2f224132dddde4f505191c3662620741 (patch) | |
| tree | c3b1727da042e808b6445db2f6a2c089c2e38541 /bin | |
| parent | 9064d88bf109cd51ec53bd50815623b71d19e832 (diff) | |
yubikey-git: opt a host in to git over ssh at git.soarez.net with the YubiKey git key; ssh entry includes enabled.d/
Diffstat (limited to 'bin')
| -rwxr-xr-x | bin/yubikey-git | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/bin/yubikey-git b/bin/yubikey-git new file mode 100755 index 0000000..9083097 --- /dev/null +++ b/bin/yubikey-git @@ -0,0 +1,138 @@ +#!/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/<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 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/<name>.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 |
