this repo has no description
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)."