92 lines · 2.7 KB · Shell
RawDownload
#!/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"' 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 = Nonefor 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 1fi 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)."