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:
pulse-triage[bot] 2026-09-22 04:28:39 +01:00
parent dd93eeb6b4
commit d5de3e26fc
3 changed files with 61 additions and 14 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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(