Files
ShadowsocksX-NG/ShadowsocksX-NG2/Scripts/packaging-gate.sh
T

231 lines
8.6 KiB
Bash
Executable File

#!/bin/sh
# Packaging gate for ShadowsocksX-NG2.
#
# Standalone assertion over a built app bundle; exits nonzero on any failure:
# 1. Nested code signatures pass strict verification (incl. every helper).
# 2. The main app and native tools are Developer ID signed with Hardened
# Runtime and secure timestamps. The app carries NO App Sandbox
# entitlement (and no network.server entitlement).
# 3. Every Vendor manifest binary is embedded at Contents/<bundleSubpath>,
# is arm64-only, and is re-signed by our Developer ID with Hardened
# Runtime, a secure timestamp and the pinned code-signing identifier
# (<bundle-id>[.plugin].<name>).
# 4. The manifest set exactly covers every Mach-O under Contents/Helpers.
#
# Usage: Scripts/packaging-gate.sh path/to/ShadowsocksX-NG2.app
# EXPECTED_TEAM_ID overrides the Team ID read from the local signing config.
set -u
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
. "$SCRIPT_DIR/lib-manifests.sh"
NG2_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
VENDOR_DIR="$NG2_ROOT/Vendor"
# Expected Team ID is injected, not pinned in the repo (open source). The same
# file the build reads supplies it here: Configs/Signing.local.xcconfig
# (gitignored). An explicit EXPECTED_TEAM_ID wins. Knowing no expected team is a
# hard failure: an unsigned or wrong-team bundle must not pass just because
# there is nothing to compare against.
LOCAL_SIGNING_CONFIG="$NG2_ROOT/Configs/Signing.local.xcconfig"
EXPECTED_TEAM_ID=${EXPECTED_TEAM_ID:-}
if [ -z "$EXPECTED_TEAM_ID" ] && [ -f "$LOCAL_SIGNING_CONFIG" ]; then
EXPECTED_TEAM_ID=$(
sed -n 's/^[[:space:]]*DEVELOPMENT_TEAM[[:space:]]*=[[:space:]]*\([^[:space:]]*\).*/\1/p' \
"$LOCAL_SIGNING_CONFIG" | tail -1
)
fi
APP=${1:-}
[ -n "$APP" ] || {
echo "usage: $0 <path/to/App.app>" >&2
exit 2
}
[ -d "$APP/Contents" ] || {
echo "gate: FAIL not an app bundle: $APP"
exit 1
}
CONTENTS="$APP/Contents"
FAILS=0
pass() { printf 'gate: PASS %s\n' "$1"; }
fail() { printf 'gate: FAIL %s\n' "$1"; FAILS=$((FAILS + 1)); }
dv_of() { codesign -dv --verbose=4 "$1" 2>&1; }
dv_value() {
# $1 = dv output, $2 = key. Handles both "Key=value" lines and fields
# embedded in the CodeDirectory line ("... flags=0x10000(runtime) ...").
printf '%s\n' "$1" |
sed -n -e "s/^$2=//p" -e "s/^CodeDirectory .* $2=\([^ )]*\).*/\1/p" |
head -1
}
check_devid_and_runtime() {
# $1 = label, $2 = dv output
authority=$(dv_value "$2" Authority)
case "$authority" in
"Developer ID Application:"*) pass "$1: Developer ID Application signed" ;;
*) fail "$1: Developer ID Application signed (got: ${authority:-none})" ;;
esac
flags=$(dv_value "$2" flags)
case "$flags" in
*runtime*) pass "$1: Hardened Runtime" ;;
*) fail "$1: Hardened Runtime (flags: ${flags:-none})" ;;
esac
if [ -n "$(dv_value "$2" Timestamp)" ]; then
pass "$1: secure timestamp present"
else
fail "$1: secure timestamp present"
fi
}
# --- 1. strict nested signature verification -------------------------------
if err=$(codesign --verify --strict --deep "$APP" 2>&1); then
pass "nested code signatures verify strictly"
else
fail "nested code signatures verify strictly: $err"
fi
# --- 2. main app identity, runtime, entitlements ----------------------------
bundle_id=$("$PLIST_BUDDY" -c 'Print :CFBundleIdentifier' "$CONTENTS/Info.plist" 2>/dev/null) || {
fail "read CFBundleIdentifier from Info.plist"
bundle_id=""
}
[ -n "$bundle_id" ] && pass "bundle identifier: $bundle_id"
app_dv=$(dv_of "$APP")
app_team=$(dv_value "$app_dv" TeamIdentifier)
app_authority=$(dv_value "$app_dv" Authority)
app_flags=$(dv_value "$app_dv" flags)
if [ -z "$EXPECTED_TEAM_ID" ]; then
fail "expected Team ID is known (set EXPECTED_TEAM_ID or write DEVELOPMENT_TEAM in Configs/Signing.local.xcconfig)"
else
[ "$app_team" = "$EXPECTED_TEAM_ID" ] ||
fail "main app TeamIdentifier is $EXPECTED_TEAM_ID (got: ${app_team:-none})"
fi
check_devid_and_runtime "main app" "$app_dv"
for binary in ShadowsocksX-NG2Agent ShadowsocksX-NG2SystemProxyHelper; do
target="$CONTENTS/MacOS/$binary"
if [ ! -f "$target" ]; then
fail "$binary: embedded in Contents/MacOS"
continue
fi
native_dv=$(dv_of "$target")
check_devid_and_runtime "$binary" "$native_dv"
native_team=$(dv_value "$native_dv" TeamIdentifier)
[ "$native_team" = "$app_team" ] ||
fail "$binary: TeamIdentifier matches the app (got: ${native_team:-none})"
done
case "$app_authority" in
"Developer ID Application:"*) ;;
*)
echo "gate: hint ad-hoc / non-Developer-ID artifacts come from the certificate-less Debug baseline; only a Developer ID signed release artifact can pass"
;;
esac
app_entitlements=$(codesign -d --entitlements - --xml "$APP" 2>/dev/null || true)
for forbidden in com.apple.security.app-sandbox com.apple.security.network.server; do
case "$app_entitlements" in
*"$forbidden"*) fail "main app has no $forbidden entitlement" ;;
*) pass "main app has no $forbidden entitlement" ;;
esac
done
# --- 3+4. per-binary manifest coverage --------------------------------------
if [ ! -d "$CONTENTS/Helpers" ]; then
fail "Contents/Helpers exists"
fi
tmproot=$(mktemp -d "${TMPDIR:-/tmp}/ng2-gate.XXXXXX")
trap 'rm -rf "$tmproot"' EXIT INT TERM
expected_list="$tmproot/expected.txt"
actual_list="$tmproot/actual.txt"
: > "$expected_list"
manifests=$(vendor_manifests) || { fail "read manifests"; manifests=""; }
for manifest in $manifests; do
name=$(basename "$(dirname "$manifest")")
plist="$tmproot/$name.plist"
manifest_to_plist "$manifest" "$plist" || { fail "read manifest $name"; continue; }
binary=$(manifest_field "$plist" binary "$manifest") || { fail "read manifest $name"; continue; }
subpath=$(manifest_field "$plist" bundleSubpath "$manifest") || { fail "read manifest $name"; continue; }
identifier=$(manifest_field "$plist" signIdentifier "$manifest") || { fail "read manifest $name"; continue; }
subpath_ok=1
case "$subpath" in
Helpers|Helpers/*) ;;
*) subpath_ok=0 ;;
esac
case "$subpath" in
""|/*|*..*) subpath_ok=0 ;;
esac
if [ "$subpath_ok" -eq 1 ]; then
pass "$name: bundleSubpath is under Helpers ($subpath)"
else
fail "$name: bundleSubpath must be Helpers or Helpers/... without '..' (got: '$subpath')"
fi
target="$CONTENTS/$subpath/$binary"
rel="${subpath#Contents/}/$binary"
printf '%s\n' "$rel" >> "$expected_list"
if [ ! -f "$target" ]; then
fail "$name: embedded at Contents/$rel"
continue
fi
[ -x "$target" ] || fail "$name: embedded binary is executable"
if err=$(codesign --verify --strict "$target" 2>&1); then
:
else
fail "$name: signature verifies strictly: $err"
fi
lipo -archs "$target" >/dev/null 2>&1 || { fail "$name: is an arm64 Mach-O binary"; continue; }
archs=$(lipo -archs "$target" | xargs)
[ "$archs" = "arm64" ] || fail "$name: is arm64-only (got: $archs)"
dv=$(dv_of "$target")
team=$(dv_value "$dv" TeamIdentifier)
actual_identifier=$(dv_value "$dv" Identifier)
check_devid_and_runtime "$name" "$dv"
[ "$team" = "$app_team" ] ||
fail "$name: TeamIdentifier matches the app (got: ${team:-none})"
[ "$actual_identifier" = "$identifier" ] ||
fail "$name: code-signing identifier is $identifier (got: ${actual_identifier:-none})"
case "$identifier" in
"$bundle_id".*) pass "$name: identifier is prefixed by the bundle id" ;;
*) fail "$name: identifier is prefixed by the bundle id ($identifier)" ;;
esac
done
# Every code file under Contents/Helpers must be manifest-covered, and vice
# versa; symlinks anywhere under Helpers are anomalous and fail outright.
if [ -d "$CONTENTS/Helpers" ]; then
if find "$CONTENTS/Helpers" -type l | grep -q .; then
fail "Contents/Helpers contains no symlinks"
fi
: > "$actual_list"
find "$CONTENTS/Helpers" -type f |
awk -v prefix="$CONTENTS/" '{print substr($0, length(prefix) + 1)}' |
sort > "$actual_list"
sort -o "$expected_list" "$expected_list"
if diff -u "$expected_list" "$actual_list" > "$tmproot/cover.diff" 2>&1; then
pass "manifest set covers every file under Contents/Helpers"
else
fail "manifest set covers every file under Contents/Helpers:
$(sed 's/^/ /' "$tmproot/cover.diff")"
fi
fi
# --- summary -----------------------------------------------------------------
if [ "$FAILS" -eq 0 ]; then
echo "gate: ALL CHECKS PASSED ($APP)"
exit 0
fi
echo "gate: $FAILS CHECK(S) FAILED ($APP)" >&2
exit 1