mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 20:59:36 +00:00
fix(release-control): qualify release-line RCs against their own branch
The secure-runtime RC qualification workflow (commit 79d87a4a2c) resolves the branch the control plane maps the version to and passes it as --main-ref, but secure_runtime_attestation_v6.verify_canonical_main_identity still required origin/main and required the candidate to be an ancestor of origin/main. Every release-train candidate therefore failed the pre-authentication step before any check ran; v6.4.5-rc.1 failed at 'Pre-authenticate exact qualification packet' with 'committed-main classification requires canonical origin/main' (run 35680955017), and all three historical runs failed.
Accept exactly origin/main or the release train's origin/release/vX.Y, verify the ref against the canonical origin's live branch tip, and check candidate ancestry against that same ref. The release-candidate classification is unchanged. Update the deployment-installability contract and the v6 unit tests, including a mapped-release-branch acceptance case.
Change-source: pulse-maintainer
This commit is contained in:
parent
dd93eeb6b4
commit
d5de3e26fc
3 changed files with 61 additions and 14 deletions
|
|
@ -534,7 +534,12 @@ workflow execution ref and SHA equal the peeled tag commit. That workflow consum
|
|||
release assets and four release signatures, runs the canonical twenty-three-scenario
|
||||
schema-v7 lab, and applies the release-candidate attester against the exact
|
||||
GitHub release ID, tag, source commit, checksums, compiler provenance, assembly
|
||||
provenance, and update-key fingerprint. The v7 host starts a rootful Docker
|
||||
provenance, and update-key fingerprint. The attester anchors the candidate to
|
||||
the branch the control plane maps the version to, either `origin/main` or the
|
||||
release train's `origin/release/vX.Y`, and verifies that remote-tracking ref
|
||||
against the canonical origin's live branch tip, so a release-train candidate is
|
||||
qualified against the line that owns its version rather than being required to
|
||||
appear on `main`. The v7 host starts a rootful Docker
|
||||
daemon inside the disposable systemd container without mounting the hosted
|
||||
runner socket or exposing a TCP listener. It imports an offline, source-bound
|
||||
fixture image and proves legacy inventory, summary-only typed-helper parity,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ ATTESTATION_TOOL_PATH = "scripts/release_control/secure_runtime_attestation_v6.p
|
|||
CANONICAL_REPOSITORY = "rcourtman/Pulse"
|
||||
CANONICAL_ORIGIN_URL = "https://github.com/rcourtman/Pulse.git"
|
||||
CANONICAL_MAIN_REF = "origin/main"
|
||||
CANONICAL_BRANCH_REF_RE = re.compile(r"^origin/(main|release/v[0-9]+\.[0-9]+)$")
|
||||
ASSEMBLY_SIGNER_WORKFLOW = "github.com/rcourtman/Pulse/.github/workflows/build-release-candidate.yml"
|
||||
COMPILER_SIGNER_WORKFLOW = "github.com/rcourtman/Pulse/.github/workflows/compile-release-payload.yml"
|
||||
RELEASE_TAG_RE = re.compile(r"^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[1-9][0-9]*$")
|
||||
|
|
@ -167,17 +168,23 @@ def parse_remote_refs(raw: bytes) -> dict[str, str]:
|
|||
|
||||
|
||||
def verify_canonical_main_identity(checkout: Path, main_ref: str) -> str:
|
||||
if main_ref != CANONICAL_MAIN_REF:
|
||||
raise v5.AttestationError("committed-main classification requires canonical origin/main")
|
||||
match = CANONICAL_BRANCH_REF_RE.fullmatch(main_ref)
|
||||
if match is None:
|
||||
raise v5.AttestationError(
|
||||
"committed-main classification requires a canonical origin branch ref"
|
||||
)
|
||||
branch = match.group(1)
|
||||
origin = v5.run_git(checkout, "remote", "get-url", "origin").stdout.decode().strip()
|
||||
if origin != CANONICAL_ORIGIN_URL:
|
||||
raise v5.AttestationError("origin remote URL is not the canonical Pulse repository URL")
|
||||
local_main = v5.resolve_commit(checkout, CANONICAL_MAIN_REF, "canonical origin/main")
|
||||
remote = v5.run_git(checkout, "ls-remote", "origin", "refs/heads/main")
|
||||
local_branch = v5.resolve_commit(checkout, main_ref, f"canonical origin/{branch}")
|
||||
remote = v5.run_git(checkout, "ls-remote", "origin", f"refs/heads/{branch}")
|
||||
remote_refs = parse_remote_refs(remote.stdout)
|
||||
if remote_refs != {"refs/heads/main": local_main}:
|
||||
raise v5.AttestationError("canonical origin/main does not match the remote main commit")
|
||||
return local_main
|
||||
if remote_refs != {f"refs/heads/{branch}": local_branch}:
|
||||
raise v5.AttestationError(
|
||||
f"canonical origin/{branch} does not match the remote branch commit"
|
||||
)
|
||||
return local_branch
|
||||
|
||||
|
||||
def verify_release_candidate_tag_identity(
|
||||
|
|
@ -609,7 +616,7 @@ def verify_and_snapshot_release_candidate_packet(
|
|||
raise v5.AttestationError("qualified commit must be the full canonical commit SHA")
|
||||
v5.require_detached_clean_checkout(checkout, resolved_commit)
|
||||
main_commit = verify_canonical_main_identity(checkout, main_ref)
|
||||
v5.require_ancestor(checkout, resolved_commit, CANONICAL_MAIN_REF)
|
||||
v5.require_ancestor(checkout, resolved_commit, main_ref)
|
||||
|
||||
expected_assets = expected_release_asset_names(architecture)
|
||||
expected_versions = expected_release_artifact_versions(tag)
|
||||
|
|
@ -952,7 +959,7 @@ def _create_attestation_with_snapshotted_artifacts(
|
|||
raise v5.AttestationError("qualified commit must be the full canonical commit SHA")
|
||||
v5.require_detached_clean_checkout(checkout, qualified_commit)
|
||||
main_commit = verify_canonical_main_identity(checkout, main_ref)
|
||||
v5.require_ancestor(checkout, qualified_commit, CANONICAL_MAIN_REF)
|
||||
v5.require_ancestor(checkout, qualified_commit, main_ref)
|
||||
with v6_contract():
|
||||
receipt, receipt_bytes = v5.load_receipt(receipt_path)
|
||||
record_path = v5.canonical_repo_path(receipt_record_path, "receipt record path")
|
||||
|
|
@ -1018,7 +1025,7 @@ def _create_attestation_with_snapshotted_artifacts(
|
|||
"qualified_ref_at_run": release_candidate_tag or qualified_commit,
|
||||
"main_ref_verified": main_ref,
|
||||
"main_ref_commit_at_attestation": main_commit,
|
||||
"qualified_commit_reachable_from_main": True,
|
||||
"qualified_commit_reachable_from_main": main_ref == CANONICAL_MAIN_REF,
|
||||
"build_checkout": "detached-worktree",
|
||||
"build_checkout_clean_except_lab_artifacts": True,
|
||||
"disposable_vm_guard_receipt_claim_validated": True,
|
||||
|
|
|
|||
|
|
@ -468,9 +468,9 @@ class SecureRuntimeAttestationV6Test(unittest.TestCase):
|
|||
verify_canonical_main_identity(self.root, CANONICAL_MAIN_REF),
|
||||
self.commit,
|
||||
)
|
||||
for caller_ref in ("HEAD", "main", "refs/heads/main", "scratch"):
|
||||
for caller_ref in ("HEAD", "main", "refs/heads/main", "scratch", "origin/feature/x"):
|
||||
with self.subTest(caller_ref=caller_ref), self.assertRaisesRegex(
|
||||
v5.AttestationError, "canonical origin/main"
|
||||
v5.AttestationError, "canonical origin branch ref"
|
||||
):
|
||||
verify_canonical_main_identity(self.root, caller_ref)
|
||||
|
||||
|
|
@ -482,10 +482,45 @@ class SecureRuntimeAttestationV6Test(unittest.TestCase):
|
|||
|
||||
with (
|
||||
mock.patch.object(v5, "run_git", side_effect=moved_remote),
|
||||
self.assertRaisesRegex(v5.AttestationError, "does not match the remote main commit"),
|
||||
self.assertRaisesRegex(v5.AttestationError, "does not match the remote branch commit"),
|
||||
):
|
||||
verify_canonical_main_identity(self.root, CANONICAL_MAIN_REF)
|
||||
|
||||
def test_committed_main_identity_accepts_mapped_release_branch(self) -> None:
|
||||
release_ref = "origin/release/v6.4"
|
||||
|
||||
def canonical(_checkout, *args, **_kwargs):
|
||||
command = tuple(args)
|
||||
if command == ("remote", "get-url", "origin"):
|
||||
output = CANONICAL_ORIGIN_URL + "\n"
|
||||
elif command == ("rev-parse", "--verify", f"{release_ref}^{{commit}}"):
|
||||
output = self.commit + "\n"
|
||||
elif command == ("ls-remote", "origin", "refs/heads/release/v6.4"):
|
||||
output = f"{self.commit}\trefs/heads/release/v6.4\n"
|
||||
else:
|
||||
raise AssertionError(f"unexpected git call {command}")
|
||||
return subprocess.CompletedProcess(args, 0, output.encode(), b"")
|
||||
|
||||
with mock.patch.object(v5, "run_git", side_effect=canonical):
|
||||
self.assertEqual(
|
||||
verify_canonical_main_identity(self.root, release_ref),
|
||||
self.commit,
|
||||
)
|
||||
|
||||
def moved_remote(_checkout, *args, **kwargs):
|
||||
result = canonical(_checkout, *args, **kwargs)
|
||||
if tuple(args) == ("ls-remote", "origin", "refs/heads/release/v6.4"):
|
||||
return subprocess.CompletedProcess(
|
||||
args, 0, f"{'c' * 40}\trefs/heads/release/v6.4\n".encode(), b""
|
||||
)
|
||||
return result
|
||||
|
||||
with (
|
||||
mock.patch.object(v5, "run_git", side_effect=moved_remote),
|
||||
self.assertRaisesRegex(v5.AttestationError, "does not match the remote branch commit"),
|
||||
):
|
||||
verify_canonical_main_identity(self.root, release_ref)
|
||||
|
||||
def test_accepts_canonical_annotated_tag_only_as_release_packet_identity(self) -> None:
|
||||
with mock.patch.object(v5, "run_git", side_effect=lambda _checkout, *args, **_kwargs: self.git_result(*args)):
|
||||
identity = verify_release_candidate_tag_identity(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue