From 373184d47cdd6f8d62b3326fac6e4ee9667ba419 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:22:28 +0800 Subject: [PATCH] Preserve producer archive identity through release report conversion --- .github/workflows/publish-current-server.yml | 8 ++++ docs/releases/server-1.6.550.md | 13 ++++++- docs/releases/server-1.6.551.md | 39 ++++++++++++++++++++ scripts/test-publisher-safety-guards.py | 26 +++++++++++++ server/Dockerfile.web-compose-release | 2 +- server/build-api-explorer-patch-image.sh | 4 +- 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 docs/releases/server-1.6.551.md diff --git a/.github/workflows/publish-current-server.yml b/.github/workflows/publish-current-server.yml index b1a683f6b7..5ff3c9f902 100644 --- a/.github/workflows/publish-current-server.yml +++ b/.github/workflows/publish-current-server.yml @@ -731,6 +731,14 @@ jobs: gzip -n -c "$evidence/web-console-runtime-vulnerabilities.tsv" \ >"$evidence/web-console-runtime-vulnerabilities.tsv.gz" test -s "$evidence/web-console-runtime-vulnerabilities.tsv.gz" + # Recheck immutable producer bytes after report conversion. A valid + # gzip stream is not proof of a valid host package (an empty gzip is + # valid too). Never transform or re-compress this producer archive. + host_archive_sha="$(sed -n 's/^ARG HOST_API_ARCHIVE_SHA256=//p' \ + source/server/Dockerfile.web-compose-release | sort -u)" + [[ "$host_archive_sha" =~ ^[0-9a-f]{64}$ ]] + printf '%s %s\n' "$host_archive_sha" \ + "$evidence/host-api-${HOST_API_VERSION}.tar.gz" | sha256sum -c - ( cd "$evidence" sha256sum \ diff --git a/docs/releases/server-1.6.550.md b/docs/releases/server-1.6.550.md index 5479fd6256..71b2d3251a 100644 --- a/docs/releases/server-1.6.550.md +++ b/docs/releases/server-1.6.550.md @@ -1,5 +1,13 @@ # Server v1.6.550 +**Standalone attachment correction:** The manual completion script mistakenly +replaced `host-api-0.38.8.tar.gz` with an empty gzip stream. Do not use that +attachment. The Server image contains the correct archive, and the +[original Host API producer release](https://github.com/PastureStack/host-api/releases/tag/v0.38.8) +is unchanged. Matching upload checksums did not detect the substitution. The +original immutable bytes remain available as failure evidence; 551 adds a +producer-identity recheck after report conversion and publishes a fresh release. + Published image: `ghcr.io/pasturestack/server:v1.6.550@sha256:03542028c3ce4be11d7ff87f96beddf5179c67caf9f0d28ad57043acf3cf52c5`. Producer: `54deb98ee33ffab78437070d61179b602844182e`. @@ -49,8 +57,9 @@ were not changed. [Run 40116218929](https://github.com/PastureStack/server/actions/runs/40116218929) remains failed because the final renderer required a missing candidate-note heading. Its tested, signed image was not rebuilt or overwritten. Original CI -bytes completed publication, all 34 immutable assets passed anonymous SHA-256 -readback, and both attestations were verified. The +bytes completed image publication and both attestations were verified. The +34 assets passed upload-to-download checksum readback, which did not establish +that the Host API attachment still matched its producer; see the correction above. The [completion receipt](https://github.com/PastureStack/server/releases/download/v1.6.550/publication-recovery.json) records the original archive and note-only correction. A permanent pre-build test now validates the actual current note as well as fixtures. diff --git a/docs/releases/server-1.6.551.md b/docs/releases/server-1.6.551.md new file mode 100644 index 0000000000..616da968a3 --- /dev/null +++ b/docs/releases/server-1.6.551.md @@ -0,0 +1,39 @@ +# Server v1.6.551 candidate + +Not published. + +## Verified component inputs + +This release retains the PCRE2 security backports and all component inputs from +550, including Web Console 1.6.195, Engine v0.183.354 and Host API 0.38.8. +No application behavior or database migration is introduced. + +The manual completion of 550 accidentally replaced its standalone Host API +Release attachment with an empty gzip stream while converting an empty browser +findings table. The image itself contains the correct producer archive. Do not +use the 550 Host API attachment; the original official Host API 0.38.8 release +remains valid. The faulty immutable attachment is retained as evidence. + +The normal publisher now checks the exact producer archive SHA-256 again after +report conversion and before upload. Its regression test rejects an empty gzip +substitution even though that stream is syntactically valid. All downloadable +archive bytes must match the producer pin; a self-consistent checksum manifest +alone does not establish that identity. + +## Acceptance and remaining risk + +Only the release-asset guards and version metadata changed. Original unaffected +PCRE2 native, consumer, UI and authorization evidence is retained. Normal release +source, image/security, startup/restart and exact attachment checks still apply. +8080 deployment and attachment readback are separate acceptance steps. No +production site, HAProxy, firewall or production database is modified. + +The prior scan retains 13 pending LOW/MEDIUM occurrences across 10 CVEs. Other +product candidates and the Ruby/Onigmo build-host coverage gap remain tracked; +this release does not claim they have been remediated. + +## Upgrade and rollback + +Use the new numeric tag after publication and preserve existing configuration, +named volumes, restart policy and security options. Keep the prior image and +matching backup for rollback. Do not patch running containers or overwrite tags. diff --git a/scripts/test-publisher-safety-guards.py b/scripts/test-publisher-safety-guards.py index f3cd736f2c..181dd72f26 100644 --- a/scripts/test-publisher-safety-guards.py +++ b/scripts/test-publisher-safety-guards.py @@ -29,6 +29,9 @@ def test_browser_report_upload_is_nonempty_lossless_and_keeps_internal_gate_byte evidence = Path(temporary) internal = evidence / 'web-console-runtime-vulnerabilities.tsv' internal.write_bytes(report) + archive = evidence / 'host-api-0.38.8.tar.gz' + producer_bytes = gzip.compress(b'unchanged producer package', mtime=0) + archive.write_bytes(producer_bytes) result = subprocess.run(['bash', '-c', 'set -euo pipefail; evidence="$1"; ' + command, 'upload-fixture', temporary], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) @@ -37,6 +40,29 @@ def test_browser_report_upload_is_nonempty_lossless_and_keeps_internal_gate_byte self.assertEqual(gzip.decompress(public.read_bytes()), report) self.assertEqual(gzip.decompress(gzip.compress(report, mtime=0)), report) self.assertEqual(internal.read_bytes(), report) + self.assertEqual(archive.read_bytes(), producer_bytes) + + def test_empty_gzip_cannot_replace_the_producer_host_package(self): + source = WORKFLOW.read_text() + release = source.split(' - name: Create immutable GitHub Release', 1)[1] + start = release.index(' host_archive_sha=') + end = release.index('\n (', start) + guard = textwrap.dedent(release[start:end]) + self.assertLess(start, release.index(' gh release create')) + import hashlib + for replace in (False, True): + with self.subTest(empty_gzip=replace), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / 'source/server').mkdir(parents=True) + original = gzip.compress(b'unchanged producer package', mtime=0) + (root / 'source/server/Dockerfile.web-compose-release').write_text( + 'ARG HOST_API_ARCHIVE_SHA256=' + hashlib.sha256(original).hexdigest() + '\n') + archive = root / 'host-api-0.38.8.tar.gz' + archive.write_bytes(gzip.compress(b'', mtime=0) if replace else original) + result = subprocess.run(['bash', '-c', + 'set -euo pipefail; evidence="$1"; HOST_API_VERSION=0.38.8; ' + guard, + 'asset-fixture', temporary], cwd=temporary, capture_output=True, text=True) + self.assertEqual(result.returncode, int(replace), result.stderr) def guard(self, prefix): lines = [line.strip() for line in WORKFLOW.read_text().splitlines() diff --git a/server/Dockerfile.web-compose-release b/server/Dockerfile.web-compose-release index 1b191db590..3521a21089 100644 --- a/server/Dockerfile.web-compose-release +++ b/server/Dockerfile.web-compose-release @@ -691,7 +691,7 @@ ARG PER_HOST_SUBNET_VERSION=0.2.5 ARG PER_HOST_SUBNET_PRODUCER_COMMIT=e102e0bb38d84b0a9cbf87e5458cf1bc74d01904 ARG PER_HOST_SUBNET_ARCHIVE_SHA256=7237e83e96a64e7f3de64eb86bff9721c4e879c35cda9e9de1e4cb1ba225f298 ARG PER_HOST_SUBNET_PROVENANCE_SHA256=afa2db0e0755c6bd3ee28e111937c521a6d84e6d71d563e99202cf4693000eaf -ARG SERVER_RELEASE_TAG=v1.6.550 +ARG SERVER_RELEASE_TAG=v1.6.551 ARG API_EXPLORER_RELEASE_TAG=v1.1.20 ARG API_EXPLORER_ARTIFACT_SHA256=c989ee12a518f6d9d2b23b1f17f48d781aacf6ec31fb65291dd7de9b1661badb ARG API_EXPLORER_COMMIT=a586f0dc0aa50df579049a8d4bf26f04400fdf75 diff --git a/server/build-api-explorer-patch-image.sh b/server/build-api-explorer-patch-image.sh index f9608ee2b4..ad4c6278bc 100755 --- a/server/build-api-explorer-patch-image.sh +++ b/server/build-api-explorer-patch-image.sh @@ -14,7 +14,7 @@ if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then fi revision=${PASTURESTACK_SERVER_REVISION:-$(git rev-parse HEAD)} -server_release_tag=${SERVER_RELEASE_TAG:-v1.6.550} +server_release_tag=${SERVER_RELEASE_TAG:-v1.6.551} source_date_epoch=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)} base_image=${BASE_IMAGE:-ghcr.io/pasturestack/server:v1.6.460@sha256:c855af8aea232dacc5bb6df68e2271d482c68b53c43ab0c108ec19118f5ab403} orchestration_engine_release_base_url=${ORCHESTRATION_ENGINE_RELEASE_BASE_URL:-https://github.com/PastureStack/orchestration-engine/releases/download} @@ -130,7 +130,7 @@ usage_telemetry_agent_archive_sha256=${USAGE_TELEMETRY_AGENT_ARCHIVE_SHA256:-6d4 usage_telemetry_agent_binary_sha256=${USAGE_TELEMETRY_AGENT_BINARY_SHA256:-90ed65f689c91801abd0cf482211c96a01e7c63ef695e4faecdf4713eff730be} supported_docker_range='~v1.12.3 || ~v1.13.0 || ~v17.03.0 || ~v17.06.0 || ~v17.09.0 || ~v17.12.0 || ~v18.03.0 || ~v18.06.0 || ~v18.09.0 || ~v19.03.2 || v24.0.9 || >=v29.4.1 <=v29.7.2 || v29.8.0' newest_docker_version=v29.8.0 -image=${IMAGE:-pasturestack-validation/server:v1.6.550} +image=${IMAGE:-pasturestack-validation/server:v1.6.551} build_options=() [[ "$revision" =~ ^[0-9a-f]{40}$ ]]