summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-25 17:56:18 +0200
committerIgor Soarez <igor@soarez.org>2026-10-25 17:56:18 +0200
commit4f40b8ea2f224132dddde4f505191c3662620741 (patch)
treec3b1727da042e808b6445db2f6a2c089c2e38541 /bin
parent9064d88bf109cd51ec53bd50815623b71d19e832 (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-xbin/yubikey-git138
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