diff --git a/config/git/config b/config/git/config index 18a4df89..0c62fabe 100644 --- a/config/git/config +++ b/config/git/config @@ -57,6 +57,12 @@ [core] whitespace=fix,-indent-with-non-tab,trailing-space,cr-at-eol +[gpg "ssh"] + # Signature verification is impossible without this: git reports every + # ssh-signed commit as unsigned and errors out per commit. Generated from + # your GitHub-registered signing keys by install/01_configure_git_signing.sh. + allowedSignersFile = ~/.config/git/allowed_signers + [apply] whitespace = nowarn diff --git a/install/01_configure_git_signing.sh b/install/01_configure_git_signing.sh new file mode 100755 index 00000000..8bcfaa58 --- /dev/null +++ b/install/01_configure_git_signing.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Build the ssh allowed-signers file git needs to verify commit signatures. +# +# Without it git cannot even attempt verification: every signed commit reads as +# unsigned, and anything printing %G? (see gg in shrc/git.sh) errors once per +# commit. Keys come from GitHub rather than ~/.ssh so commits signed on another +# machine still verify here. Re-run after registering a new signing key: +# +# ./install.sh --force 01_configure_git_signing + +set -euo pipefail + +# Hardcoded instead of XDG_CONFIG_HOME because config/git/config has to name +# this path literally: git config values cannot expand environment variables. +SIGNERS_FILE="${HOME:?}/.config/git/allowed_signers" + +if ! command -v gh >/dev/null 2>&1; then + echo "gh is not installed; skipping allowed signers setup" + exit 0 +fi + +email="$(git config --get user.email || true)" +github_user="$(git config --get github.user || true)" + +if [[ -z $email ]] || [[ -z $github_user ]]; then + echo "user.email and github.user must be set (see ~/.gitconfig.local); skipping allowed signers setup" + exit 0 +fi + +tmpfile="$(mktemp)" +trap 'rm -f "$tmpfile"' EXIT + +# This endpoint is public, so it does not matter which account gh is +# authenticated as. +if ! gh api "/users/$github_user/ssh_signing_keys" --jq '.[].key' | + awk -v email="$email" 'NF { printf "%s namespaces=\"git\" %s\n", email, $0 }' >"$tmpfile"; then + echo "Could not fetch signing keys for $github_user; leaving $SIGNERS_FILE unchanged" + exit 0 +fi + +if [[ ! -s $tmpfile ]]; then + echo "No ssh signing keys registered for $github_user; leaving $SIGNERS_FILE unchanged" + exit 0 +fi + +mkdir -p "$(dirname "$SIGNERS_FILE")" +mv "$tmpfile" "$SIGNERS_FILE" +chmod 644 "$SIGNERS_FILE" +echo "Wrote $(grep -c '' "$SIGNERS_FILE") signing keys to $SIGNERS_FILE" diff --git a/shrc/git.sh b/shrc/git.sh index bb301d0b..09906bff 100644 --- a/shrc/git.sh +++ b/shrc/git.sh @@ -412,6 +412,21 @@ divergent() { fi } +# Whether `git log` can verify the signatures it is about to print. git picks the +# verification backend from each signature's own payload, so an ssh-signed commit +# needs gpg.ssh.allowedSignersFile regardless of gpg.format; without it git errors +# once per commit and reports every commit as unsigned. Signing with ssh locally is +# a good proxy for "this history is ssh-signed", so a missing signers file only +# rules verification out in that case. +git_can_verify_signatures() { + local signers + signers="$(git config --get gpg.ssh.allowedSignersFile)" + if [[ -n $signers ]] && [[ -f ${signers/#\~/$HOME} ]]; then + return 0 + fi + [[ "$(git config --get gpg.format)" != "ssh" ]] +} + gg() { # Validate git setup before proceeding if ! validate_git_setup; then @@ -428,9 +443,17 @@ gg() { if [[ "$(current_branch)" != "$mainline_ref" ]]; then git fetch origin "$mainline_ref" fi + # %G? is the signature status: G good, B bad, U good but untrusted (signer not in + # the allowed signers file), X expired, Y expired key, R revoked key, E cannot + # check, N none. Dropped entirely when git cannot verify, so an unverifiable + # setup shows no column rather than a column of misleading Ns. + local sig='' + if git_can_verify_signatures; then + sig='%C(magenta)%G?%Creset ' + fi git log \ --graph \ - --pretty=format:'%Cred%h%Creset %aN: %s %Cgreen(%cr)%Creset' \ + --pretty=format:"%Cred%h%Creset ${sig}%aN: %s %Cgreen(%cr)%Creset" \ --abbrev-commit \ --date=relative \ "$(current_branch)" \