From 240adaa5e4258cb3c198276e521fe4086a1bccc8 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:50:48 +0100 Subject: [PATCH] Authenticate every published installer --- .../subsystems/deployment-installability.md | 9 +++++++- .../installtests/build_release_assets_test.go | 11 +++++++++- scripts/release_asset_common.sh | 3 +++ .../tests/test_validate_published_release.py | 22 +++++++++++++++++++ scripts/validate-published-release.sh | 22 +++++++++++++++++++ scripts/validate-release.sh | 14 ++++++++++++ 6 files changed, 79 insertions(+), 2 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b0629eaa8c..9edf4eaaf2 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -3429,6 +3429,12 @@ installers consume. macOS notarization is intentionally skipped for v1: the README documents the Gatekeeper bypass and the install-script flow downloads the same unsigned binary, with the audit trail of signed-manifest and SHA256 verification preserved. +Because the shell installer is itself an executable bootstrap trust boundary, +release assembly must include `install-mcp.sh` in the authenticated checksum +manifest and emit its checksum and detached-signature sidecars before upload; +validating only the binary that an unauthenticated installer chooses is not a +complete install chain. Local and post-publication validation require every +published installer entry exactly once in that signed manifest. The adapter's complete request/response tool-list projection, manifest projection, capability and governance metadata formatting, request/response tool filtering, typed input-schema projection, and API route/body call @@ -4416,7 +4422,8 @@ release packet, and make post-publication validation authenticate `checksums.txt` and every listed artifact's `.sshsig` against the configured `PULSE_UPDATE_SIGNING_PUBLIC_KEY`, not merely test that sidecars are present. Validation must fail if the trust root is unavailable, if any signature is -invalid, if any published artifact or `checksums.txt` is missing its +invalid, if any published installer is absent from the authenticated checksum +manifest, if any published artifact or `checksums.txt` is missing its `.sshsig` sidecar, if the authenticated checksum manifest contains duplicate asset filenames or trailing fields, or if the canonical release-packet SBOM is absent so published RC/stable downloads can keep the diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 5950afc924..adbbf7ad87 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -1363,6 +1363,9 @@ func TestReleaseValidationRequiresSignedSidecars(t *testing.T) { } localValidator := string(localValidatorBytes) localRequired := []string{ + `"install-mcp.sh"`, + `for installer in install.sh install-docker.sh install-mcp.sh install-mcp.ps1 install.ps1 pulse-auto-update.sh; do`, + `checksums.txt must contain exactly one entry for release installer ${installer}`, `"pulse-v${PULSE_VERSION}-release.sbom.spdx.json"`, `release_sbom="pulse-${PULSE_TAG}-release.sbom.spdx.json"`, `error "checksums.txt is missing ${release_sbom}"`, @@ -1460,6 +1463,9 @@ func TestReleaseValidationRequiresSignedSidecars(t *testing.T) { } publishedValidator := string(publishedValidatorBytes) publishedRequired := []string{ + `REQUIRED_SIGNED_INSTALLERS=(`, + `install-mcp.sh`, + `Authenticated checksums.txt must contain exactly one valid entry for published installer ${installer}.`, `RELEASE_SBOM="pulse-${TAG}-release.sbom.spdx.json"`, `PULSE_UPDATE_SIGNING_PUBLIC_KEY is required to authenticate published release assets.`, `go -C "$REPO_ROOT" run ./scripts/release_update_key.go public-key-ssh`, @@ -1503,6 +1509,7 @@ func TestReleaseValidationRequiresSignedSidecars(t *testing.T) { "make post-publication validation authenticate", "every listed artifact's `.sshsig` against the configured", "Validation must fail if the trust root is unavailable", + "any published installer is absent from the authenticated checksum", "release-packet SBOM is absent", "download endpoints must return checksum and signature headers", "must disable Go's automatic VCS stamping", @@ -2805,6 +2812,7 @@ func TestBuildReleasePackagesPulseMcpForAllPlatforms(t *testing.T) { `checksum_files+=( pulse-mcp-linux-* )`, `checksum_files+=( pulse-mcp-darwin-* )`, `checksum_files+=( pulse-mcp-freebsd-* )`, + `checksum_files+=( install-mcp.sh )`, } { if !strings.Contains(string(commonContent), needle) { t.Fatalf("release checksum collection missing bare MCP assets: %s", needle) @@ -2816,6 +2824,7 @@ func TestBuildReleasePackagesPulseMcpForAllPlatforms(t *testing.T) { "pulse-mcp-linux-amd64", "pulse-mcp-darwin-arm64", "pulse-mcp-freebsd-amd64", + "install-mcp.sh", } { if err := os.WriteFile(filepath.Join(releaseDir, asset), []byte(asset), 0o755); err != nil { t.Fatalf("write MCP checksum fixture: %v", err) @@ -2826,7 +2835,7 @@ func TestBuildReleasePackagesPulseMcpForAllPlatforms(t *testing.T) { if err != nil { t.Fatalf("collect MCP release checksum files: %v\n%s", err, checksumOutput) } - for _, asset := range []string{"pulse-mcp-linux-amd64", "pulse-mcp-darwin-arm64", "pulse-mcp-freebsd-amd64"} { + for _, asset := range []string{"pulse-mcp-linux-amd64", "pulse-mcp-darwin-arm64", "pulse-mcp-freebsd-amd64", "install-mcp.sh"} { if !strings.Contains(string(checksumOutput), asset) { t.Fatalf("bare MCP release asset %s missing from checksum/signature input:\n%s", asset, checksumOutput) } diff --git a/scripts/release_asset_common.sh b/scripts/release_asset_common.sh index dff43526ac..e73356c12a 100644 --- a/scripts/release_asset_common.sh +++ b/scripts/release_asset_common.sh @@ -287,6 +287,9 @@ pulse_release_collect_checksum_files() { if [[ -f "install-docker.sh" ]]; then checksum_files+=( install-docker.sh ) fi + if [[ -f "install-mcp.sh" ]]; then + checksum_files+=( install-mcp.sh ) + fi if [[ -f "pulse-auto-update.sh" ]]; then checksum_files+=( pulse-auto-update.sh ) fi diff --git a/scripts/tests/test_validate_published_release.py b/scripts/tests/test_validate_published_release.py index a651d180f6..0a35e57ab8 100644 --- a/scripts/tests/test_validate_published_release.py +++ b/scripts/tests/test_validate_published_release.py @@ -47,7 +47,12 @@ class ValidatePublishedReleaseTest(unittest.TestCase): payloads = { SBOM: b'{"spdxVersion":"SPDX-2.3"}\n', + "install-docker.sh": b"#!/bin/sh\necho docker installer\n", + "install-mcp.sh": b"#!/bin/sh\necho mcp installer\n", + "install-mcp.ps1": b"Write-Output 'mcp installer'\r\n", + "install.ps1": b"Write-Output 'agent installer'\r\n", "install.sh": b"#!/bin/sh\necho pulse\n", + "pulse-auto-update.sh": b"#!/bin/sh\necho update\n", } checksum_lines: list[str] = [] for name, content in payloads.items(): @@ -154,6 +159,23 @@ printf '%s\n' "${FAKE_SSH_PUBLIC_KEY}" self.assertNotEqual(result.returncode, 0) self.assertIn("SSH signature verification failed for install.sh", result.stderr) + def test_rejects_unsigned_published_mcp_installer(self) -> None: + checksums = (self.assets / "checksums.txt").read_text(encoding="utf-8") + filtered = "".join( + line + for line in checksums.splitlines(keepends=True) + if not line.endswith(" install-mcp.sh\n") + ) + self.replace_signed_checksums(filtered) + + result = self.run_validator() + + self.assertNotEqual(result.returncode, 0) + self.assertIn( + "Authenticated checksums.txt must contain exactly one valid entry for published installer install-mcp.sh.", + result.stderr, + ) + def test_rejects_forged_checksum_manifest_before_using_it(self) -> None: with (self.assets / "checksums.txt").open("a", encoding="utf-8") as handle: handle.write(f"{'0' * 64} attacker-payload\n") diff --git a/scripts/validate-published-release.sh b/scripts/validate-published-release.sh index 34cc36d1d1..002cba299b 100755 --- a/scripts/validate-published-release.sh +++ b/scripts/validate-published-release.sh @@ -85,6 +85,21 @@ if ! verify_signature "$CHECKSUMS_PATH" "$CHECKSUMS_SIG_PATH" "checksums.txt for exit 1 fi +REQUIRED_SIGNED_INSTALLERS=( + install.sh + install-docker.sh + install-mcp.sh + install-mcp.ps1 + install.ps1 + pulse-auto-update.sh +) +for installer in "${REQUIRED_SIGNED_INSTALLERS[@]}"; do + if ! awk -v name="$installer" '$2 == name { found = 1 } END { exit !found }' "$CHECKSUMS_PATH"; then + echo "Authenticated checksums.txt must contain exactly one valid entry for published installer ${installer}." >&2 + exit 1 + fi +done + RELEASE_SBOM="pulse-${TAG}-release.sbom.spdx.json" if ! awk '{print $2}' "$CHECKSUMS_PATH" | grep -Fx "$RELEASE_SBOM" >/dev/null 2>&1; then echo "checksums.txt does not list ${RELEASE_SBOM} for ${TAG}" >&2 @@ -191,4 +206,11 @@ if [[ "$status" -ne 0 ]]; then exit 1 fi +for installer in "${REQUIRED_SIGNED_INSTALLERS[@]}"; do + if ! grep -Fxq -- "$installer" "$SEEN_FILENAMES_PATH"; then + echo "Authenticated checksums.txt must contain exactly one valid entry for published installer ${installer}." >&2 + exit 1 + fi +done + echo "Published release assets for ${TAG} match authenticated checksums.txt, *.sha256 files, and verified *.sshsig sidecars." diff --git a/scripts/validate-release.sh b/scripts/validate-release.sh index 73a89254eb..af5ddb28a4 100755 --- a/scripts/validate-release.sh +++ b/scripts/validate-release.sh @@ -449,6 +449,11 @@ info "Checking required release assets..." # They are only included in Docker images for /download/ endpoints required_assets=( "install.sh" + "install-docker.sh" + "install-mcp.sh" + "install-mcp.ps1" + "install.ps1" + "pulse-auto-update.sh" "checksums.txt" "pulse-v${PULSE_VERSION}-release.sbom.spdx.json" "pulse-v${PULSE_VERSION}.tar.gz" @@ -490,6 +495,15 @@ if [ $missing_count -gt 0 ]; then fi success "All ${#required_assets[@]} required release assets present" +for installer in install.sh install-docker.sh install-mcp.sh install-mcp.ps1 install.ps1 pulse-auto-update.sh; do + manifest_matches="$(awk -v name="${installer}" '$2 == name && NF == 2 { count++ } END { print count + 0 }' checksums.txt)" + if [ "${manifest_matches}" -ne 1 ]; then + error "checksums.txt must contain exactly one entry for release installer ${installer}" + exit 1 + fi +done +success "All published release installers are uniquely listed in checksums.txt" + # Validate published install.sh is the Pulse SERVER installer. # Across v6 rc.1 → rc.5 the rendered AGENT installer was published here by # mistake, silently breaking the LXC quickstart, the in-product Update Pulse