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
This commit is contained in:
parent
cb6620f33b
commit
0db73f0605
5 changed files with 140 additions and 17 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
48
workplans/WHITEHAT-WP-0005-broker-receipt-admission.md
Normal file
48
workplans/WHITEHAT-WP-0005-broker-receipt-admission.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue