#!/usr/bin/env bashset -euo pipefail# Verifies that the appcast entry for the given version has a valid ed25519 signature# and that the enclosure length matches the downloaded archive.## Usage: SPARKLE_PRIVATE_KEY_FILE=/path/to/key ./Scripts/verify_appcast.sh [version]ROOT=$(cd "$(dirname "$0")/.." && pwd)VERSION=${1:-$(source "$ROOT/version.env" && echo "$MARKETING_VERSION")}APPCAST="${ROOT}/appcast.xml"if [[ -z "${SPARKLE_PRIVATE_KEY_FILE:-}" ]]; then echo "SPARKLE_PRIVATE_KEY_FILE is required" >&2 exit 1fiif [[ ! -f "$SPARKLE_PRIVATE_KEY_FILE" ]]; then echo "Sparkle key file not found: $SPARKLE_PRIVATE_KEY_FILE" >&2 exit 1fiif [[ ! -f "$APPCAST" ]]; then echo "appcast.xml not found at $APPCAST" >&2 exit 1fi# Clean the key file: strip comments/blank lines and require exactly one line of base64.function cleaned_key_path() { local tmp key_lines key_lines=$(grep -v '^[[:space:]]*#' "$SPARKLE_PRIVATE_KEY_FILE" | sed '/^[[:space:]]*$/d') if [[ $(printf "%s\n" "$key_lines" | wc -l) -ne 1 ]]; then echo "Sparkle key file must contain exactly one base64 line (no comments/blank lines)." >&2 exit 1 fi tmp=$(mktemp) printf "%s" "$key_lines" > "$tmp" echo "$tmp"}KEY_FILE=$(cleaned_key_path)trap 'rm -f "$KEY_FILE" "$TMP_ZIP"' EXITTMP_ZIP=$(mktemp /tmp/codexbar-enclosure.XXXX.zip)python3 - "$APPCAST" "$VERSION" >"$TMP_ZIP.meta" <<'PY'import sys, xml.etree.ElementTree as ETappcast = sys.argv[1]version = sys.argv[2]tree = ET.parse(appcast)root = tree.getroot()ns = {"sparkle": "http://www.andymatuschak.org/xml-namespaces/sparkle"}entry = Nonefor item in root.findall("./channel/item"): sv = item.findtext("sparkle:shortVersionString", default="", namespaces=ns) if sv == version: entry = item breakif entry is None: sys.exit("No appcast entry found for version {}".format(version))enclosure = entry.find("enclosure")url = enclosure.get("url")sig = enclosure.get("{http://www.andymatuschak.org/xml-namespaces/sparkle}edSignature")length = enclosure.get("length")if not all([url, sig, length]): sys.exit("Missing url/signature/length in appcast for version {}".format(version))print(url)print(sig)print(length)PYreadarray -t META <"$TMP_ZIP.meta"URL="${META[0]}"SIG="${META[1]}"LEN_EXPECTED="${META[2]}"echo "Downloading enclosure: $URL"curl -L -o "$TMP_ZIP" "$URL"LEN_ACTUAL=$(stat -f%z "$TMP_ZIP")if [[ "$LEN_ACTUAL" != "$LEN_EXPECTED" ]]; then echo "Length mismatch: expected $LEN_EXPECTED, got $LEN_ACTUAL" >&2 exit 1fiecho "Verifying Sparkle signature…"sign_update --verify "$TMP_ZIP" "$SIG" --ed-key-file "$KEY_FILE"echo "Appcast entry for $VERSION verified (signature and length match)."
#!/usr/bin/env bash
set -euo pipefail
# Verifies that the appcast entry for the given version has a valid ed25519 signature
# and that the enclosure length matches the downloaded archive.
#
# Usage: SPARKLE_PRIVATE_KEY_FILE=/path/to/key ./Scripts/verify_appcast.sh [version]
ROOT=$(cd "$(dirname "$0")/.." && pwd)
VERSION=${1:-$(source "$ROOT/version.env" && echo "$MARKETING_VERSION")}
APPCAST="${ROOT}/appcast.xml"
if [[ -z "${SPARKLE_PRIVATE_KEY_FILE:-}" ]]; then
echo "SPARKLE_PRIVATE_KEY_FILE is required" >&2
exit 1
fi
if [[ ! -f "$SPARKLE_PRIVATE_KEY_FILE" ]]; then
echo "Sparkle key file not found: $SPARKLE_PRIVATE_KEY_FILE" >&2
exit 1
fi
if [[ ! -f "$APPCAST" ]]; then
echo "appcast.xml not found at $APPCAST" >&2
exit 1
fi
# Clean the key file: strip comments/blank lines and require exactly one line of base64.
function cleaned_key_path() {
local tmp key_lines
key_lines=$(grep -v '^[[:space:]]*#' "$SPARKLE_PRIVATE_KEY_FILE" | sed '/^[[:space:]]*$/d')
if [[ $(printf "%s\n" "$key_lines" | wc -l) -ne 1 ]]; then
echo "Sparkle key file must contain exactly one base64 line (no comments/blank lines)." >&2
exit 1
fi
tmp=$(mktemp)
printf "%s" "$key_lines" > "$tmp"
echo "$tmp"
}
KEY_FILE=$(cleaned_key_path)
trap 'rm -f "$KEY_FILE" "$TMP_ZIP"' EXIT
TMP_ZIP=$(mktemp /tmp/codexbar-enclosure.XXXX.zip)
python3 - "$APPCAST" "$VERSION" >"$TMP_ZIP.meta" <<'PY'
import sys, xml.etree.ElementTree as ET
appcast = sys.argv[1]
version = sys.argv[2]
tree = ET.parse(appcast)
root = tree.getroot()
ns = {"sparkle": "http://www.andymatuschak.org/xml-namespaces/sparkle"}
entry = None
for item in root.findall("./channel/item"):
sv = item.findtext("sparkle:shortVersionString", default="", namespaces=ns)
if sv == version:
entry = item
break
if entry is None:
sys.exit("No appcast entry found for version {}".format(version))
enclosure = entry.find("enclosure")
url = enclosure.get("url")
sig = enclosure.get("{http://www.andymatuschak.org/xml-namespaces/sparkle}edSignature")
length = enclosure.get("length")
if not all([url, sig, length]):
sys.exit("Missing url/signature/length in appcast for version {}".format(version))
print(url)
print(sig)
print(length)
PY
readarray -t META <"$TMP_ZIP.meta"
URL="${META[0]}"
SIG="${META[1]}"
LEN_EXPECTED="${META[2]}"
echo "Downloading enclosure: $URL"
curl -L -o "$TMP_ZIP" "$URL"
LEN_ACTUAL=$(stat -f%z "$TMP_ZIP")
if [[ "$LEN_ACTUAL" != "$LEN_EXPECTED" ]]; then
echo "Length mismatch: expected $LEN_EXPECTED, got $LEN_ACTUAL" >&2
exit 1
fi
echo "Verifying Sparkle signature…"
sign_update --verify "$TMP_ZIP" "$SIG" --ed-key-file "$KEY_FILE"
echo "Appcast entry for $VERSION verified (signature and length match)."