#!/usr/bin/bash
#
#   verify_signature.sh - functions for checking PGP signatures
#
#   Copyright (c) 2011-2021 Pacman Development Team <pacman-dev@archlinux.org>
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

[[ -n "$LIBMAKEPKG_INTEGRITY_VERIFY_SIGNATURE_SH" ]] && return
LIBMAKEPKG_INTEGRITY_VERIFY_SIGNATURE_SH=1

LIBRARY=${LIBRARY:-'/usr/share/makepkg'}

source "$LIBRARY/util/message.sh"
source "$LIBRARY/util/pkgbuild.sh"

# Filter the contents of a GnuPG statusfile to only contain understood terms to narrow the file's scope and circumvent
# the use of terms (e.g. NOTATION_DATA) that may contain unescaped binary data
filter_gnupg_statusfile() {
	grep -E "(.*SIG| TRUST_.*)"
}

check_pgpsigs() {
	(( SKIPPGPCHECK )) && return 0
	! source_has_signatures && return 0

	msg "$(gettext "Verifying source file signatures with %s...")" "gpg"

	local netfile proto pubkey success status fingerprint trusted
	local warnings=0
	local errors=0
	local statusfile_raw="$(mktemp)"
	local statusfile=$(mktemp)
	local all_sources

	case $1 in
		all)
			get_all_sources 'all_sources'
			;;
		*)
			get_all_sources_for_arch 'all_sources'
			;;
	esac
	for netfile in "${all_sources[@]}"; do
		proto="$(get_protocol "$netfile")"

		if declare -f verify_${proto}_signature > /dev/null; then
			verify_${proto}_signature "$netfile" "$statusfile" || continue
		else
			verify_file_signature "$netfile" "$statusfile" || continue
		fi

		# these variables are assigned values in parse_gpg_statusfile
		success=0
		status=
		pubkey=
		fingerprint=
		trusted=
		parse_gpg_statusfile "$statusfile"
		if (( ! $success )); then
			printf '%s' "$(gettext "FAILED")" >&2
			case "$status" in
				"missingkey")
					printf ' (%s)' "$(gettext "unknown public key") $pubkey" >&2
					;;
				"revokedkey")
					printf " ($(gettext "public key %s has been revoked"))" "$pubkey" >&2
					;;
				"bad")
					printf ' (%s)' "$(gettext "bad signature from public key") $pubkey" >&2
					;;
				"error")
					printf ' (%s)' "$(gettext "error during signature verification")" >&2
					;;
			esac
			errors=1
		else
			if (( ${#validpgpkeys[@]} == 0 && !trusted )); then
				printf "%s ($(gettext "the public key %s is not trusted"))" $(gettext "FAILED") "$fingerprint" >&2
				errors=1
			elif (( ${#validpgpkeys[@]} > 0 )) && ! in_array "$fingerprint" "${validpgpkeys[@]}"; then
				printf "%s (%s %s)" "$(gettext "FAILED")" "$(gettext "invalid public key")" "$fingerprint" >&2
				errors=1
			else
				printf '%s' "$(gettext "Passed")" >&2
				case "$status" in
					"expired")
						printf ' (%s)' "$(gettext "WARNING:") $(gettext "the signature has expired.")" >&2
						warnings=1
						;;
					"expiredkey")
						printf ' (%s)' "$(gettext "WARNING:") $(gettext "the key has expired.")" >&2
						warnings=1
						;;
				esac
			fi
		fi
		printf '\n' >&2
	done

	rm -f "$statusfile" "$statusfile_raw"

	if (( errors )); then
		error "$(gettext "One or more PGP signatures could not be verified!")"
		exit 1
	fi

	if (( warnings )); then
		warning "$(gettext "Warnings have occurred while verifying the signatures.")"
		plainerr "$(gettext "Please make sure you really trust them.")"
	fi
}

verify_file_signature() {
	local netfile="$1" statusfile="$2"
	local file ext decompress found sourcefile

	file="$(get_filename "$netfile")"
	if [[ $file != *.@(sig?(n)|asc) ]]; then
		return 1
	fi

	printf "    %s ... " "${file%.*}" >&2

	if ! file="$(get_filepath "$netfile")"; then
		printf '%s\n' "$(gettext "SIGNATURE NOT FOUND")" >&2
		errors=1
		return 1
	fi

	found=0
	for ext in "" gz bz2 xz lrz lzo Z zst; do
		if sourcefile="$(get_filepath "${file%.*}${ext:+.$ext}")"; then
			found=1
			break
		fi
	done
	if (( ! found )); then
		printf '%s\n' "$(gettext "SOURCE FILE NOT FOUND")" >&2
		errors=1
		return 1
	fi

	case "$ext" in
		gz)  decompress="gzip -c -d -f" ;;
		bz2) decompress="bzip2 -c -d -f" ;;
		xz)  decompress="xz -c -d" ;;
		lrz) decompress="lrzip -q -d" ;;
		lzo) decompress="lzop -c -d -q" ;;
		zst) decompress="zstd -d -q -f" ;;
		Z)   decompress="uncompress -c -f" ;;
		"")  decompress="cat" ;;
	esac

	# verify the signature and write metadata to a status file
	if ! $decompress < "$sourcefile" | gpg --quiet --batch --status-file "$statusfile_raw" --verify "$file" - 2> /dev/null; then
		printf '%s\n' "$(gettext "%s is unable to verify the signature.")" "gpg" >&2
		errors=1
		return 1
	fi

	# create a statusfile that contains only understood terms
	if ! filter_gnupg_statusfile > "$statusfile" < "$statusfile_raw"; then
		printf '%s\n' "$(gettext "unable to extract signature metadata.")" >&2
		errors=1
		return 1
	fi

	return 0
}

verify_git_signature() {
	local netfile=$1 statusfile=$2
	local dir fragment query fragtype fragval

	dir=$(get_filepath "$netfile")
	fragment=$(get_uri_fragment "$netfile")
	query=$(get_uri_query "$netfile")

	if [[ $query != signed ]]; then
		return 1
	fi

	case ${fragment%%=*} in
		tag)
			fragtype=tag
			fragval=${fragment##*=}
			;;
		commit|branch)
			fragtype=commit
			fragval=${fragment##*=}
			;;
		'')
			fragtype=commit
			fragval=HEAD
	esac

	printf "    %s git repo ... " "${dir##*/}" >&2

	# verify the signature and write metadata to a status file
	if ! git -C "$dir" verify-$fragtype --raw "$fragval" > "$statusfile_raw" 2>&1; then
		printf '%s\n' "$(gettext "%s is unable to verify the signature.")" "git" >&2
		errors=1
		return 1
	fi

	# create a statusfile that contains only understood terms
	if ! filter_gnupg_statusfile > "$statusfile" < "$statusfile_raw"; then
		printf '%s\n' "$(gettext "unable to extract signature metadata.")" >&2
		errors=1
		return 1
	fi

	return 0
}

parse_gpg_statusfile() {
	local type arg1 arg6 arg10

	# ensure the NEWSIG keyword is part of the metadata
	if ! grep -qs NEWSIG "$statusfile"; then
		printf '%s\n' "$(gettext "SIGNATURE NOT FOUND")" >&2
		errors=1
		return 1
	fi

	while read -r _ type arg1 _ _ _ _ arg6 _ _ _ arg10 _; do
		case "$type" in
			GOODSIG)
				pubkey=$arg1
				success=1
				status="good"
				;;
			EXPSIG)
				pubkey=$arg1
				success=1
				status="expired"
				;;
			EXPKEYSIG)
				pubkey=$arg1
				success=1
				status="expiredkey"
				;;
			REVKEYSIG)
				pubkey=$arg1
				success=0
				status="revokedkey"
				;;
			BADSIG)
				pubkey=$arg1
				success=0
				status="bad"
				;;
			ERRSIG)
				pubkey=$arg1
				success=0
				if [[ $arg6 == 9 ]]; then
					status="missingkey"
				else
					status="error"
				fi
				;;
			VALIDSIG)
				if [[ $arg10 ]]; then
					# If the file was signed with a subkey, arg10 contains
					# the fingerprint of the primary key
					fingerprint=$arg10
				else
					fingerprint=$arg1
				fi
				;;
			TRUST_UNDEFINED|TRUST_NEVER)
				trusted=0
				;;
			TRUST_MARGINAL|TRUST_FULLY|TRUST_ULTIMATE)
				trusted=1
				;;
		esac
	done < "$1"
}

source_has_signatures() {
	local netfile all_sources proto

	get_all_sources_for_arch 'all_sources'
	for netfile in "${all_sources[@]}"; do
		proto="$(get_protocol "$netfile")"
		query=$(get_uri_query "$netfile")

		if [[ ${netfile%%::*} = *.@(sig?(n)|asc) ]] || \
				( declare -f verify_${proto}_signature > /dev/null && [[ $query = signed ]] ); then
			return 0
		fi
	done
	return 1
}
