#!/usr/bin/bash --

# Usage: $0 <source-dir> [<ref>]
# Example refs:
#  master
#  HEAD
#  mainstream/master
# Default ref: HEAD
set -euo pipefail
: "${KEYRING_DIR_GIT=}" "${NO_CHECK=}" "${DEBUG=}" "${VERBOSE=0}"
unset GNUPGHOME tags tag hash_len format expected_hash BUILDER_DIR

case $0 in (/*) dir=${0%/*}/;; (*/*) dir=./${0%/*};; (*) dir=.;; esac
BUILDER_DIR=$dir/..
export BUILDER_DIR

# shellcheck source=scripts/common
source "$dir/common"

[ "$DEBUG" = "1" ] && set -x

if [ "${NO_CHECK-}" != "" ]; then
    echo 'Sorry, NO_CHECK is no longer supported'>&2
    echo 'If you really want to turn of signature checking, use “INSECURE_SKIP_CHECKING” or “LESS_SECURE_SIGNED_COMMITS_SUFFICIENT” option'>&2
    exit 1
fi

case ${CHECK=signed-tag} in
(signed-tag|signed-tag-or-commit) :;;
(insecure-no-check)
    echo 'Please avoid calling this script if you don’t want signatures checked'>&2
    exit 1
    ;;
(*)
    printf 'Invalid value for $CHECK: %q\n' "$CHECK">&2
    exit 1
    ;;
esac

if [ -n "$KEYRING_DIR_GIT" ]; then
    GNUPGHOME="$(readlink -m "$KEYRING_DIR_GIT")"
    export GNUPGHOME
    if [ ! -d "$GNUPGHOME" ]; then
        mkdir -p "$GNUPGHOME"
        chmod 700 "$GNUPGHOME"
        gpg --import qubes-developers-keys.asc
        # Trust Qubes Master Signing Key
        echo '427F11FD0FAA4B080123F01CDDFA1A3E36879494:6:' | gpg --import-ownertrust
    fi
    if [ qubes-developers-keys.asc -nt "$GNUPGHOME/pubring.gpg" ]; then
        gpg --import qubes-developers-keys.asc
        touch "$GNUPGHOME/pubring.gpg"
    fi
    maintainers=$(env | grep -oP '^ALLOWED_COMPONENTS_[a-fA-F0-9]{40}') || :
    for maintainer in $maintainers
    do
        read -a allowed_components <<<"${!maintainer}"
        COMPONENT="$(basename "$1")"
        COMPONENT="${COMPONENT//./builder}"
        if elementIn "$COMPONENT" "${allowed_components[@]}"; then
            keyid=${maintainer#ALLOWED_COMPONENTS_}
            gpg --import "$BUILDER_DIR/keys/$keyid.asc" || exit 1
            echo "$keyid:6:" | gpg --import-ownertrust
        fi
    done
    gpgconf --kill gpg-agent
fi

pushd "$1" > /dev/null || exit 2

case $# in
    (1) expected_hash=$(git rev-parse --verify -q HEAD);;
    (2) expected_hash=$2;;
    (*) echo 'USAGE: verify-git-tag directory [revision]'>&2; exit 1;;
esac

hash_len=${#expected_hash}
if [ "$hash_len" -ne 40 ] && [ "$hash_len" -ne 64 ]; then
    echo "---> Bad Git hash value (wrong length); failing" >&2
    exit 1
elif ! [[ "$expected_hash" =~ ^[a-f0-9]+$ ]]; then
    echo "---> Bad Git hash value (bad character); failing" >&2
    exit 1
fi

verify_git_obj () {
    local content newsig_number
    content=$(git -c gpg.program=gpg -c gpg.minTrustLevel=fully "verify-$1" --raw -- "$2" 2>&1 >/dev/null) &&
    newsig_number=$(printf %s\\n "$content" | grep -c '^\[GNUPG:] NEWSIG') &&
    [ "$newsig_number" = 1 ] && {
        printf %s\\n "$content" |
        grep '^\[GNUPG:] TRUST_\(FULLY\|ULTIMATE\) 0 pgp\>' >/dev/null
    }
}

# Git format string, see man:git-for-each-ref(1) for details.
#
# The %(if)...%(then)...%(end) skips lightweight tags, which have no object to
# point to.  The colons allow a SHA-1 hash to be distinguished from a truncated
# SHA-256 hash, and also allow a truncated line to be detected.
format='%(if:equals=tag)%(objecttype)%(then)%(objectname):%(object):%(end)'
tags="$(git tag "--points-at=$expected_hash" "--format=$format" | head -c 500)" || exit 1
for tag in $tags; do
    if (( ${#tag} != hash_len * 2 + 2)); then
        echo '---> Bad Git hash value (wrong length); failing' >&2
        exit 1
    elif ! [[ "${tag:hash_len}" = ":$expected_hash:" ]]; then
        printf %s\\n "---> Tag has wrong hash (found ${tag:hash_len + 1:hash_len}, expected $expected_hash)">&2
        exit 1
    fi
    tag="${tag:0:hash_len}"
    if verify_git_obj tag "$tag"; then
        echo "---> Good tag $tag"
        exit 0
    elif [ "0$VERBOSE" -ge 1 ]; then
        echo "---> Signed tag $tag cannot be verified"
    fi
done

if [ -z "${tag+x}" ]; then
    echo "---> No tag pointing at $expected_hash"
else
    echo "---> No valid signed tag found!"
    if [ "0$VERBOSE" -eq 0 ]; then
        echo "---> One invalid tag: $tag"
    fi
fi

if verify_git_obj commit "$expected_hash"; then
    case $CHECK in
    (signed-tag-or-commit)
        echo "---> $expected_hash does not have a signed tag"
        echo "---> However, it is signed by a trusted key, and \$CHECK is set to $CHECK"
        echo "---> Accepting it anyway"
        exit 0
        ;;
    (signed-tag)
        echo "---> $expected_hash is a commit signed by a trusted key ― did the signer forget to add a tag?"
        ;;
    (*)
        echo "---> internal error (this is a bug)"
        ;;
    esac
fi

exit 1
