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
|