#!/usr/bin/env bash
# Verify the GPG signature embedded in a Safeguard Desktop AppImage.
#
#   bash appimage-verify.sh Safeguard-<version>-x86_64.AppImage [public-key.asc]
#
# appimagetool --sign signs the lowercase hex SHA-256 (64 characters, no newline) of the
# AppImage with its .sha256_sig and .sig_key ELF sections filled with zeros, and stores the
# armoured detached signature in .sha256_sig (what `<AppImage> --appimage-signature` prints).
# This script recomputes that digest and checks the signature with gpg against the given
# public key, in a throwaway keyring (yours is not touched), without running the AppImage.
# It succeeds only for a good signature made by the expected key: the Safeguard Desktop
# release key below, or APPIMAGE_VERIFY_FPR (the fingerprint of another key you trust).
#
# The public key defaults to safeguard-desktop-release.asc next to this script (as published
# at https://safeguard.sh/keys/) or, in the sg-desktop checkout, build/appimage/.
# Needs bash, gpg, readelf (binutils), dd, head, tail and sha256sum.
set -euo pipefail

RELEASE_KEY_FPR=DC0FC295C8A0ACA245D7111EBCB516DE84C8F86C

die() { echo "appimage-verify: $*" >&2; exit 1; }
[ $# -ge 1 ] && [ $# -le 2 ] || die "usage: appimage-verify.sh <AppImage> [public-key.asc]"
image="$1"
[ -f "$image" ] || die "no such file: $image"
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
key="${2:-}"
if [ -z "$key" ]; then
  for candidate in "$here/safeguard-desktop-release.asc" "$here/../build/appimage/safeguard-desktop-release.asc"; do
    [ -f "$candidate" ] && { key="$candidate"; break; }
  done
  [ -n "$key" ] || die "no public key given and none next to this script"
fi
[ -f "$key" ] || die "no such key file: $key"
expected="${APPIMAGE_VERIFY_FPR:-$RELEASE_KEY_FPR}"
expected="$(printf '%s' "$expected" | tr -d ' ' | tr '[:lower:]' '[:upper:]')"
for tool in gpg readelf dd head tail sha256sum; do
  command -v "$tool" > /dev/null || die "$tool is required"
done

# section NAME -> "<offset> <size>" in bytes, from the ELF section headers.
section() {
  local line
  line="$(readelf -S -W "$image" 2> /dev/null | sed -E 's/^ *\[ *[0-9]+\] *//' | awk -v s="$1" '$1 == s && !n++ { print $4, $5 }')"
  [ -n "$line" ] || die "$image has no $1 section (not a type-2 AppImage?)"
  set -- $line
  echo "$((16#$1)) $((16#$2))"
}
read -r sig_off sig_len < <(section .sha256_sig)
read -r key_off key_len < <(section .sig_key)

tmp="$(mktemp -d)"
trap 'gpgconf --homedir "$tmp/gnupg" --kill gpg-agent > /dev/null 2>&1 || true; rm -rf "$tmp"' EXIT

# The embedded signature, read from the file (the runtime's --appimage-signature prints the same).
dd if="$image" bs=1 skip="$sig_off" count="$sig_len" status=none | tr -d '\0' > "$tmp/signature.asc"
grep -q 'BEGIN PGP SIGNATURE' "$tmp/signature.asc" || die "$image is not signed (its .sha256_sig section is empty)"

# The signed data: SHA-256 of the file with both signature sections zeroed, as lowercase hex.
# `tail | head -c` ends tail with SIGPIPE, so this pipeline runs without pipefail and its
# result is checked instead.
size="$(wc -c < "$image")"
if [ "$sig_off" -lt "$key_off" ]; then a_off=$sig_off a_len=$sig_len b_off=$key_off b_len=$key_len
else a_off=$key_off a_len=$key_len b_off=$sig_off b_len=$sig_len; fi
[ $((a_off + a_len)) -le "$b_off" ] && [ $((b_off + b_len)) -le "$size" ] || die "$image has overlapping or truncated signature sections"
set +o pipefail
{
  head -c "$a_off" "$image"
  head -c "$a_len" /dev/zero
  tail -c +"$((a_off + a_len + 1))" "$image" | head -c "$((b_off - a_off - a_len))"
  head -c "$b_len" /dev/zero
  tail -c +"$((b_off + b_len + 1))" "$image"
} | sha256sum | cut -c1-64 | tr -d '\n' > "$tmp/digest"
set -o pipefail
grep -Eqx '[0-9a-f]{64}' "$tmp/digest" || die "could not compute the digest of $image"

mkdir -m 700 "$tmp/gnupg"
gpg --homedir "$tmp/gnupg" --batch --quiet --import "$key" 2> /dev/null || die "cannot import $key"
status="$(gpg --homedir "$tmp/gnupg" --batch --status-fd 1 --verify "$tmp/signature.asc" "$tmp/digest" 2> "$tmp/gpg.log" || true)"
# VALIDSIG <signing-key fpr> … <primary-key fpr>: the primary key's fingerprint is the last field.
signer="$(printf '%s\n' "$status" | awk '$2 == "VALIDSIG" && !n++ { print (NF >= 12 ? $NF : $3) }')"
goodsig=0
case "$status" in "[GNUPG:] GOODSIG "* | *$'\n'"[GNUPG:] GOODSIG "*) goodsig=1 ;; esac
if [ "$goodsig" = 0 ] || [ -z "$signer" ]; then
  cat "$tmp/gpg.log" >&2
  die "BAD: the signature in $image does not verify against $key"
fi
[ "$signer" = "$expected" ] || die "BAD: signed by $signer, expected $expected"
grep -E 'Good signature|expire' "$tmp/gpg.log" | sed 's/^gpg: //' || true
echo "OK: $(basename "$image") is signed by $signer"
