From 65ca1b97cfd7b5e285151e87f947ad11f00818c7 Mon Sep 17 00:00:00 2001 From: proxyctl coordinator Date: Fri, 17 Jul 2026 10:07:46 +0800 Subject: [PATCH] fix: harden release validation --- .github/workflows/ci.yml | 4 ++-- scripts/build-release.sh | 2 +- scripts/test-hermetic.sh | 28 +++++++++++++++++-------- scripts/test-release.sh | 37 +++++++++++++++++++++++++++++++++ scripts/verify-clean-machine.sh | 30 ++++++++++++++++++-------- 5 files changed, 80 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74377fc..f7e25f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,8 @@ jobs: test: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: go.mod cache: false diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 405b4e0..83b661e 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -21,7 +21,7 @@ build() { output="$out/proxyctl-linux-$arch" ( cd "$repo" - CGO_ENABLED=0 GOOS=linux GOARCH="$arch" \ + GOENV=off GOFLAGS= CGO_ENABLED=0 GOOS=linux GOARCH="$arch" \ go build -mod=readonly -buildvcs=false -trimpath \ -ldflags="-s -w -X main.version=$VERSION" \ -o "$output" ./cmd/proxyctl diff --git a/scripts/test-hermetic.sh b/scripts/test-hermetic.sh index 2c66b83..e238599 100755 --- a/scripts/test-hermetic.sh +++ b/scripts/test-hermetic.sh @@ -5,21 +5,31 @@ repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) work=$(mktemp -d) trap 'rm -rf "$work"' EXIT HUP INT TERM +snapshot_path() { + path=$1 + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + printf 'absent\t%s\n' "$path" + return + fi + + # Keep traversal, metadata collection, hashing, and sorting as separate + # commands so set -e observes every failure without relying on pipefail. + find -H "$path" -xdev \ + -exec stat --printf='metadata\t%F\t%a\t%u\t%g\t%N\n' -- {} + \ + >"$work/snapshot-metadata" + LC_ALL=C sort "$work/snapshot-metadata" + find -H "$path" -xdev -type f -exec sha256sum -- {} + \ + >"$work/snapshot-content" + LC_ALL=C sort "$work/snapshot-content" +} + snapshot() { for path in \ "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.docker/config.json" \ /etc/apt/apt.conf.d /etc/docker/daemon.json /var/lib/proxyctl \ /etc/systemd/system/docker.service.d do - if [ -d "$path" ]; then - find "$path" -type f -print0 2>/dev/null | sort -z | xargs -0 -r sha256sum 2>/dev/null || ls -ld "$path" - elif [ -r "$path" ]; then - sha256sum "$path" - elif [ -e "$path" ]; then - ls -ld "$path" - else - printf 'absent %s\n' "$path" - fi + snapshot_path "$path" done if command -v gsettings >/dev/null 2>&1; then gsettings list-recursively org.gnome.system.proxy 2>&1 || true diff --git a/scripts/test-release.sh b/scripts/test-release.sh index c68a67a..c3d2e5e 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -5,15 +5,52 @@ repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) work=$(mktemp -d) trap 'rm -rf "$work"' EXIT HUP INT TERM +assert_no_process_test_seam() { + artifact=$1 + if LC_ALL=C grep -a -F \ + -e 'proxyctl_process_test' \ + -e 'PROXYCTL_PROCESS_TEST_HOME' \ + "$artifact" >/dev/null + then + printf 'release artifact contains the process-test seam: %s\n' "$artifact" >&2 + return 1 + fi +} + VERSION=v1.0.0 "$repo/scripts/build-release.sh" "$work/first" VERSION=v1.0.0 "$repo/scripts/build-release.sh" "$work/second" +GOFLAGS='-tags=proxyctl_process_test' \ + VERSION=v1.0.0 "$repo/scripts/build-release.sh" "$work/malicious-goflags" +printf 'GOFLAGS=-tags=proxyctl_process_test\n' >"$work/malicious-goenv" +GOENV="$work/malicious-goenv" \ + VERSION=v1.0.0 "$repo/scripts/build-release.sh" "$work/malicious-goenv-build" for arch in amd64 arm64; do binary="proxyctl-linux-$arch" test -x "$work/first/$binary" cmp "$work/first/$binary" "$work/second/$binary" + cmp "$work/first/$binary" "$work/malicious-goflags/$binary" + cmp "$work/first/$binary" "$work/malicious-goenv-build/$binary" + assert_no_process_test_seam "$work/first/$binary" + assert_no_process_test_seam "$work/malicious-goflags/$binary" + assert_no_process_test_seam "$work/malicious-goenv-build/$binary" done cmp "$work/first/SHA256SUMS" "$work/second/SHA256SUMS" +cmp "$work/first/SHA256SUMS" "$work/malicious-goflags/SHA256SUMS" +cmp "$work/first/SHA256SUMS" "$work/malicious-goenv-build/SHA256SUMS" + +( + cd "$repo" + GOENV=off GOFLAGS= CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -mod=readonly -buildvcs=false -trimpath \ + -tags=proxyctl_process_test -o "$work/known-contaminated" ./cmd/proxyctl +) +if assert_no_process_test_seam "$work/known-contaminated"; then + echo 'release artifact validation accepted a known contaminated binary' >&2 + exit 1 +fi +printf 'process-test seam scan rejected a known contaminated binary\n' + ( cd "$work/first" sha256sum -c SHA256SUMS diff --git a/scripts/verify-clean-machine.sh b/scripts/verify-clean-machine.sh index d5cd30d..998a5ba 100755 --- a/scripts/verify-clean-machine.sh +++ b/scripts/verify-clean-machine.sh @@ -10,19 +10,31 @@ state="$work/state" bin="$work/bin" mkdir -p "$home" "$config" "$state" "$bin" +snapshot_host_path() { + path=$1 + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + printf 'absent\t%s\n' "$path" + return + fi + + # Avoid pipelines: traversal, stat, hash, and sort failures must stop the + # verification rather than being mistaken for an unchanged host snapshot. + find -H "$path" -xdev \ + -exec stat --printf='metadata\t%F\t%a\t%u\t%g\t%N\n' -- {} + \ + >"$work/host-snapshot-metadata" + LC_ALL=C sort "$work/host-snapshot-metadata" + find -H "$path" -xdev -type f -exec sha256sum -- {} + \ + >"$work/host-snapshot-content" + LC_ALL=C sort "$work/host-snapshot-content" +} + snapshot_host() { for path in \ "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.docker/config.json" \ - /etc/apt/apt.conf.d/99proxyctl /etc/docker/daemon.json \ - /var/lib/proxyctl/docker-daemon.json + /etc/apt/apt.conf.d /etc/docker/daemon.json /var/lib/proxyctl \ + /etc/systemd/system/docker.service.d do - if [ -r "$path" ]; then - sha256sum "$path" - elif [ -e "$path" ]; then - ls -ld "$path" - else - printf 'absent %s\n' "$path" - fi + snapshot_host_path "$path" done if command -v gsettings >/dev/null 2>&1; then gsettings get org.gnome.system.proxy mode 2>&1 || true