mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Bind activation marker to release attestation
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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}."
|
||||
|
||||
Reference in New Issue
Block a user