all repos — dot @ e01393235ef52fc05f2a4a3768f22e73eb12cd73

Shell, editor and terminal configuration, and the tools that go with them

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