#!/bin/bash
# git-verify-to-tip
# -----------------
#
# Verify PGP signatures on all commits from the last signed tag
# or any arbitrary object in the repository history.
#
# This script can be installed as hooks/pre-push.
#
# Configurable parameters
# -----------------------
# We always ensure the signing key is both GOOD and VALID, which means
# that the keys you are checking against should be imported into your
# gnupghome and signed by a trusted key (e.g. your own). If you want to
# use a different GNUPG directory other than the one in your home, you
# can "export GNUPGHOME=some/path" before running this script. In addition,
# you may explicitly specify the keys to trust in the ONLYKEYS parameter below,
# e.g. if you want to make sure the signatures came from a very small subset
# of developers. Pipe-separate multiple keys, e.g.:
# ONLYKEYS="ABAF11C65A2970B130ABE3C479BE3E4300411886|647F28654894E3BD457199BE38DBBDC86092693E"
ONLYKEYS=
#
# By default, we check signatures on every commit, but if you set this to
# --merges, we will only check signatures on merges. You can also add any
# other flags accepted by git-rev-list.
REVFLAGS=
#
# When set to "" we start from the latest annotated tag we find.
# You can also list an arbitrary commit object here.
# When running as hooks/pre-push, we ignore this entirely and use the
# commit information provided by git on stdin.
STARTFROM=
#
# We can also get these parameters from the git config. E.g.:
# [verify-to-tip]
#   onlykeys = ABAF11C65A2970B130ABE3C479BE3E4300411886|647F28654894E3BD457199BE38DBBDC86092693E
#   revflags = --merges
#   startfrom = abcdef123456
#
if [[ -z ${ONLYKEYS} ]]; then
    ONLYKEYS=$(git config --get verify-to-tip.onlykeys)
fi
if [[ -z ${REVFLAGS} ]]; then
    REVFLAGS=$(git config --get verify-to-tip.revflags)
fi
if [[ -z ${STARTFROM} ]]; then
    STARTFROM=$(git config --get verify-to-tip.startfrom)
fi

# End configuration

function verify_raw_gpg {
    # We are looking for [GNUPG:] GOODSIG and [GNUPG:] VALIDSIG
    # They must be both present, or this is not a valid sig
    COUNT=$(echo "${1}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
    if [[ ${COUNT} -lt 2 ]]; then
        return 1
    fi
    if [[ -z ${ONLYKEYS} ]]; then
        return 0
    fi
    if $(echo "${1}" | grep -q -E "^\[GNUPG:\] VALIDSIG .* (${ONLYKEYS})\$"); then
        return 0
    fi
    return 1
}

function verify_rev_range {
    REVRANGE=${1}
    REVFLAGS=${2}
    for REV in $(git rev-list ${REVRANGE} ${REVFLAGS}); do
        echo "Verifying $REV"
        RAWOUT=$(git verify-commit --raw ${REV} 2>&1)
        if ! verify_raw_gpg "${RAWOUT}"; then
            echo "CRITICAL: ${REV} signature did NOT verify:"
            echo "${RAWOUT}"
            return 1
        fi
    done
    return 0
}

# Are we running from hooks/pre-push? If so, $1 and $2 should be set.
if [[ -z "${1}${2}" ]]; then
    # Not running as a pre-push hook.
    if [[ -z ${STARTFROM} ]]; then
        # verify the last annotated tag
        STARTFROM=$(git describe --abbrev=0)
        RAWOUT=$(git verify-tag --raw ${STARTFROM} 2>&1)
    else
        # verify the arbitrary commit provided
        RAWOUT=$(git verify-commit --raw ${STARTFROM} 2>&1)
    fi

    echo "Verifying ${STARTFROM}"
    if ! verify_raw_gpg "${RAWOUT}"; then
        echo "CRITICAL: ${STARTFROM} signature did NOT verify:"
        echo "${RAWOUT}"
        exit 1
    fi
    REVRANGE="${STARTFROM}.."
    if ! verify_rev_range "${REVRANGE}" "${REVFLAGS}"; then
        exit 1
    fi
else
    # We are in a pre-push hook
    Z40="0000000000000000000000000000000000000000"

    while read LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
        if [[ ${LOCAL_SHA} == ${Z40} ]]; then
            # Ignore delete
            continue
        fi
        if [[ ${REMOTE_SHA} == ${Z40} ]]; then
           # New branch, examine all commits
           REVRANGE=${LOCAL_SHA}
        else
           # Update to existing branch, examine new commits
           REVRANGE="${REMOTE_SHA}..${LOCAL_SHA}"
        fi

        if ! verify_rev_range "${REVRANGE}" "${REVFLAGS}"; then
            exit 1
        fi

    done
fi

echo "Verified successfully."
exit 0
