summaryrefslogtreecommitdiff
path: root/bin/yubikey-git
blob: 6e46c1f26054b8045281757865ede5eee7c6bf73 (plain)
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