#!/bin/bash # # Hawser sample verifier. # # Checks the public FocusTimer download end to end: the bytes you have, the # outer disk image, and the app inside it. Nothing is installed and nothing is # launched -- the image is mounted read-only and detached before this exits. # # curl -fsSLO https://hawserkit.com/FocusTimer.dmg # curl -fsSL https://hawserkit.com/verify.sh | bash -s FocusTimer.dmg # # Or, if piping a script from the internet into bash is not your idea of a # verification step (it should not be): download both, read this file, run it. # # curl -fsSLO https://hawserkit.com/verify.sh && less verify.sh # bash verify.sh FocusTimer.dmg # set -uo pipefail MANIFEST_URL="https://hawserkit.com/verify.json" DMG="${1:-FocusTimer.dmg}" MANIFEST="${HAWSER_MANIFEST:-}" pass=0; fail=0 ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass+1)); } no() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail+1)); } note() { printf ' %s\n' "$1"; } head1(){ printf '\n\033[1m%s\033[0m\n' "$1"; } trap 'if [ -n "${MOUNT:-}" ] && [ -d "${MOUNT:-}" ]; then hdiutil detach "$MOUNT" -quiet 2>/dev/null; fi' EXIT [ "$(uname -s)" = "Darwin" ] || { echo "This verifier only runs on macOS."; exit 2; } [ -f "$DMG" ] || { echo "Not found: $DMG"; echo "Usage: bash verify.sh /path/to/FocusTimer.dmg"; exit 2; } # ---------------------------------------------------------------- manifest --- # Pulled over HTTPS so the hash you compare against is the one we publish, not # one this script carries. Fall back to a local copy with HAWSER_MANIFEST=path. if [ -n "$MANIFEST" ]; then MJSON=$(cat "$MANIFEST" 2>/dev/null) MSRC="$MANIFEST" else MJSON=$(curl -fsSL --max-time 20 "$MANIFEST_URL" 2>/dev/null) MSRC="$MANIFEST_URL" fi [ -n "$MJSON" ] || { echo "Could not read the manifest ($MSRC)."; echo "Offline? Save verify.json locally and re-run with HAWSER_MANIFEST=./verify.json"; exit 2; } jget() { printf '%s' "$MJSON" | /usr/bin/python3 -c " import json,sys d=json.load(sys.stdin) for k in sys.argv[1].split('.'): d=d[k] print(d)" "$1" 2>/dev/null; } WANT_SHA=$(jget artifact.sha256) WANT_BYTES=$(jget artifact.bytes) WANT_TEAM=$(jget expect.team_id) WANT_AUTH=$(jget expect.authority) WANT_APP=$(jget expect.app_bundle) WANT_MINOS=$(jget expect.minimum_macos) WANT_SPARKLE=$(jget expect.sparkle_version) [ -n "$WANT_SHA" ] || { echo "Manifest at $MSRC is not readable as JSON."; exit 2; } echo "Hawser verifier -- $DMG" note "manifest: $MSRC" # --------------------------------------------------------- layer 1: bytes --- head1 "1. The bytes you downloaded" GOT_BYTES=$(stat -f%z "$DMG") if [ "$GOT_BYTES" = "$WANT_BYTES" ]; then ok "size is $GOT_BYTES bytes, as published" else no "size is $GOT_BYTES bytes, manifest says $WANT_BYTES"; fi GOT_SHA=$(shasum -a 256 "$DMG" | awk '{print $1}') if [ "$GOT_SHA" = "$WANT_SHA" ]; then ok "SHA-256 matches the published manifest" else no "SHA-256 does not match the published manifest" note "yours: $GOT_SHA" note "published: $WANT_SHA" note "Stop here. Do not mount this file. Re-download and run this again." exit 1 fi # ----------------------------------------------------- layer 2: outer disk --- head1 "2. The disk image itself" DINFO=$(codesign -dv --verbose=2 "$DMG" 2>&1) if printf '%s' "$DINFO" | grep -q "Authority=$WANT_AUTH"; then ok "signed by $WANT_AUTH" else no "not signed by the expected Developer ID"; note "$(printf '%s' "$DINFO" | grep -m1 '^Authority=' || echo 'no signing authority found')"; fi if printf '%s' "$DINFO" | grep -q "TeamIdentifier=$WANT_TEAM"; then ok "team identifier is $WANT_TEAM" else no "unexpected team identifier"; fi if printf '%s' "$DINFO" | grep -q "Notarization Ticket=stapled"; then ok "notarisation ticket is stapled to the image" else no "no stapled notarisation ticket on the image"; fi if xcrun stapler validate "$DMG" >/dev/null 2>&1; then ok "stapler validates the ticket offline" else no "stapler could not validate the ticket"; fi # The check the page used to be missing: Gatekeeper's verdict on the *image*, # under the assessment context a real double-click would use. GK=$(spctl -a -vv -t open --context context:primary-signature "$DMG" 2>&1) if printf '%s' "$GK" | grep -q ": accepted"; then ok "Gatekeeper accepts the image ($(printf '%s' "$GK" | grep -m1 '^source=' | cut -d= -f2-))" else no "Gatekeeper rejected the image"; note "$GK" fi # ------------------------------------------------------ layer 3: inner app --- head1 "3. The app inside it" MOUNT=$(mktemp -d /tmp/hawser-verify.XXXXXX) if ! hdiutil attach -readonly -nobrowse -noautoopen -mountpoint "$MOUNT" "$DMG" >/dev/null 2>&1; then no "could not mount the image read-only"; MOUNT=""; else ok "mounted read-only at $MOUNT (nothing is installed, nothing is launched)" APP="$MOUNT/$WANT_APP" if [ ! -d "$APP" ]; then no "$WANT_APP not found inside the image" else if codesign --verify --deep --strict "$APP" >/dev/null 2>&1; then ok "app passes codesign --verify --deep --strict" else no "app fails deep/strict verification"; fi if codesign --verify -R='anchor apple generic and certificate leaf[subject.OU]="'"$WANT_TEAM"'"' "$APP" >/dev/null 2>&1; then ok "app satisfies an Apple-anchored requirement pinned to $WANT_TEAM" else no "app does not satisfy the pinned Developer ID requirement"; fi if spctl -a -vv -t install "$APP" 2>&1 | grep -q ": accepted"; then ok "Gatekeeper accepts the app" else no "Gatekeeper rejected the app"; fi BIN="$APP/Contents/MacOS/$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$APP/Contents/Info.plist" 2>/dev/null)" ARCHS=$(lipo -archs "$BIN" 2>/dev/null) if printf '%s' "$ARCHS" | grep -q x86_64 && printf '%s' "$ARCHS" | grep -q arm64; then ok "universal binary: $ARCHS" else no "not a universal binary (got: ${ARCHS:-unknown})"; fi MINOS=$(otool -l "$BIN" 2>/dev/null | awk '/minos/{if(!f){print $2;f=1}}') if [ "$MINOS" = "$WANT_MINOS" ]; then ok "deployment target is macOS $MINOS" else no "deployment target is ${MINOS:-unknown}, manifest says $WANT_MINOS"; fi SPK="$APP/Contents/Frameworks/Sparkle.framework" if [ -d "$SPK" ]; then SV=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$SPK/Resources/Info.plist" 2>/dev/null) if [ "$SV" = "$WANT_SPARKLE" ]; then ok "Sparkle $SV is embedded" else no "Sparkle version is ${SV:-unknown}, manifest says $WANT_SPARKLE"; fi if codesign --verify --deep --strict "$SPK" >/dev/null 2>&1; then ok "Sparkle framework and its XPC services are correctly signed" else no "Sparkle framework fails deep/strict verification"; fi else no "Sparkle framework not found" fi fi fi # ------------------------------------------------------------- honest scope --- head1 "What this did not check" cat <<'SCOPE' The sample is a packaging and signing proof, not a live service. SUFeedURL = https://hawserkit.com/focustimer-appcast.xml PolarOrganizationID = REPLACE_ME The feed is real and answers with one EdDSA-signed item for this same version, so Sparkle finds a valid signed feed and correctly reports there is nothing newer -- which is not the same as watching an update install. The licence id is a placeholder, so the checks above say nothing about activation, offline grace, refunds or revocation. Those run against your own organization once you fill that value in. If someone tells you this download proves them, they are overselling it -- including us. SCOPE head1 "Result" if [ "$fail" -eq 0 ]; then printf ' \033[32m%s checks passed, 0 failed.\033[0m\n\n' "$pass"; exit 0 else printf ' \033[31m%s passed, %s FAILED.\033[0m Do not trust this download.\n\n' "$pass" "$fail"; exit 1 fi