diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index eaef072958..71c46fa75c 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -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, diff --git a/scripts/release_control/secure_runtime_attestation_v6.py b/scripts/release_control/secure_runtime_attestation_v6.py index 3e063499fa..aae6b3f72a 100644 --- a/scripts/release_control/secure_runtime_attestation_v6.py +++ b/scripts/release_control/secure_runtime_attestation_v6.py @@ -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, diff --git a/scripts/release_control/secure_runtime_attestation_v6_test.py b/scripts/release_control/secure_runtime_attestation_v6_test.py index 91db466794..0235605428 100644 --- a/scripts/release_control/secure_runtime_attestation_v6_test.py +++ b/scripts/release_control/secure_runtime_attestation_v6_test.py @@ -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(