Complete release and helper download safeguards

This commit is contained in:
pulse-triage[bot]
2026-08-30 00:41:58 +01:00
parent 718de25b2b
commit ac2a2fe020
6 changed files with 55 additions and 5 deletions
+5 -3
View File
@@ -74,9 +74,11 @@ Normal stable publication and stable dry runs select `signpath` directly.
- Release checksums and detached signatures are published alongside artifacts
and verified independently after publication.
- Release activation requires GitHub CLI 2.97.0 or newer, which includes the
literal signer-identity matcher fix. The published checksum manifest must
carry build provenance from the exact `create-release.yml` workflow and
release source commit; repository-level provenance is not sufficient.
literal signer-identity matcher fix. The shared
`scripts/require-safe-gh-attestation.sh` guard enforces this floor. The
published checksum manifest must carry build provenance from the exact
`create-release.yml` workflow and release source commit; repository-level
provenance is not sufficient.
- Every new release is assembled and validated as a draft. Its activation
marker is uploaded and digest-checked before publication; GitHub must then
report the published release as immutable, protecting its tag and complete
@@ -4522,3 +4522,10 @@ the customer-promotion lease or mutate a floating image tag, Helm index,
paid-runtime pointer, or live environment until the check passes. Repository
release immutability must therefore be enabled before merging or running this
activation path.
Attestation policy decisions require GitHub CLI 2.97.0 or newer so signer
repository and workflow names are matched literally. The shared verifier must
also bind the downloaded `checksums.txt` bytes to the immutable release and to
SLSA v1 provenance from the exact `create-release.yml` workflow and expected
source SHA. Multi-asset download retries clear both the activation marker and
checksum manifest first so a partial attempt cannot poison every later retry.
+17
View File
@@ -10253,6 +10253,23 @@ func TestContract_AgentHelperDownloadIsLinuxOnlyAndSeparatelySigned(t *testing.T
if response.Code != http.StatusBadRequest {
t.Fatalf("non-Linux helper status = %d, want %d", response.Code, http.StatusBadRequest)
}
// The installer fetches this artifact before it has an API credential, so
// the full router must preserve the documented public download boundary.
cfg := newTestConfigWithTokens(t)
cfg.AuthUser = "admin"
cfg.AuthPass = "hashed"
publicRouter := NewRouter(cfg, nil, nil, nil, nil, "v6.0.0")
request = httptest.NewRequest(
http.MethodPost,
"/download/pulse-agent-helper?arch=linux-amd64",
nil,
)
response = httptest.NewRecorder()
publicRouter.Handler().ServeHTTP(response, request)
if response.Code != http.StatusMethodNotAllowed {
t.Fatalf("public helper download status = %d, want %d", response.Code, http.StatusMethodNotAllowed)
}
}
func TestContract_AgentRunnerDownloadIsLinuxOnlyAndSeparatelySigned(t *testing.T) {
+1
View File
@@ -4742,6 +4742,7 @@ func TestPublicDownloadEndpointsBypassAuth(t *testing.T) {
"/install.sh",
"/install.ps1",
"/download/pulse-agent",
"/download/pulse-agent-helper?arch=linux-amd64",
}
for idx, path := range paths {
@@ -25,6 +25,7 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
asset_verification_succeeds: bool = True,
provenance_verification_succeeds: bool = True,
gh_version: str = "2.97.0",
partial_download_once: bool = False,
):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
@@ -54,6 +55,14 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
while [ "$#" -gt 0 ]; do
if [ "$1" = --dir ]; then
mkdir -p "$2"
if [ "$PARTIAL_DOWNLOAD_ONCE" = true ] && [ ! -e "$DOWNLOAD_STATE" ]; then
touch "$DOWNLOAD_STATE"
printf '%s\\n' 'abc pulse-v6.5.0-linux-amd64.tar.gz' > "$2/checksums.txt"
exit 1
fi
if [ -e "$2/release-activation.json" ] || [ -e "$2/checksums.txt" ]; then
exit 66
fi
printf '%s\\n' '{{"schema_version": 1}}' > "$2/release-activation.json"
printf '%s\\n' 'abc pulse-v6.5.0-linux-amd64.tar.gz' > "$2/checksums.txt"
exit 0
@@ -79,9 +88,13 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
env.update(
{
"PATH": f"{root}:{env['PATH']}",
"PULSE_RELEASE_ATTESTATION_ATTEMPTS": "1",
"PULSE_RELEASE_ATTESTATION_ATTEMPTS": (
"2" if partial_download_once else "1"
),
"PULSE_RELEASE_ATTESTATION_RETRY_DELAY": "0",
"GH_VERSION": gh_version,
"PARTIAL_DOWNLOAD_ONCE": str(partial_download_once).lower(),
"DOWNLOAD_STATE": str(root / "download-state"),
}
)
result = subprocess.run(
@@ -166,6 +179,13 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
self.assertIn("checksum manifest build provenance verification failed", result.stderr)
self.assertIn("attestation verify", calls)
def test_recovers_from_a_partial_multi_asset_download(self) -> None:
result, calls = self.run_verifier(
self.release(), partial_download_once=True
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls.count("release download"), 2)
def test_rejects_an_unsafe_github_cli_before_release_lookup(self) -> None:
result, calls = self.run_verifier(self.release(), gh_version="2.96.1")
self.assertNotEqual(result.returncode, 0)
+4 -1
View File
@@ -124,7 +124,10 @@ fi
# release attestation rather than trusting filename and JSON identity alone.
downloaded=false
for attempt in $(seq 1 "$ATTESTATION_ATTEMPTS"); do
rm -f "$activation_asset"
# A previous attempt can leave either asset behind after a partial
# download. Clear both because gh release download refuses to overwrite
# existing files unless explicitly told to do so.
rm -f "$activation_asset" "$checksums_asset"
if gh release download "$TAG" \
--repo "$REPO" \
--pattern release-activation.json \