Bind activation marker to release attestation

This commit is contained in:
pulse-triage[bot]
2026-08-29 21:11:12 +01:00
parent 1a48d3cbd6
commit 4d60d679f0
5 changed files with 92 additions and 14 deletions
+4 -2
View File
@@ -79,8 +79,10 @@ Normal stable publication and stable dry runs select `signpath` directly.
asset set from replacement.
- Customer-facing image aliases, Helm indexes, paid-runtime pointers, and demo
environments are not promoted until `gh release verify <tag> --repo
rcourtman/Pulse` validates GitHub's signed release attestation. Operators can
use the same command to verify the packet independently.
rcourtman/Pulse` validates GitHub's signed release attestation and `gh release
verify-asset <tag> <downloaded-asset> --repo rcourtman/Pulse` binds the
downloaded activation marker to that attestation. Operators can use the same
commands to verify the packet and any downloaded release asset independently.
## Project roles
@@ -4418,9 +4418,13 @@ fail and compensate the still-mutable publication back to a marker-free draft.
`scripts/verify-github-release-integrity.sh` is the shared post-publication
check. It binds the release database ID, tag, exact source SHA, immutable state,
and single digest-bearing activation marker, then requires `gh release verify`
to validate GitHub's signed release attestation. The source release verdict,
activation-only recovery, and `release-convergence.yml` must all use that
check. Convergence must not acquire 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.
to validate GitHub's signed release attestation. It must then download the
activation marker from that release and require `gh release verify-asset` to
bind the exact consumed bytes to the signed release attestation. Filename,
stored digest presence, and marker JSON identity are not substitutes for this
asset proof. The source release verdict, activation-only recovery, and
`release-convergence.yml` must all use that check. Convergence must not acquire
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.
@@ -79,8 +79,10 @@ Normal stable publication and stable dry runs select `signpath` directly.
asset set from replacement.
- Customer-facing image aliases, Helm indexes, paid-runtime pointers, and demo
environments are not promoted until `gh release verify <tag> --repo
rcourtman/Pulse` validates GitHub's signed release attestation. Operators can
use the same command to verify the packet independently.
rcourtman/Pulse` validates GitHub's signed release attestation and `gh release
verify-asset <tag> <downloaded-asset> --repo rcourtman/Pulse` binds the
downloaded activation marker to that attestation. Operators can use the same
commands to verify the packet and any downloaded release asset independently.
## Project roles
@@ -17,7 +17,13 @@ SOURCE_SHA = "a" * 40
class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
def run_verifier(self, release: dict, *, verification_succeeds: bool = True):
def run_verifier(
self,
release: dict,
*,
verification_succeeds: bool = True,
asset_verification_succeeds: bool = True,
):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
calls = root / "calls"
@@ -38,6 +44,21 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
printf '%s\\n' '{{"verified": true}}'
exit {0 if verification_succeeds else 1}
fi
if [ "$1 $2" = "release download" ]; then
while [ "$#" -gt 0 ]; do
if [ "$1" = --dir ]; then
mkdir -p "$2"
printf '%s\\n' '{{"schema_version": 1}}' > "$2/release-activation.json"
exit 0
fi
shift
done
exit 65
fi
if [ "$1 $2" = "release verify-asset" ]; then
printf '%s\\n' '{{"verified": true}}'
exit {0 if asset_verification_succeeds else 1}
fi
exit 64
"""
),
@@ -86,8 +107,9 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
def test_accepts_immutable_release_with_verified_attestation(self) -> None:
result, calls = self.run_verifier(self.release())
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("is immutable and attested", result.stdout)
self.assertIn("is immutable, attested, and activation-asset-bound", result.stdout)
self.assertIn("release verify v6.5.0 --repo rcourtman/Pulse --format json", calls)
self.assertIn("release verify-asset v6.5.0", calls)
def test_rejects_mutable_release_before_attestation(self) -> None:
result, calls = self.run_verifier(self.release(immutable=False))
@@ -109,6 +131,14 @@ class VerifyGitHubReleaseIntegrityTest(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertIn("attestation verification failed", result.stderr)
def test_rejects_activation_asset_outside_release_attestation(self) -> None:
result, calls = self.run_verifier(
self.release(), asset_verification_succeeds=False
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("activation asset verification failed", result.stderr)
self.assertIn("release verify-asset v6.5.0", calls)
if __name__ == "__main__":
unittest.main()
+42 -2
View File
@@ -53,7 +53,12 @@ done
release_json="$(mktemp)"
attestation_json="$(mktemp)"
cleanup() { rm -f "$release_json" "$attestation_json"; }
activation_dir="$(mktemp -d)"
activation_asset="${activation_dir}/release-activation.json"
cleanup() {
rm -f "$release_json" "$attestation_json"
rm -rf "$activation_dir"
}
trap cleanup EXIT
gh api \
@@ -108,7 +113,42 @@ if ! jq -e 'type == "object" or type == "array"' "$attestation_json" >/dev/null;
exit 1
fi
# Release verification proves that GitHub signed the immutable packet. Bind the
# activation marker that convergence consumes to that packet as a separate
# proof: verify-asset checks the downloaded bytes' digest against the signed
# release attestation rather than trusting filename and JSON identity alone.
downloaded=false
for attempt in $(seq 1 "$ATTESTATION_ATTEMPTS"); do
rm -f "$activation_asset"
if gh release download "$TAG" \
--repo "$REPO" \
--pattern release-activation.json \
--dir "$activation_dir" && \
[ -s "$activation_asset" ]; then
downloaded=true
break
fi
if [ "$attempt" -lt "$ATTESTATION_ATTEMPTS" ]; then
echo "Activation asset for ${TAG} is not downloadable yet (${attempt}/${ATTESTATION_ATTEMPTS}); retrying." >&2
sleep "$ATTESTATION_RETRY_DELAY"
fi
done
if [ "$downloaded" != true ]; then
echo "GitHub release activation asset download failed for ${TAG}." >&2
exit 1
fi
if ! gh release verify-asset "$TAG" "$activation_asset" \
--repo "$REPO" --format json > "$attestation_json"; then
echo "GitHub release activation 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 activation asset verification returned malformed JSON for ${TAG}." >&2
exit 1
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 and attested: release_id=${release_id} source_sha=${source_sha} assets=${asset_count}."
echo "[OK] GitHub release ${TAG} is immutable, attested, and activation-asset-bound: release_id=${release_id} source_sha=${source_sha} assets=${asset_count}."