mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Publish portable candidate build provenance
This commit is contained in:
@@ -44,6 +44,12 @@ a check fails. Six-hour lock-watch evidence explicitly records its narrower
|
||||
`release_lock` mode and skipped full-surface checks. The job is read-only and
|
||||
requires the public `PULSE_UPDATE_SIGNING_PUBLIC_KEY` repository variable.
|
||||
|
||||
Future release candidates also carry
|
||||
`release-build-provenance.sigstore.json`, produced by the hosted
|
||||
`build-release-candidate.yml` job after complete candidate validation. The
|
||||
bundle is covered by the immutable candidate manifest and lets consumers
|
||||
verify downloaded files offline against the candidate-builder identity.
|
||||
|
||||
## Issue Triage Automation
|
||||
|
||||
**Files**:
|
||||
|
||||
@@ -634,7 +634,9 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
actions: read
|
||||
attestations: write
|
||||
contents: read
|
||||
id-token: write
|
||||
outputs:
|
||||
artifact_name: ${{ steps.identity.outputs.artifact_name }}
|
||||
manifest_artifact_name: ${{ steps.identity.outputs.manifest_artifact_name }}
|
||||
@@ -817,7 +819,43 @@ jobs:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: ./scripts/validate-release.sh "${VERSION}" --skip-docker
|
||||
|
||||
- name: Create immutable candidate manifest
|
||||
- name: Create candidate subject manifest
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 scripts/release_candidate_manifest.py create \
|
||||
--release-dir release \
|
||||
--version "${VERSION}" \
|
||||
--source-sha "${GITHUB_SHA}" \
|
||||
--output release-candidate-manifest/release-candidate.json
|
||||
jq -r \
|
||||
'.assets[] | "\(.sha256) release/\(.name)"' \
|
||||
release-candidate-manifest/release-candidate.json \
|
||||
> "$RUNNER_TEMP/release-candidate-subjects.sha256"
|
||||
|
||||
# Generate provenance at the hosted boundary that assembled and validated
|
||||
# the complete candidate, rather than later in the publication job. Keep
|
||||
# the Sigstore bundle with the candidate so customers can verify an exact
|
||||
# downloaded asset without access to GitHub's attestation API.
|
||||
- name: Attest complete release candidate
|
||||
id: attest_release_candidate
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-checksums: ${{ runner.temp }}/release-candidate-subjects.sha256
|
||||
|
||||
- name: Preserve portable build provenance
|
||||
env:
|
||||
PROVENANCE_BUNDLE: ${{ steps.attest_release_candidate.outputs.bundle-path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -s "${PROVENANCE_BUNDLE}"
|
||||
jq -e 'type == "object"' "${PROVENANCE_BUNDLE}" >/dev/null
|
||||
install -m 0644 \
|
||||
"${PROVENANCE_BUNDLE}" \
|
||||
release/release-build-provenance.sigstore.json
|
||||
|
||||
- name: Seal immutable candidate manifest
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
|
||||
@@ -307,7 +307,9 @@ jobs:
|
||||
if: ${{ needs.prepare.outputs.historical_asset_backfill_only != 'true' }}
|
||||
permissions:
|
||||
actions: write
|
||||
attestations: write
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/build-release-candidate.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
@@ -798,8 +800,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
outputs:
|
||||
release_id: ${{ steps.create_release.outputs.release_id }}
|
||||
release_url: ${{ steps.create_release.outputs.release_url }}
|
||||
@@ -839,11 +839,6 @@ jobs:
|
||||
--version "${{ needs.prepare.outputs.version }}" \
|
||||
--source-sha "${GITHUB_SHA}"
|
||||
|
||||
- name: Attest release assets
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-path: release/*
|
||||
|
||||
- name: Prepare release notes
|
||||
id: generate_notes
|
||||
run: |
|
||||
@@ -1164,6 +1159,10 @@ jobs:
|
||||
if ls release/*.tgz 1> /dev/null 2>&1; then
|
||||
release_upload_with_retry "${TAG}" release/*.tgz --clobber
|
||||
fi
|
||||
release_upload_with_retry \
|
||||
"${TAG}" \
|
||||
release/release-build-provenance.sigstore.json \
|
||||
--clobber
|
||||
for bare_agent in \
|
||||
release/pulse-agent-linux-amd64 \
|
||||
release/pulse-agent-linux-arm64 \
|
||||
|
||||
@@ -73,7 +73,9 @@ jobs:
|
||||
if: ${{ inputs.version != '' }}
|
||||
permissions:
|
||||
actions: write
|
||||
attestations: write
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/build-release-candidate.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
|
||||
@@ -73,6 +73,13 @@ Normal stable publication and stable dry runs select `signpath` directly.
|
||||
policy is invalid.
|
||||
- Release checksums and detached signatures are published alongside artifacts
|
||||
and verified independently after publication.
|
||||
- The GitHub-hosted `build-release-candidate.yml` assembly job emits SLSA v1
|
||||
provenance for every candidate file after complete local validation. Its
|
||||
Sigstore bundle is then added to the immutable candidate as
|
||||
`release-build-provenance.sigstore.json`; publication cannot replace that
|
||||
bundle without failing the candidate manifest. This preserves the exact
|
||||
builder evidence for offline verification instead of recreating provenance
|
||||
in the later publication job.
|
||||
- The exact-version OCI Helm chart is published only by the hosted
|
||||
`publish-helm-chart.yml` workflow. Its SHA-256 manifest digest and GitHub
|
||||
build-provenance attestation must bind to the release source commit before
|
||||
@@ -84,8 +91,11 @@ Normal stable publication and stable dry runs select `signpath` directly.
|
||||
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.
|
||||
`build-release-candidate.yml` workflow and release source commit;
|
||||
repository-level provenance is not sufficient. Immutable releases created
|
||||
before portable candidate bundles remain verified against their original
|
||||
`create-release.yml` publication provenance. Both paths reject provenance
|
||||
emitted from a self-hosted runner.
|
||||
- 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
|
||||
|
||||
@@ -26,6 +26,39 @@ See the [Code Signing Policy](CODE_SIGNING_POLICY.md) for build provenance,
|
||||
approval roles, signing scope, and reporting requirements. Release downloads
|
||||
are published on the [GitHub Releases page](https://github.com/rcourtman/Pulse/releases).
|
||||
|
||||
## Verify release build provenance
|
||||
|
||||
New release packets include `release-build-provenance.sigstore.json`, the
|
||||
Sigstore bundle emitted by the hosted workflow that assembled and validated
|
||||
the candidate. Verify a downloaded asset against that exact workflow and the
|
||||
release source commit with GitHub CLI 2.97.0 or newer:
|
||||
|
||||
```bash
|
||||
export PULSE_VERSION=vX.Y.Z
|
||||
export PULSE_ASSET=pulse-vX.Y.Z-linux-amd64.tar.gz
|
||||
gh release download "${PULSE_VERSION}" --repo rcourtman/Pulse \
|
||||
--pattern "${PULSE_ASSET}" \
|
||||
--pattern release-build-provenance.sigstore.json
|
||||
SOURCE_SHA="$(gh api "repos/rcourtman/Pulse/releases/tags/${PULSE_VERSION}" \
|
||||
--jq .target_commitish)"
|
||||
printf '%s\n' "${SOURCE_SHA}" > release-source-sha.txt
|
||||
gh attestation verify "${PULSE_ASSET}" \
|
||||
--repo rcourtman/Pulse \
|
||||
--bundle release-build-provenance.sigstore.json \
|
||||
--signer-workflow github.com/rcourtman/Pulse/.github/workflows/build-release-candidate.yml \
|
||||
--source-digest "${SOURCE_SHA}" \
|
||||
--deny-self-hosted-runners \
|
||||
--predicate-type https://slsa.dev/provenance/v1
|
||||
```
|
||||
|
||||
For an offline target, also run `gh attestation trusted-root >
|
||||
trusted_root.jsonl` on the connected trusted machine and transfer that file
|
||||
with the asset, bundle, and `release-source-sha.txt`. On the offline target,
|
||||
restore `SOURCE_SHA="$(cat release-source-sha.txt)"` and add
|
||||
`--custom-trusted-root trusted_root.jsonl` to the verification command. Refresh
|
||||
the trusted root whenever importing newly signed material; an old copy cannot
|
||||
report later key revocation or rotation.
|
||||
|
||||
## 🚀 Quick Start (Recommended)
|
||||
|
||||
### Proxmox VE (LXC installer)
|
||||
|
||||
@@ -964,6 +964,13 @@ artifact-selection behaviour.
|
||||
unpublished draft until every exact-version customer artifact required by
|
||||
the cut has passed its owned proof. It must then be published and publicly
|
||||
verified before any mutable customer pointer or live environment advances.
|
||||
After complete local packet validation, the GitHub-hosted candidate assembly
|
||||
job must issue SLSA v1 provenance over every file represented by the
|
||||
pre-provenance candidate manifest. It must preserve the resulting portable
|
||||
Sigstore bundle as `release-build-provenance.sigstore.json` before sealing
|
||||
the final immutable candidate manifest. The publication job may transport
|
||||
only that manifest-bound bundle; it must not replace candidate-build
|
||||
provenance with a later publication-bound attestation.
|
||||
Standard post-upload validation
|
||||
must compare that manifest with GitHub's server-side asset digests instead
|
||||
of downloading the complete release packet again. Historical repair and
|
||||
@@ -4568,6 +4575,12 @@ 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.
|
||||
SLSA v1 provenance from the exact expected source SHA while rejecting
|
||||
self-hosted provenance. A manifest-bound
|
||||
`release-build-provenance.sigstore.json` must itself pass the immutable release
|
||||
asset proof and must verify `checksums.txt` from the exact
|
||||
`build-release-candidate.yml` signer using that local bundle. Immutable
|
||||
historical releases without a portable candidate bundle retain verification
|
||||
against their original `create-release.yml` publication provenance. Multi-asset
|
||||
download retries clear the activation marker, checksum manifest, and portable
|
||||
bundle first so a partial attempt cannot poison every later retry.
|
||||
|
||||
@@ -73,6 +73,11 @@ Normal stable publication and stable dry runs select `signpath` directly.
|
||||
policy is invalid.
|
||||
- Release checksums and detached signatures are published alongside artifacts
|
||||
and verified independently after publication.
|
||||
- The GitHub-hosted `build-release-candidate.yml` assembly job emits SLSA v1
|
||||
provenance for every candidate file after complete local validation. Its
|
||||
portable Sigstore bundle is published as
|
||||
`release-build-provenance.sigstore.json` and covered by the immutable
|
||||
candidate manifest. Verification rejects self-hosted provenance.
|
||||
- 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
|
||||
|
||||
@@ -26,6 +26,39 @@ See the [Code Signing Policy](CODE_SIGNING_POLICY.md) for build provenance,
|
||||
approval roles, signing scope, and reporting requirements. Release downloads
|
||||
are published on the [GitHub Releases page](https://github.com/rcourtman/Pulse/releases).
|
||||
|
||||
## Verify release build provenance
|
||||
|
||||
New release packets include `release-build-provenance.sigstore.json`, the
|
||||
Sigstore bundle emitted by the hosted workflow that assembled and validated
|
||||
the candidate. Verify a downloaded asset against that exact workflow and the
|
||||
release source commit with GitHub CLI 2.97.0 or newer:
|
||||
|
||||
```bash
|
||||
export PULSE_VERSION=vX.Y.Z
|
||||
export PULSE_ASSET=pulse-vX.Y.Z-linux-amd64.tar.gz
|
||||
gh release download "${PULSE_VERSION}" --repo rcourtman/Pulse \
|
||||
--pattern "${PULSE_ASSET}" \
|
||||
--pattern release-build-provenance.sigstore.json
|
||||
SOURCE_SHA="$(gh api "repos/rcourtman/Pulse/releases/tags/${PULSE_VERSION}" \
|
||||
--jq .target_commitish)"
|
||||
printf '%s\n' "${SOURCE_SHA}" > release-source-sha.txt
|
||||
gh attestation verify "${PULSE_ASSET}" \
|
||||
--repo rcourtman/Pulse \
|
||||
--bundle release-build-provenance.sigstore.json \
|
||||
--signer-workflow github.com/rcourtman/Pulse/.github/workflows/build-release-candidate.yml \
|
||||
--source-digest "${SOURCE_SHA}" \
|
||||
--deny-self-hosted-runners \
|
||||
--predicate-type https://slsa.dev/provenance/v1
|
||||
```
|
||||
|
||||
For an offline target, also run `gh attestation trusted-root >
|
||||
trusted_root.jsonl` on the connected trusted machine and transfer that file
|
||||
with the asset, bundle, and `release-source-sha.txt`. On the offline target,
|
||||
restore `SOURCE_SHA="$(cat release-source-sha.txt)"` and add
|
||||
`--custom-trusted-root trusted_root.jsonl` to the verification command. Refresh
|
||||
the trusted root whenever importing newly signed material; an old copy cannot
|
||||
report later key revocation or rotation.
|
||||
|
||||
## 🚀 Quick Start (Recommended)
|
||||
|
||||
### Proxmox VE (LXC installer)
|
||||
|
||||
@@ -882,8 +882,7 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) {
|
||||
`gh release upload "$@"`,
|
||||
`gh release upload failed on attempt ${attempt}/${max_attempts}; retrying in ${wait_seconds}s`,
|
||||
`gh release upload failed after ${max_attempts} attempts`,
|
||||
`uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4`,
|
||||
`subject-path: release/*`,
|
||||
`release/release-build-provenance.sigstore.json`,
|
||||
`gh api "repos/${{ github.repository }}/releases?per_page=100" --paginate`,
|
||||
`git push origin "refs/tags/${TAG}" --force`,
|
||||
`--rawfile body "$NOTES_FILE"`,
|
||||
|
||||
@@ -1377,12 +1377,14 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertRegex(
|
||||
workflow,
|
||||
r"(?ms)^ build_release_candidate:\n.*?^ permissions:\n"
|
||||
r" actions: write\n contents: read\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
|
||||
r" actions: write\n attestations: write\n contents: read\n"
|
||||
r" id-token: write\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
|
||||
)
|
||||
self.assertRegex(
|
||||
release_workflow,
|
||||
r"(?ms)^ build_release_candidate:\n.*?^ permissions:\n"
|
||||
r" actions: write\n contents: read\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
|
||||
r" actions: write\n attestations: write\n contents: read\n"
|
||||
r" id-token: write\n uses: \.\/\.github\/workflows\/build-release-candidate\.yml$",
|
||||
)
|
||||
self.assertIn("Definitive Dry-Run Verdict", workflow)
|
||||
self.assertIn('require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success', workflow)
|
||||
@@ -1731,10 +1733,27 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn("tests/integration/release-integration-diagnostics/", content)
|
||||
self.assertIn("--target runtime_prebuilt", docker_build)
|
||||
self.assertIn("--target agent_runtime_prebuilt", docker_build)
|
||||
self.assertIn("id-token: write", content)
|
||||
self.assertIn("attestations: write", content)
|
||||
self.assertIn("uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4", content)
|
||||
self.assertIn("subject-path: release/*", content)
|
||||
self.assertIn("id-token: write", candidate_workflow)
|
||||
self.assertIn("attestations: write", candidate_workflow)
|
||||
self.assertIn(
|
||||
"uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4",
|
||||
candidate_workflow,
|
||||
)
|
||||
self.assertIn(
|
||||
"subject-checksums: ${{ runner.temp }}/release-candidate-subjects.sha256",
|
||||
candidate_workflow,
|
||||
)
|
||||
self.assertIn("Preserve portable build provenance", candidate_workflow)
|
||||
self.assertIn("release-build-provenance.sigstore.json", candidate_workflow)
|
||||
self.assertLess(
|
||||
candidate_workflow.index("Validate complete candidate locally"),
|
||||
candidate_workflow.index("Attest complete release candidate"),
|
||||
)
|
||||
self.assertLess(
|
||||
candidate_workflow.index("Attest complete release candidate"),
|
||||
candidate_workflow.index("Seal immutable candidate manifest"),
|
||||
)
|
||||
self.assertIn("release-build-provenance.sigstore.json", content)
|
||||
build_script = read("scripts/build-release.sh")
|
||||
release_asset_helper = read("scripts/release_asset_common.sh")
|
||||
backfill_script = read("scripts/backfill-release-assets.sh")
|
||||
|
||||
@@ -65,6 +65,10 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
|
||||
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"
|
||||
if [ "$HAS_PORTABLE_PROVENANCE" = true ]; then
|
||||
printf '%s\\n' '{{"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json"}}' \
|
||||
> "$2/release-build-provenance.sigstore.json"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
shift
|
||||
@@ -95,6 +99,13 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
|
||||
"GH_VERSION": gh_version,
|
||||
"PARTIAL_DOWNLOAD_ONCE": str(partial_download_once).lower(),
|
||||
"DOWNLOAD_STATE": str(root / "download-state"),
|
||||
"HAS_PORTABLE_PROVENANCE": str(
|
||||
any(
|
||||
asset.get("name")
|
||||
== "release-build-provenance.sigstore.json"
|
||||
for asset in release.get("assets", [])
|
||||
)
|
||||
).lower(),
|
||||
}
|
||||
)
|
||||
result = subprocess.run(
|
||||
@@ -109,8 +120,8 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
|
||||
return result, call_text
|
||||
|
||||
@staticmethod
|
||||
def release(*, immutable: bool = True) -> dict:
|
||||
return {
|
||||
def release(*, immutable: bool = True, portable_provenance: bool = False) -> dict:
|
||||
release = {
|
||||
"id": 123,
|
||||
"tag_name": "v6.5.0",
|
||||
"target_commitish": SOURCE_SHA,
|
||||
@@ -127,6 +138,16 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
|
||||
}
|
||||
],
|
||||
}
|
||||
if portable_provenance:
|
||||
release["assets"].append(
|
||||
{
|
||||
"name": "release-build-provenance.sigstore.json",
|
||||
"state": "uploaded",
|
||||
"size": 1200,
|
||||
"digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
)
|
||||
return release
|
||||
|
||||
def test_accepts_immutable_release_with_verified_attestation(self) -> None:
|
||||
result, calls = self.run_verifier(self.release())
|
||||
@@ -143,8 +164,28 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
|
||||
calls,
|
||||
)
|
||||
self.assertIn(f"--source-digest {SOURCE_SHA}", calls)
|
||||
self.assertIn("--deny-self-hosted-runners", calls)
|
||||
self.assertIn("--predicate-type https://slsa.dev/provenance/v1", calls)
|
||||
|
||||
def test_prefers_portable_candidate_build_provenance(self) -> None:
|
||||
result, calls = self.run_verifier(self.release(portable_provenance=True))
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("portable candidate-build provenance", result.stdout)
|
||||
self.assertIn(
|
||||
"--signer-workflow github.com/rcourtman/Pulse/.github/workflows/build-release-candidate.yml",
|
||||
calls,
|
||||
)
|
||||
self.assertIn("--bundle ", calls)
|
||||
self.assertIn("release-build-provenance.sigstore.json", calls)
|
||||
|
||||
def test_rejects_invalid_portable_provenance_asset(self) -> None:
|
||||
release = self.release(portable_provenance=True)
|
||||
release["assets"][1]["size"] = 0
|
||||
result, calls = self.run_verifier(release)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("invalid portable provenance asset", result.stderr)
|
||||
self.assertNotIn("release verify", calls)
|
||||
|
||||
def test_rejects_mutable_release_before_attestation(self) -> None:
|
||||
result, calls = self.run_verifier(self.release(immutable=False))
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
@@ -59,7 +59,9 @@ attestation_json="$(mktemp)"
|
||||
activation_dir="$(mktemp -d)"
|
||||
activation_asset="${activation_dir}/release-activation.json"
|
||||
checksums_asset="${activation_dir}/checksums.txt"
|
||||
SIGNER_WORKFLOW="github.com/${REPO}/.github/workflows/create-release.yml"
|
||||
provenance_asset="${activation_dir}/release-build-provenance.sigstore.json"
|
||||
legacy_signer_workflow="github.com/${REPO}/.github/workflows/create-release.yml"
|
||||
candidate_signer_workflow="github.com/${REPO}/.github/workflows/build-release-candidate.yml"
|
||||
cleanup() {
|
||||
rm -f "$release_json" "$attestation_json"
|
||||
rm -rf "$activation_dir"
|
||||
@@ -97,6 +99,30 @@ if ! jq -e \
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# New release candidates carry the exact Sigstore bundle created by the hosted
|
||||
# candidate builder. Existing immutable releases predate that asset and retain
|
||||
# their publication-workflow provenance, so continuity checks remain valid
|
||||
# until the next stable packet is activated.
|
||||
provenance_asset_count="$(
|
||||
jq '[.assets[]? | select(.name == "release-build-provenance.sigstore.json")] | length' \
|
||||
"$release_json"
|
||||
)"
|
||||
if [ "$provenance_asset_count" -gt 1 ]; then
|
||||
echo "GitHub release ${TAG} contains duplicate portable provenance assets." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$provenance_asset_count" = 1 ] && ! jq -e \
|
||||
'[.assets[]? | select(
|
||||
.name == "release-build-provenance.sigstore.json" and
|
||||
.state == "uploaded" and
|
||||
(.size | type == "number" and . > 0) and
|
||||
(.digest | type == "string" and test("^sha256:[0-9a-f]{64}$"))
|
||||
)] | length == 1' \
|
||||
"$release_json" >/dev/null; then
|
||||
echo "GitHub release ${TAG} has an invalid portable provenance asset." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verified=false
|
||||
for attempt in $(seq 1 "$ATTESTATION_ATTEMPTS"); do
|
||||
if gh release verify "$TAG" --repo "$REPO" --format json > "$attestation_json"; then
|
||||
@@ -124,17 +150,24 @@ fi
|
||||
# release attestation rather than trusting filename and JSON identity alone.
|
||||
downloaded=false
|
||||
for attempt in $(seq 1 "$ATTESTATION_ATTEMPTS"); do
|
||||
# A previous attempt can leave either asset behind after a partial
|
||||
# download. Clear both because gh release download refuses to overwrite
|
||||
# A previous attempt can leave any asset behind after a partial
|
||||
# download. Clear them 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 \
|
||||
--pattern checksums.txt \
|
||||
--dir "$activation_dir" && \
|
||||
rm -f "$activation_asset" "$checksums_asset" "$provenance_asset"
|
||||
download_args=(
|
||||
"$TAG"
|
||||
--repo "$REPO"
|
||||
--pattern release-activation.json
|
||||
--pattern checksums.txt
|
||||
--dir "$activation_dir"
|
||||
)
|
||||
if [ "$provenance_asset_count" = 1 ]; then
|
||||
download_args+=(--pattern release-build-provenance.sigstore.json)
|
||||
fi
|
||||
if gh release download "${download_args[@]}" && \
|
||||
[ -s "$activation_asset" ] && \
|
||||
[ -s "$checksums_asset" ]; then
|
||||
[ -s "$checksums_asset" ] && \
|
||||
{ [ "$provenance_asset_count" = 0 ] || [ -s "$provenance_asset" ]; }; then
|
||||
downloaded=true
|
||||
break
|
||||
fi
|
||||
@@ -174,11 +207,29 @@ if ! jq -e 'type == "object" or type == "array"' "$attestation_json" >/dev/null;
|
||||
echo "GitHub release checksum manifest verification returned malformed JSON for ${TAG}." >&2
|
||||
exit 1
|
||||
fi
|
||||
signer_workflow="$legacy_signer_workflow"
|
||||
bundle_args=()
|
||||
if [ "$provenance_asset_count" = 1 ]; then
|
||||
if ! gh release verify-asset "$TAG" "$provenance_asset" \
|
||||
--repo "$REPO" --format json > "$attestation_json"; then
|
||||
echo "GitHub release portable provenance asset verification failed for ${TAG}." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e 'type == "object" or type == "array"' "$attestation_json" >/dev/null; then
|
||||
echo "GitHub release portable provenance asset verification returned malformed JSON for ${TAG}." >&2
|
||||
exit 1
|
||||
fi
|
||||
signer_workflow="$candidate_signer_workflow"
|
||||
bundle_args=(--bundle "$provenance_asset")
|
||||
fi
|
||||
|
||||
if ! gh attestation verify "$checksums_asset" \
|
||||
--repo "$REPO" \
|
||||
--signer-workflow "$SIGNER_WORKFLOW" \
|
||||
--signer-workflow "$signer_workflow" \
|
||||
--source-digest "$EXPECTED_SOURCE_SHA" \
|
||||
--deny-self-hosted-runners \
|
||||
--predicate-type https://slsa.dev/provenance/v1 \
|
||||
"${bundle_args[@]}" \
|
||||
>/dev/null; then
|
||||
echo "Release checksum manifest build provenance verification failed for ${TAG}." >&2
|
||||
exit 1
|
||||
@@ -187,4 +238,9 @@ fi
|
||||
release_id="$(jq -r '.id' "$release_json")"
|
||||
source_sha="$(jq -r '.target_commitish' "$release_json")"
|
||||
asset_count="$(jq -r '.assets | length' "$release_json")"
|
||||
echo "[OK] GitHub release ${TAG} is immutable, release-attested, activation-asset-bound, and build-provenance-bound: release_id=${release_id} source_sha=${source_sha} assets=${asset_count}."
|
||||
if [ "$provenance_asset_count" = 1 ]; then
|
||||
provenance_status="portable candidate-build provenance"
|
||||
else
|
||||
provenance_status="legacy publication provenance"
|
||||
fi
|
||||
echo "[OK] GitHub release ${TAG} is immutable, release-attested, activation-asset-bound, and build-provenance-bound (${provenance_status}): release_id=${release_id} source_sha=${source_sha} assets=${asset_count}."
|
||||
|
||||
Reference in New Issue
Block a user