this repo has no description
at main 92 lines 2.7 kB view raw
1#!/usr/bin/env bash 2set -euo pipefail 3 4# Verifies that the appcast entry for the given version has a valid ed25519 signature 5# and that the enclosure length matches the downloaded archive. 6# 7# Usage: SPARKLE_PRIVATE_KEY_FILE=/path/to/key ./Scripts/verify_appcast.sh [version] 8 9ROOT=$(cd "$(dirname "$0")/.." && pwd) 10VERSION=${1:-$(source "$ROOT/version.env" && echo "$MARKETING_VERSION")} 11APPCAST="${ROOT}/appcast.xml" 12 13if [[ -z "${SPARKLE_PRIVATE_KEY_FILE:-}" ]]; then 14 echo "SPARKLE_PRIVATE_KEY_FILE is required" >&2 15 exit 1 16fi 17if [[ ! -f "$SPARKLE_PRIVATE_KEY_FILE" ]]; then 18 echo "Sparkle key file not found: $SPARKLE_PRIVATE_KEY_FILE" >&2 19 exit 1 20fi 21if [[ ! -f "$APPCAST" ]]; then 22 echo "appcast.xml not found at $APPCAST" >&2 23 exit 1 24fi 25 26# Clean the key file: strip comments/blank lines and require exactly one line of base64. 27function cleaned_key_path() { 28 local tmp key_lines 29 key_lines=$(grep -v '^[[:space:]]*#' "$SPARKLE_PRIVATE_KEY_FILE" | sed '/^[[:space:]]*$/d') 30 if [[ $(printf "%s\n" "$key_lines" | wc -l) -ne 1 ]]; then 31 echo "Sparkle key file must contain exactly one base64 line (no comments/blank lines)." >&2 32 exit 1 33 fi 34 tmp=$(mktemp) 35 printf "%s" "$key_lines" > "$tmp" 36 echo "$tmp" 37} 38 39KEY_FILE=$(cleaned_key_path) 40trap 'rm -f "$KEY_FILE" "$TMP_ZIP"' EXIT 41 42TMP_ZIP=$(mktemp /tmp/codexbar-enclosure.XXXX.zip) 43 44python3 - "$APPCAST" "$VERSION" >"$TMP_ZIP.meta" <<'PY' 45import sys, xml.etree.ElementTree as ET 46 47appcast = sys.argv[1] 48version = sys.argv[2] 49tree = ET.parse(appcast) 50root = tree.getroot() 51ns = {"sparkle": "http://www.andymatuschak.org/xml-namespaces/sparkle"} 52 53entry = None 54for item in root.findall("./channel/item"): 55 sv = item.findtext("sparkle:shortVersionString", default="", namespaces=ns) 56 if sv == version: 57 entry = item 58 break 59 60if entry is None: 61 sys.exit("No appcast entry found for version {}".format(version)) 62 63enclosure = entry.find("enclosure") 64url = enclosure.get("url") 65sig = enclosure.get("{http://www.andymatuschak.org/xml-namespaces/sparkle}edSignature") 66length = enclosure.get("length") 67 68if not all([url, sig, length]): 69 sys.exit("Missing url/signature/length in appcast for version {}".format(version)) 70 71print(url) 72print(sig) 73print(length) 74PY 75 76readarray -t META <"$TMP_ZIP.meta" 77URL="${META[0]}" 78SIG="${META[1]}" 79LEN_EXPECTED="${META[2]}" 80 81echo "Downloading enclosure: $URL" 82curl -L -o "$TMP_ZIP" "$URL" 83 84LEN_ACTUAL=$(stat -f%z "$TMP_ZIP") 85if [[ "$LEN_ACTUAL" != "$LEN_EXPECTED" ]]; then 86 echo "Length mismatch: expected $LEN_EXPECTED, got $LEN_ACTUAL" >&2 87 exit 1 88fi 89 90echo "Verifying Sparkle signature…" 91sign_update --verify "$TMP_ZIP" "$SIG" --ed-key-file "$KEY_FILE" 92echo "Appcast entry for $VERSION verified (signature and length match)."