From 8fbd9a063c0568bfbc043cdad16492553ce8defa Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Fri, 25 Oct 2024 11:18:52 +0200 Subject: [PATCH] feat: verify commit signature before push Check commit signature and if it fails, check if any signed tags associated with commit exist from a keyring that can be found only locally. For: https://github.com/ben-grande/qusal/issues/105 --- .codespellrc | 2 +- .github/workflows/main.yaml | 14 ++++++ .pre-commit-config.yaml | 18 +++++++ docs/CONTRIBUTE.md | 10 ++++ scripts/commit-verify.sh | 93 +++++++++++++++++++++++++++++++++++++ scripts/spec-get.sh | 2 +- scripts/toc-gen.sh | 4 +- 7 files changed, 139 insertions(+), 4 deletions(-) create mode 100755 scripts/commit-verify.sh diff --git a/.codespellrc b/.codespellrc index 5ed3cd6..9c94525 100644 --- a/.codespellrc +++ b/.codespellrc @@ -5,5 +5,5 @@ [codespell] skip = LICENSES,.git,*.asc,./rpm_spec/*-*.spec,*.muttrc,./salt/sys-cacher/files/server/conf/*_mirrors_*,./salt/dotfiles/files/vim/.config/vim/after/plugin/update-time.vim -ignore-words-list = uptodate,iterm,doas +ignore-words-list = uptodate,iterm,doas,fpr ignore-regex = (nnoremap|bind)\b.* diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1a94c42..52b8d09 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -70,3 +70,17 @@ jobs: else gitlint --debug --commits "${base}..${head}" fi + - name: Verify that commits have associated signatures + run: | + if test "${{ github.event_name}}" = "pull_request" + then + exit 0 + fi + base="${{ github.event.before }}" + head="${{ github.event.after }}" + if test "${base}" = "${head}" || test -z "${base}" + then + scripts/commit-verify.sh "${head}" + else + scripts/commit-verify.sh $(git rev-list --reverse ${base}..${head}) + fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 35f3f02..19cc56f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,7 @@ repos: language: script pass_filenames: true description: Prohibit Unicode + stages: [commit] - id: spell-lint name: spell-lint @@ -26,6 +27,7 @@ repos: language: script pass_filenames: true description: Spellcheck files + stages: [commit] - id: shell-lint name: shell-lint @@ -43,6 +45,7 @@ repos: \.(policy|asc|txt|top|sls|jinja|toml|vim|py|muttrc|nft|md|spec| list|sources|repo|socket|timer|service|y(a)?ml)$ description: Lint Shellscripts + stages: [commit] - id: markdown-lint name: markdown-lint @@ -51,6 +54,7 @@ repos: pass_filenames: true files: \.md$ description: Lint Markdown files + stages: [commit] - id: python-lint name: python-lint @@ -59,6 +63,7 @@ repos: pass_filenames: true files: \.py$ description: Lint Python files + stages: [commit] - id: salt-lint name: salt-lint @@ -67,6 +72,7 @@ repos: pass_filenames: true files: (^minion.d/.*.conf|\.(sls|top|jinja|j2|tmpl|tst))$ description: Lint Salt files + stages: [commit] - id: yaml-lint name: yaml-lint @@ -75,6 +81,7 @@ repos: pass_filenames: true files: \.(yaml|yml)$ description: Lint YAML files + stages: [commit] - id: qubesbuilder-gen name: qubesbuilder-gen @@ -84,6 +91,7 @@ repos: pass_filenames: false files: salt/\S+/README.md description: Check if .qubesbuilder is up to date + stages: [commit] # - id: spec-gen # name: spec-gen @@ -96,6 +104,7 @@ repos: # ^(rpm_spec/template/template.spec|salt/.*| # scripts/spec-(get|gen)\.sh)$ # description: Check if RPM SPEC files are up to date + # stages: [commit] - id: license-lint name: license-lint @@ -104,3 +113,12 @@ repos: language: python pass_filenames: false description: Lint files to comply with the REUSE Specification + stages: [commit] + + - id: commit-verify + name: commit-verify + entry: scripts/commit-verify.sh + language: script + pass_filenames: false + description: Verify if there is a valid signature associated with the revisions + stages: [push] diff --git a/docs/CONTRIBUTE.md b/docs/CONTRIBUTE.md index e0ac067..6882dc7 100644 --- a/docs/CONTRIBUTE.md +++ b/docs/CONTRIBUTE.md @@ -89,3 +89,13 @@ To run pre-commit linters: ```sh pre-commit run ``` + +### Maintainer's lint + +Note this is only required for maintainers. + +Install the `pre-push` hooks: + +```sh +pre-commit install -t pre-push +``` diff --git a/scripts/commit-verify.sh b/scripts/commit-verify.sh new file mode 100755 index 0000000..7573390 --- /dev/null +++ b/scripts/commit-verify.sh @@ -0,0 +1,93 @@ +#!/bin/sh + +## SPDX-FileCopyrightText: 2024 Benjamin Grande M. S. +## +## SPDX-License-Identifier: AGPL-3.0-or-later + +set -eu + +key_dir="${KEY_DIR:-"salt/qubes-builder/files/client/qusal/keys"}" +key_suffix="${KEY_SUFFIX:-".asc"}" + +usage(){ + printf '%s\n' "Usage: ${0##*/} [REV...] +Info: + Default key directory (KEY_DIR): '${key_dir}' + Default key suffix (KEY_SUFFIX): '${key_suffix}' +Example: + ${0##*/} # HEAD + ${0##*/} HEAD # HEAD + ${0##*/} a # revision 'a' + ${0##*/} \$(git rev-list HEAD~5..) # 5 revs before and until HEAD + ${0##*/} \$(git rev-list a^..) # from rev 'a' until HEAD + ${0##*/} \$(git rev-list a^..b) # from rev 'a' until revision 'b' + ${0##*/} \$(git rev-list a..) # from child of rev 'a' until HEAD + ${0##*/} \$(git rev-list HEAD) # all revs until HEAD + KEY_DIR=/path KEY_SUFFIX=.gpg ${0##*/} # custom key path and suffix" +} + +case "${1-}" in + -h|--?help) usage; exit 1;; + *) ;; +esac + +command -v git >/dev/null || + { printf '%s\n' "Missing program: git" >&2; exit 1; } +command -v gpg >/dev/null || + { printf '%s\n' "Missing program: gpg" >&2; exit 1; } +command -v gpgconf >/dev/null || + { printf '%s\n' "Missing program: gpgconf" >&2; exit 1; } +repo_toplevel="$(git rev-parse --show-toplevel)" +test -d "${repo_toplevel}" || exit 1 +cd "${repo_toplevel}" +unset repo_toplevel + +gpg_homedir="$(mktemp -d)" +trap 'rm -rf -- "${gpg_homedir}"' EXIT INT HUP QUIT ABRT +export GNUPGHOME="${gpg_homedir}" +otrust="${gpg_homedir}/otrust.txt" +gpg_agent="$(gpgconf --list-components | awk -F: '/^gpg-agent:/{print $3}')" +gpg_cmd="gpg --status-fd=2" + +${gpg_cmd} --agent-program "${gpg_agent}" \ + --import "${key_dir}"/*"${key_suffix}" >/dev/null 2>&1 + +${gpg_cmd} --with-colons --list-public-keys | awk -F ':' '{ + if (prev_line ~ /^pub$/ && $1 ~ /^fpr$/) { + print $10 ":6:" + } + prev_line = $1 +}' | tee -- "${otrust}" >/dev/null + +${gpg_cmd} --import-ownertrust "${otrust}" >/dev/null 2>&1 + +fail="0" + +for rev in "${@:-"HEAD"}"; do + tag_success="0" + rev="$(git rev-parse --verify "${rev}")" + + if git verify-commit -- "${rev}" >/dev/null 2>&1; then + continue + fi + + tag_list="$(git tag --points-at="${rev}")" + if test -n "${tag_list}"; then + for tag in ${tag_list}; do + if git verify-tag -- "${tag}" >/dev/null 2>&1; then + tag_success="1" + continue + fi + done + fi + if test "${tag_success}" = "1"; then + continue + fi + + fail=1 + printf '%s\n' "error: no valid signature associated with rev: ${rev}" >&2 +done + +if test "${fail}" = "1"; then + exit 1 +fi diff --git a/scripts/spec-get.sh b/scripts/spec-get.sh index 07c6340..8c926ce 100755 --- a/scripts/spec-get.sh +++ b/scripts/spec-get.sh @@ -22,7 +22,7 @@ block_max_chars(){ char_value="${2}" less_than="${3}" if test "${#char_value}" -ge "${less_than}"; then - err_msg="Error: ${char_key} is too long. Must be <${less_than} chars." + err_msg="error: ${char_key} is too long. Must be <${less_than} chars." printf '%s\n' "${err_msg}" >&2 printf '%s\n' "Key contents: ${char_value}" >&2 exit 1 diff --git a/scripts/toc-gen.sh b/scripts/toc-gen.sh index bc6badd..8147d07 100755 --- a/scripts/toc-gen.sh +++ b/scripts/toc-gen.sh @@ -21,7 +21,7 @@ esac ## update on save. if ! vim -e -c 'setf markdown' -c 'if !exists(":GenTocGFM") | cq | endif' -c q then - err_msg="Error: Vim Plugin mzlogin/vim-markdown-toc isn't installed." + err_msg="error: Vim Plugin mzlogin/vim-markdown-toc isn't installed." printf '%s\n' "${err_msg}" >&2 exit 1 fi @@ -29,7 +29,7 @@ fi for f in "${@}"; do if ! test -f "${f}"; then - printf '%s\n' "Error: Not a regular file: ${f}" >&2 + printf '%s\n' "error: Not a regular file: ${f}" >&2 exit 1 fi if ! grep -q -e "^## Table of Contents$" -- "${f}"; then