Files
ShadowsocksX-NG/ShadowsocksX-NG2/Scripts/fetch-external-binaries.sh
T
Qiu Yuzhou a20df3001f feat(ng2): external binary supply chain and packaging gate (#24)
Pin sslocal v1.25.0 (aarch64-apple-darwin) and v2ray-plugin v1.3.2 via
committed manifests (tag + asset URL + archive SHA-256 + bundle location +
sign identifier):

- Scripts/fetch-external-binaries.sh downloads the pinned archive, verifies
  its SHA-256 byte-for-byte, extracts only the named member (arm64-only
  check) and stamps the local cache; archive hash mismatch fails hard, and
  any manifest/local-artifact drift forces a re-verified refetch. It re-runs
  as a pre-build phase so tampering fails the build itself.
- project.yml installs both binaries via Copy Files into
  Contents/Helpers[/Plugins]; generate requires the artifacts to exist
  (fetch before generate, else Copy Files phases are silently dropped).
- Scripts/sign-embedded-helpers.sh re-signs each helper Developer ID with
  --options runtime --timestamp and the pinned identifier
  (<bundle-id>[.plugin.]<name>) before Xcode seals the outer app signature.
- Scripts/packaging-gate.sh asserts strict nested signature verification,
  DevID + Hardened Runtime on the app with no App Sandbox entitlement, and
  exact per-binary manifest coverage under Contents/Helpers.
- Tests pin both manifests' schema and anchor hashes.

Verified: build + gate all green; byte-flip, foreign-binary and
missing-binary tampers each caught; 5 unit tests pass; no submodules or
in-repo builds; notarization stays out of scope (#39).
2026-09-20 14:27:21 +08:00

124 lines
4.3 KiB
Bash
Executable File

#!/bin/sh
# Fetch and hash-verify the external binaries pinned under Vendor/.
#
# For every Vendor/<name>/manifest.json (see lib-manifests.sh for the schema):
# 1. If the extracted binary is present and its recorded SHA-256 stamp still
# matches, nothing is downloaded (build cache hit).
# 2. Otherwise the pinned release asset is downloaded and its archive
# SHA-256 must equal manifest archiveSHA256 — a mismatch fails the build.
# 3. Only manifest archiveMember is extracted, must be an arm64-only Mach-O,
# and is installed as Vendor/<name>/<binary> with a fresh stamp.
#
# Trust root is the committed manifest (tag + asset URL + archive SHA-256);
# local artifacts are a cache and are silently refetched when they drift.
# Usage: Scripts/fetch-external-binaries.sh (run from anywhere)
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
. "$SCRIPT_DIR/lib-manifests.sh"
NG2_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
VENDOR_DIR="$NG2_ROOT/Vendor"
fetch_one() {
manifest=$1
name=$(basename "$(dirname "$manifest")")
tmproot=$(mktemp -d "${TMPDIR:-/tmp}/ng2-fetch.XXXXXX")
trap 'rm -rf "$tmproot"' EXIT INT TERM
plist="$tmproot/manifest.plist"
manifest_to_plist "$manifest" "$plist"
binary=$(manifest_field "$plist" binary "$manifest")
member=$(manifest_field "$plist" archiveMember "$manifest")
asset=$(manifest_field "$plist" asset "$manifest")
url=$(manifest_field "$plist" url "$manifest")
expected=$(manifest_field "$plist" archiveSHA256 "$manifest")
dir="$VENDOR_DIR/$name"
artifact="$dir/$binary"
stamp="$dir/.fetched.sha256"
printf '%s' "$expected" | grep -Eq '^[0-9a-f]{64}$' || {
echo "error: $manifest: archiveSHA256 must be 64 lowercase hex chars" >&2
return 1
}
if [ -f "$artifact" ] && [ -f "$stamp" ]; then
stamp_archive=$(awk '$1 == "archive" {print $2}' "$stamp")
stamp_binary=$(awk '$1 == "binary" {print $2}' "$stamp")
actual_binary=$(shasum -a 256 "$artifact" | awk '{print $1}')
if [ "$stamp_archive" = "$expected" ] && [ "$stamp_binary" = "$actual_binary" ]; then
echo "fetch($name): $binary up to date"
rm -rf "$tmproot"
trap - EXIT INT TERM
return 0
fi
echo "fetch($name): pinned manifest or local artifact changed; refetching release"
fi
echo "fetch($name): $url"
archive="$tmproot/$asset"
curl --fail --location --silent --show-error --retry 3 \
--connect-timeout 15 --output "$archive" "$url" || {
echo "error: $name: download failed: $url" >&2
return 1
}
actual=$(shasum -a 256 "$archive" | awk '{print $1}')
[ "$actual" = "$expected" ] || {
echo "error: $name: archive SHA-256 mismatch (expected $expected, got $actual)
error: $name: the pinned release asset changed; redo the static manifest before upgrading" >&2
return 1
}
if tar -tf "$archive" | grep -Eq '(^|/)\.\.(/|$)|^/'; then
echo "error: $name: archive contains unsafe member paths" >&2
return 1
fi
if ! tar -tf "$archive" | grep -Fqx "$member"; then
echo "error: $name: archive has no member '$member'" >&2
return 1
fi
xdir="$tmproot/extracted"
mkdir -p "$xdir"
tar -xf "$archive" -C "$xdir" "$member"
extracted="$xdir/$member"
[ -f "$extracted" ] && [ ! -L "$extracted" ] || {
echo "error: $name: extracted member is not a regular file: $member" >&2
return 1
}
lipo -archs "$extracted" >/dev/null 2>&1 || {
echo "error: $name: extracted member is not a Mach-O binary: $member" >&2
return 1
}
archs=$(lipo -archs "$extracted" | xargs)
[ "$archs" = "arm64" ] || {
echo "error: $name: expected arm64-only binary, got: $archs" >&2
return 1
}
mkdir -p "$dir"
cp "$extracted" "$artifact"
chmod 755 "$artifact"
actual_binary=$(shasum -a 256 "$artifact" | awk '{print $1}')
{
printf 'archive\t%s\n' "$expected"
printf 'binary\t%s\n' "$actual_binary"
} > "$stamp.tmp"
mv "$stamp.tmp" "$stamp"
echo "fetch($name): verified and installed $binary ($actual_binary)"
rm -rf "$tmproot"
trap - EXIT INT TERM
}
manifests=$(vendor_manifests) || exit 1
for manifest in $manifests; do
fetch_one "$manifest"
done