From 0db73f0605f02955440e02f7a0fbf5b2557a6165 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 22 Aug 2026 22:32:15 +0200 Subject: [PATCH] Require a WP-0025 broker receipt at admit-plane Platform reproduced a recanonicalized projection receipt with a substituted all-zero broker digest on the operational path. WP-0025 admission now requires --broker-receipt, threads it through broker_from_receipt, and refuses unless the digest matches exactly. Assistant: grok Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb --- src/whitehat_security/cli.py | 10 +++- src/whitehat_security/platform_custody.py | 37 +++++++------ tests/test_cli.py | 52 +++++++++++++++++++ tests/test_platform_custody_adapter.py | 10 ++++ ...ITEHAT-WP-0005-broker-receipt-admission.md | 48 +++++++++++++++++ 5 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 workplans/WHITEHAT-WP-0005-broker-receipt-admission.md diff --git a/src/whitehat_security/cli.py b/src/whitehat_security/cli.py index 9053169..87563f1 100644 --- a/src/whitehat_security/cli.py +++ b/src/whitehat_security/cli.py @@ -96,6 +96,10 @@ def main(argv: list[str] | None = None) -> None: "--contract", help="WP-0025 projection contract; required for railiance.custody-projection-receipt", ) + admit_plane.add_argument( + "--broker-receipt", + help="WP-0025 broker-readiness receipt; required for railiance.custody-projection-receipt", + ) commands.add_parser("kill-switch") deliver = commands.add_parser("deliver") deliver.add_argument("report") @@ -145,7 +149,11 @@ def main(argv: list[str] | None = None) -> None: record = Engagement.load(args.engagement) registration = load_registration(args.registration) broker = ( - broker_from_receipt(args.receipt, contract_path=args.contract) + broker_from_receipt( + args.receipt, + contract_path=args.contract, + broker_path=args.broker_receipt, + ) if args.receipt else default_broker(record) ) lease = admit(engagement=record, registration=registration, diff --git a/src/whitehat_security/platform_custody.py b/src/whitehat_security/platform_custody.py index 2f3347c..0d9097a 100644 --- a/src/whitehat_security/platform_custody.py +++ b/src/whitehat_security/platform_custody.py @@ -397,21 +397,20 @@ class PlatformCustodyBroker: receipt: dict[str, Any], *, contract: dict[str, Any], + broker: dict[str, Any], cleanup: dict[str, Any] | None = None, - broker: dict[str, Any] | None = None, now: datetime | None = None, ) -> None: if contract is None: raise AuthorizationError("WP-0025 projection receipt requires a bound contract") + if broker is None: + raise AuthorizationError("WP-0025 projection receipt requires a bound broker receipt") self.contract = validate_projection_contract(contract) self.receipt = validate_projection_receipt(receipt, contract=self.contract) - if broker is not None: - validated = validate_broker_readiness(broker, contract=self.contract, now=now) - if digest(validated) != self.receipt["broker_receipt_digest"]: - raise AuthorizationError("projection receipt broker digest does not match broker receipt") - self.broker = validated - else: - self.broker = None + validated = validate_broker_readiness(broker, contract=self.contract, now=now) + if digest(validated) != self.receipt["broker_receipt_digest"]: + raise AuthorizationError("projection receipt broker digest does not match broker receipt") + self.broker = validated self.cleanup = ( validate_cleanup_receipt( cleanup, projection=self.receipt, contract=self.contract @@ -425,23 +424,22 @@ class PlatformCustodyBroker: receipt_path: str | Path, *, contract_path: str | Path, + broker_path: str | Path, cleanup_path: str | Path | None = None, - broker_path: str | Path | None = None, now: datetime | None = None, ) -> "PlatformCustodyBroker": if contract_path is None: raise AuthorizationError("WP-0025 projection receipt requires a bound contract") + if broker_path is None: + raise AuthorizationError("WP-0025 projection receipt requires a bound broker receipt") receipt = json.loads(Path(receipt_path).read_text(encoding="utf-8")) contract = json.loads(Path(contract_path).read_text(encoding="utf-8")) + broker = json.loads(Path(broker_path).read_text(encoding="utf-8")) cleanup = ( json.loads(Path(cleanup_path).read_text(encoding="utf-8")) if cleanup_path is not None else None ) - broker = ( - json.loads(Path(broker_path).read_text(encoding="utf-8")) - if broker_path is not None else None - ) - return cls(receipt, contract=contract, cleanup=cleanup, broker=broker, now=now) + return cls(receipt, contract=contract, broker=broker, cleanup=cleanup, now=now) def project(self, engagement: Engagement, registration: dict[str, Any], now: datetime | None = None) -> tuple[IdentityHandle, ...]: @@ -476,13 +474,20 @@ class PlatformCustodyBroker: def broker_from_receipt( - path: str | Path, *, contract_path: str | Path | None = None + path: str | Path, + *, + contract_path: str | Path | None = None, + broker_path: str | Path | None = None, ) -> Any: """Select the WP-0025 adapter or the legacy value-safe receipt broker.""" data = json.loads(Path(path).read_text(encoding="utf-8")) if data.get("interface") == PROJECTION_INTERFACE: if contract_path is None: raise AuthorizationError("WP-0025 projection receipt requires a bound contract") - return PlatformCustodyBroker.load(path, contract_path=contract_path) + if broker_path is None: + raise AuthorizationError("WP-0025 projection receipt requires a bound broker receipt") + return PlatformCustodyBroker.load( + path, contract_path=contract_path, broker_path=broker_path + ) from .plane import ReceiptBroker return ReceiptBroker(data) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9f607cc..e6e6ab5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -164,6 +164,58 @@ def test_admit_plane_wp0025_receipt_requires_contract(tmp_path, capsys): assert "requires a bound contract" in capsys.readouterr().err +def test_admit_plane_wp0025_receipt_requires_broker_receipt(tmp_path, capsys): + engagement = tmp_path / "engagement.json" + receipt = tmp_path / "receipt.json" + contract = tmp_path / "contract.json" + engagement.write_text(json.dumps(_live_e2_record()), encoding="utf-8") + receipt.write_text(json.dumps({ + "interface": "railiance.custody-projection-receipt", + "version": 1, + "engagement_id": "WH-ENG-CLI-RECEIPT", + "secret_values_observed": False, + }), encoding="utf-8") + contract.write_text(json.dumps({ + "interface": "railiance.custody-projection-contract", + "version": 1, + }), encoding="utf-8") + with pytest.raises(SystemExit) as stopped: + main(["admit-plane", str(engagement), "targets/audit-core-e2.json", + "--receipt", str(receipt), "--contract", str(contract)]) + assert stopped.value.code == 2 + assert "requires a bound broker receipt" in capsys.readouterr().err + + +def test_admit_plane_wp0025_mismatched_broker_receipt(tmp_path, capsys): + from test_platform_custody_adapter import ( + broker_readiness, contract, digest, projection_receipt, + ) + + bound = contract() + receipt_doc = projection_receipt(bound) + receipt_doc["broker_receipt_digest"] = "0" * 64 + receipt_doc["receipt_id"] = "sha256:" + digest( + {key: value for key, value in receipt_doc.items() if key != "receipt_id"} + ) + engagement = tmp_path / "engagement.json" + receipt = tmp_path / "receipt.json" + contract_path = tmp_path / "contract.json" + broker_path = tmp_path / "broker.json" + engagement.write_text(json.dumps(_live_e2_record()), encoding="utf-8") + receipt.write_text(json.dumps(receipt_doc), encoding="utf-8") + contract_path.write_text(json.dumps(bound), encoding="utf-8") + broker_path.write_text(json.dumps(broker_readiness(bound)), encoding="utf-8") + with pytest.raises(SystemExit) as stopped: + main([ + "admit-plane", str(engagement), "targets/audit-core-e2.json", + "--receipt", str(receipt), + "--contract", str(contract_path), + "--broker-receipt", str(broker_path), + ]) + assert stopped.value.code == 2 + assert "broker digest" in capsys.readouterr().err + + def test_deliver_queues_abort_without_calling_it_target_assurance(tmp_path, capsys): report = json.loads( Path("evidence/WH-ENG-20260822-AUDIT-E2-02-abort.json").read_text(encoding="utf-8") diff --git a/tests/test_platform_custody_adapter.py b/tests/test_platform_custody_adapter.py index 860be1f..2adf98a 100644 --- a/tests/test_platform_custody_adapter.py +++ b/tests/test_platform_custody_adapter.py @@ -245,6 +245,16 @@ def test_receipt_without_contract_is_refused(tmp_path): broker_from_receipt(path) +def test_receipt_without_broker_is_refused(tmp_path): + bound = contract() + receipt = tmp_path / "receipt.json" + contract_path = tmp_path / "contract.json" + receipt.write_text(json.dumps(projection_receipt(bound)), encoding="utf-8") + contract_path.write_text(json.dumps(bound), encoding="utf-8") + with pytest.raises(AuthorizationError, match="requires a bound broker receipt"): + broker_from_receipt(receipt, contract_path=contract_path) + + def test_noncanonical_receipt_id_is_refused(): receipt = projection_receipt() receipt["receipt_id"] = "sha256:" + "e" * 64 diff --git a/workplans/WHITEHAT-WP-0005-broker-receipt-admission.md b/workplans/WHITEHAT-WP-0005-broker-receipt-admission.md new file mode 100644 index 0000000..0cc0437 --- /dev/null +++ b/workplans/WHITEHAT-WP-0005-broker-receipt-admission.md @@ -0,0 +1,48 @@ +--- +id: WHITEHAT-WP-0005 +type: workplan +title: "Require a WP-0025 broker receipt at plane admission" +domain: infotech +repo: whitehat-security +status: finished +owner: net-kingdom +topic_slug: whitehat-security +created: "2026-08-22" +updated: "2026-08-22" +related: + - WHITEHAT-WP-0004 + - RAILIANCE-WP-0025 +--- + +# WHITEHAT-WP-0005 — broker receipt admission + +## Goal + +Close the remaining WP-0025 operational binding from platform review +`cb6620f` / `5a0eb6b`: a projection receipt must not admit a plane lease +unless the caller also supplies the broker-readiness receipt whose digest +equals `broker_receipt_digest`. + +This plan authorizes no engagement, runner, credential, or traffic. + +## Origin + +Platform progress at 2026-08-22T20:26:00Z: `PlatformCustodyBroker` checks +the broker digest only when optional `broker=` is supplied; +`broker_from_receipt` / `admit-plane` expose no broker-receipt input. A +recanonicalized projection receipt with a substituted all-zero broker +digest was accepted on the operational constructor path. + +## Tasks + +### T01 — Required `--broker-receipt` and CLI fail-closed coverage + +```task +id: WHITEHAT-WP-0005-T01 +status: done +priority: high +``` + +Add required `--broker-receipt` for WP-0025 admission, pass it through +`broker_from_receipt`, and require exact digest equality. CLI tests cover +a missing broker receipt and a mismatched recanonicalized digest.