dotfiles/files/git/.config/git/template/hooks/pre-receive.sample
Ben Grande d13a21a734
fix: avoid echo usage
Echo can interpret operand as an option and checking every variable to
be echoed is troublesome while with printf, if the format specifier is
present before the operand, printing as string can be enforced.
2024-08-06 18:12:46 +02:00

218 lines
6.1 KiB
Bash
Executable File

#!/bin/sh
# SPDX-FileCopyrightText: 2023 - 2024 Benjamin Grande M. S. <ben.grande.b@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
## Force signature validation for everything that can be signed.
## - signed pushes (if the server is configured to receive it);
## - signed commits, unsigned commit can be validated by a signed tag pointing
## to the commit;
## - signed tags.
##
## Enable signed pushes client side:
## [push]
## gpgSign = if-asked
##
## Enable signed pushes server side:
## [receive]
## advertisePushOptions = true
## certNonceSeed = "<random-string>"
##
## You may only want a set of keys to be able to sign the push and they might
## different from the keys that are able to sign commit and tags.
## Unfortunately git does not allow setting a different gpg home for this, you
## have to point GIT_PUSH_CERT_PGPHOMEDIR to the desired keyring. It is
## analogous to setting a GNUPGHOME but will take precedence over GNUPGHOME.
##
# shellcheck disable=SC2317
set -eu
command -v git >/dev/null || exit 1
: "${GIT_PUSH_CERT_STATUS:-}"
: "${GIT_PUSH_CERT_KEY:-}"
: "${GIT_PUSH_CERT_NONCE_STATUS:-}"
: "${GIT_PUSH_CERT_NONCE:-}"
obj_rejected=""
zero="$(git hash-object --stdin </dev/null | tr "0-9a-f" "0")"
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/XXXXXX")"
trap 'rm -f "${tmpdir}"' HUP INT QUIT ABRT TERM EXIT
err(){
printf '%s\n' "${*}" >&2
}
require_signed_push(){
if test -z "${GIT_PUSH_CERT-}"; then
err "Pushes must be signed but client didn't sign it."
exit 1
fi
check_signed_push
}
check_signed_push(){
if test -n "${GIT_PUSH_CERT_PGPHOMEDIR-}"; then
check_signed_push_keyring
return 0
fi
if test "${GIT_PUSH_CERT_STATUS}" = "G"; then
true "Cert validation succeeded. Key: ${GIT_PUSH_CERT_KEY}"
else
err "Cert validation failed. Status: ${GIT_PUSH_CERT_STATUS}"
err "Cert key: ${GIT_PUSH_CERT_KEY}"
exit 1
fi
if test "${GIT_PUSH_CERT_NONCE_STATUS}" = "OK"; then
true "Cert nonce validation succeeded. Nonce: ${GIT_PUSH_CERT_NONCE}"
else
err "Cert nonce validation failed. Status: ${GIT_PUSH_CERT_NONCE_STATUS}"
exit 1
fi
}
check_signed_push_keyring(){
true "Using keyring to verify push certificate: ${GIT_PUSH_CERT_PGPHOMEDIR}"
gpg="$(git config --get gpg.program || printf '%s\n' "gpg")"
cert="$(git cat-file blob "${GIT_PUSH_CERT}")"
begin_sig="-----BEGIN PGP SIGNATURE-----"
cert_msg="${tmpdir}"/cert.msg
cert_sig="${tmpdir}"/cert.sig
in_sig=0
printf '%s\n' "${cert}" | while read -r line; do
if test "${in_sig}" = "1" || test "${line}" = "${begin_sig}"; then
in_sig=1
printf '%s\n' "${line}" | tee -a "${cert_sig}"
elif test "${line}" != "${begin_sig}"; then
printf '%s\n' "${line}" | tee -a "${cert_msg}"
else
break
fi
done
gpg_out="$("${gpg}" --status-fd=1 --no-default-keyring \
--homedir "${GIT_PUSH_CERT_PGPHOMEDIR}" \
--verify "${cert_sig}" "${cert_msg}" 2>&1)"
gpg_ec="$?"
gpg_sig="$(printf '%s\n' "${gpg_out}" | awk '/ GOODSIG /{print $3}')"
if test "${gpg_ec}" = "0"; then
true "Cert validation succeeded. Key: ${gpg_sig}"
else
err "Cert validation failed. Verification output:"
err "${gpg_out}"
fi
rm -rf "${tmpdir}"
if test "${GIT_PUSH_CERT_NONCE_STATUS}" = "OK"; then
true "Cert nonce validation succeeded. Nonce: ${GIT_PUSH_CERT_NONCE}"
else
err "Cert nonce validation failed. Status: ${GIT_PUSH_CERT_NONCE_STATUS}"
exit 1
fi
}
## Force signed pushes if receive.certNonceSeed is set.
if git config --get receive.certNonceSeed >/dev/null 2>&1; then
require_signed_push
fi
while read -r oldrev newrev ref; do
true "${oldrev} ${newrev} ${ref}"
test "${newrev}" = "${zero}" && continue
if test "${oldrev}" = "${zero}"; then
objects="$(git rev-list "${newrev}")"
else
objects="$(git rev-list "${oldrev}..${newrev}")"
fi
obj_type="$(git cat-file -t "${newrev}")"
if test "${obj_type}" = "tag"; then
if git verify-tag "${newrev}" >/dev/null 2>&1; then
true "Tag validation succeeded for tag: ${newrev}"
else
obj_rejected="${obj_rejected-} ${newrev} "
continue
fi
commit_tag="$(git rev-list -n1 "${newrev}")"
obj_rejected="$(printf '%s\n' "${obj_rejected}" | \
sed -e "s/ ${commit_tag} //")"
err "Commit validation done by tag: ${commit_tag} ${newrev}"
continue
fi
for obj in ${objects}; do
if git verify-commit "${obj}" >/dev/null 2>&1; then
true "Commit validation succeeded commit: ${obj}"
continue
else
err "No valid signature in commit: ${obj}"
obj_rejected="${obj_rejected-} ${obj} "
fi
done
done
test -n "${obj_rejected}" || exit 0
obj_rejected="$(printf '%s\n' "${obj_rejected}" | tr -s " " |
sed -e "s/^ //" -e "s/ $//")"
err "Couldn't verify objects: ${obj_rejected}"
exit 1
## Force signed pushes if receive.certNonceSeed is set.
if git config --get receive.certNonceSeed >/dev/null 2>&1; then
require_signed_push
fi
while read -r oldrev newrev ref; do
true "${oldrev} ${newrev} ${ref}"
test "${newrev}" = "${zero}" && continue
if test "${oldrev}" = "${zero}"; then
objects="$(git rev-list "${newrev}")"
else
objects="$(git rev-list "${oldrev}..${newrev}")"
fi
obj_type="$(git cat-file -t "${newrev}")"
if test "${obj_type}" = "tag"; then
if git verify-tag "${newrev}" >/dev/null 2>&1; then
true "Tag validation succeeded for tag: ${newrev}"
else
obj_rejected="${obj_rejected-} ${newrev} "
continue
fi
commit_tag="$(git rev-list -n1 "${newrev}")"
obj_rejected="$(printf '%s\n' "${obj_rejected}" | \
sed -e "s/ ${commit_tag} //")"
err "Commit validation done by tag: ${commit_tag} ${newrev}"
continue
fi
for obj in ${objects}; do
if git verify-commit "${obj}" >/dev/null 2>&1; then
true "Commit validation succeeded for commit: ${obj}"
continue
else
err "No valid signature in commit: ${obj}"
obj_rejected="${obj_rejected-} ${obj} "
fi
done
done
test -n "${obj_rejected}" || exit 0
obj_rejected="$(printf '%s\n' "${obj_rejected}" | tr -s " " | \
sed -e "s/^ //;s/ $//")"
err "Couldn't verify objects: ${obj_rejected}"
exit 1