diff --git a/README.md b/README.md index 107c249..2209b5e 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The following sequence is mandatory and ordered. Candidate bytes are inert: neve 1. Obtain explicit live-upload authority naming the repo/release/candidate. Record a non-secret authority reference. Fetch the reviewed warehouse PR, confirm the exact full commit SHA, clean worktree, and exact `origin`. 2. Confirm GitHub host/account/effective write permission and exact repo/release numeric+node identities. Authentication reports are suppressed to avoid credential metadata. -3. Independently verify the allowlisted immutable forge repository/workflow/ref/full SHA, candidate release/tag/ID/node ID/immutable state, canonical envelope, source manifest, checksums, staging asset-list digest, raw attestation bundle, and every inert asset size/digest. The preflight command itself freshly queries `github.com`, downloads the staging artifact and every candidate-release asset, compares those bytes with the local candidate, and reruns the raw protocol/attestation verifier; it never trusts an operator-supplied verification record. The warehouse consumes the exact audited promotion implementation pinned by [`protocol/forge-promotion-envelope-v1.json`](protocol/forge-promotion-envelope-v1.json) and separately requires the candidate issuer to contain or descend from the independently audited component-policy remediation pinned by [`protocol/forge-component-policy-v1.json`](protocol/forge-component-policy-v1.json). The exact remediated component/build schemas and validator blobs must also match that second pin. The warehouse does not redefine forge canonicalization, and a pre-remediation, unrelated, dirty, or regressed issuer is rejected. +3. Independently verify the allowlisted immutable forge repository/workflow/ref/full SHA, candidate release/tag/ID/node ID/immutable state, canonical envelope, source manifest, checksums, staging asset-list digest, raw attestation bundle, and every inert asset size/digest. The preflight command itself freshly queries `github.com`, downloads the staging artifact and every candidate-release asset, compares those bytes with the local candidate, and reruns the raw protocol/attestation verifier; it never trusts an operator-supplied verification record. The warehouse consumes the exact audited promotion implementation pinned by [`protocol/forge-promotion-envelope-v1.json`](protocol/forge-promotion-envelope-v1.json) and separately requires the live candidate issuer to descend from both the independently audited component-policy remediation and the Phase-6 policy revision pinned by [`protocol/forge-component-policy-v1.json`](protocol/forge-component-policy-v1.json). The exact current component/build schemas and validator blobs must match that second pin. Its older audited fixture blobs remain in an explicit `testFixture` lane used only with the noncryptographic test marker; they cannot authorize a live candidate. The warehouse does not redefine forge canonicalization, and a pre-revision, unrelated, dirty, or regressed live issuer is rejected. 4. Capture the complete FR-039 snapshot through all pages. It binds repository/release/body identities plus every legacy asset ID/node ID/name/state/size/API and independent download digest/API URL/download URL/content type/timestamps. A partial name-only inventory is invalid. 5. Run complete-set zero-write preflight. It reports every absent, identical no-op, and conflicting candidate name. Any conflict creates no release write, journal, catalog state, or stable index change. 6. Bind explicit authority, exact proposal, canonical candidate/envelope/list, complete snapshot hash, and intended warning-body digest into a mode-`0600` receipt. Type the exact receipt-hash confirmation. diff --git a/protocol/forge-component-policy-v1.json b/protocol/forge-component-policy-v1.json index 7c1632d..ee7ae65 100644 --- a/protocol/forge-component-policy-v1.json +++ b/protocol/forge-component-policy-v1.json @@ -2,6 +2,7 @@ "schemaVersion": "forge-component-policy-pin-v1", "repository": "acedward/midnight-binary-forge", "minimumCommitSha": "ddd2838d0226eeaaca8f7a42ad82cba1a132bbfe", + "policyRevisionCommitSha": "0170c0c4dbad8707e3b34e4daf029d3377d47077", "audit": { "path": "audits/00002-binary-publication-pipeline-phase1-remediation.md", "sha256": "8277a72c892d9d8ea87e3276cd6a75f1470ec1b282ed8495a7d9add3b4a8db72", @@ -15,11 +16,28 @@ }, { "path": "schema/build-set-v1.schema.json", - "sha256": "0800a100413b6866b12530af74421d78ff800514f79ad124ef2ffedcbad31d87" + "sha256": "77a08d5397d27edb45b08e5f6a6229bb6f195ec8117c5c8cae06a8901f690c78" }, { "path": "scripts/validate_catalog.py", - "sha256": "a0e5b6d1787db3faf1d9e40280682f7acca882b9706477126e51bffc1b1832d3" + "sha256": "4b4c6215fb852dcbdfb12b899a724642747e143cb6a2527605d50c261ed49a06" } - ] + ], + "testFixture": { + "commitSha": "ddd2838d0226eeaaca8f7a42ad82cba1a132bbfe", + "files": [ + { + "path": "schema/component-v1.schema.json", + "sha256": "e5073ce9367af76e43ee1eb675a73a819d4940dcd88619c5b5fc4e92bd28b982" + }, + { + "path": "schema/build-set-v1.schema.json", + "sha256": "0800a100413b6866b12530af74421d78ff800514f79ad124ef2ffedcbad31d87" + }, + { + "path": "scripts/validate_catalog.py", + "sha256": "a0e5b6d1787db3faf1d9e40280682f7acca882b9706477126e51bffc1b1832d3" + } + ] + } } diff --git a/scripts/promotion_tool.py b/scripts/promotion_tool.py index c3ab419..d4d597c 100755 --- a/scripts/promotion_tool.py +++ b/scripts/promotion_tool.py @@ -495,6 +495,11 @@ def validate_component_policy_checkout(record: dict[str, Any], checkout: Path) - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) expect(ancestry.returncode == 0, "candidate issuer does not descend from audited component-policy remediation") + revision_ancestry = subprocess.run( + ["git", "-C", os.fspath(checkout), "merge-base", "--is-ancestor", pin["policyRevisionCommitSha"], head], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + expect(revision_ancestry.returncode == 0, "candidate issuer does not descend from Phase-6 component-policy revision") for item in pin["files"]: path = checkout / item["path"] expect(path.is_file() and not path.is_symlink() and sha256_file(path) == item["sha256"], f"component-policy blob mismatch: {item['path']}") diff --git a/scripts/verify-candidate b/scripts/verify-candidate index 4f3878a..c524dbb 100755 --- a/scripts/verify-candidate +++ b/scripts/verify-candidate @@ -177,19 +177,26 @@ def main() -> int: fail("forge component-policy checkout origin is not allowlisted") minimum_component_commit = component_pin["minimumCommitSha"] if args.test_noncryptographic_fixture: - if component_head != minimum_component_commit: - fail("test component-policy checkout must be the exact independently audited remediation pin") + fixture = component_pin["testFixture"] + if component_head != fixture["commitSha"]: + fail("test component-policy checkout must be the exact nonauthorizing fixture pin") + pinned_component_files = fixture["files"] else: if component_head != claims["issuer"]["commitSha"]: fail("candidate issuer commit is not the exact component-policy checkout HEAD") - ancestry = subprocess.run( - ["git", "-C", os.fspath(component_checkout), "merge-base", "--is-ancestor", minimum_component_commit, component_head], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - if ancestry.returncode != 0: - fail("candidate issuer does not descend from the independently audited component-policy remediation") - for item in component_pin["files"]: + for required_ancestor, label in ( + (minimum_component_commit, "independently audited component-policy remediation"), + (component_pin["policyRevisionCommitSha"], "Phase-6 component-policy revision"), + ): + ancestry = subprocess.run( + ["git", "-C", os.fspath(component_checkout), "merge-base", "--is-ancestor", required_ancestor, component_head], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if ancestry.returncode != 0: + fail(f"candidate issuer does not descend from the {label}") + pinned_component_files = component_pin["files"] + for item in pinned_component_files: path = component_checkout / item["path"] if not path.is_file() or path.is_symlink() or sha256(path) != item["sha256"]: fail(f"component-policy pinned blob mismatch: {item['path']}") diff --git a/tests/test_promotion.py b/tests/test_promotion.py index 2276a8e..6e01a34 100644 --- a/tests/test_promotion.py +++ b/tests/test_promotion.py @@ -604,6 +604,9 @@ def test_prerequisite_live_recheck_rejects_cross_state_checkout(self) -> None: def test_component_policy_pin_rejects_pre_pin_unrelated_and_regressed_checkouts(self) -> None: pin = load_json(ROOT / "protocol/forge-component-policy-v1.json") + self.assertEqual(pin["testFixture"]["commitSha"], pin["minimumCommitSha"]) + self.assertEqual({row["path"] for row in pin["testFixture"]["files"]}, {row["path"] for row in pin["files"]}) + self.assertNotEqual(pin["policyRevisionCommitSha"], pin["minimumCommitSha"]) _, verification = self.evidence(canonical_bytes(self.envelope([], self.fixture_proposal([])))) with tempfile.TemporaryDirectory() as temporary: checkout = Path(temporary) / "component"