Bind WP-0025 receipts to a canonical projection contract
Railiance's WP-0025 review found the first adapter fail-open: --contract was optional, receipt_id was not canonical, and target, identities, resources, UIDs, broker digest and times were unbound. Require the contract and refuse any receipt that is not the platform canonical form. Assistant: grok Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb
This commit is contained in:
parent
da6f5fb3f8
commit
5a0eb6b343
6 changed files with 592 additions and 140 deletions
|
|
@ -147,6 +147,23 @@ def test_example_projection_receipt_is_value_safe():
|
|||
assert broker.receipt["mounted_keys"] == ["token-a", "token-b"]
|
||||
|
||||
|
||||
def test_admit_plane_wp0025_receipt_requires_contract(tmp_path, capsys):
|
||||
engagement = tmp_path / "engagement.json"
|
||||
receipt = tmp_path / "receipt.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")
|
||||
with pytest.raises(SystemExit) as stopped:
|
||||
main(["admit-plane", str(engagement), "targets/audit-core-e2.json",
|
||||
"--receipt", str(receipt)])
|
||||
assert stopped.value.code == 2
|
||||
assert "requires a bound contract" 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")
|
||||
|
|
|
|||
|
|
@ -1,19 +1,33 @@
|
|||
import copy
|
||||
import json
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from whitehat_security.engagement import AuthorizationError, Engagement
|
||||
from whitehat_security.platform_custody import (
|
||||
CLEANUP_INTERFACE, CONTRACT_INTERFACE, PROJECTION_INTERFACE,
|
||||
PlatformCustodyBroker, load_schemas, validate_broker_readiness,
|
||||
validate_cleanup_receipt, validate_projection_contract,
|
||||
CLEANUP_INTERFACE,
|
||||
CONTRACT_INTERFACE,
|
||||
PROJECTION_INTERFACE,
|
||||
PlatformCustodyBroker,
|
||||
broker_from_receipt,
|
||||
contract_digest,
|
||||
digest,
|
||||
interface_artifacts,
|
||||
load_schemas,
|
||||
resource_names,
|
||||
validate_broker_readiness,
|
||||
validate_cleanup_receipt,
|
||||
validate_projection_contract,
|
||||
validate_projection_receipt,
|
||||
)
|
||||
from whitehat_security.plane import KillSwitch, admit
|
||||
from whitehat_security.targets import load_registration
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 22, 12, tzinfo=UTC)
|
||||
NOW = datetime(2026, 8, 22, 12, 2, tzinfo=UTC)
|
||||
|
||||
|
||||
def contract() -> dict:
|
||||
|
|
@ -44,7 +58,7 @@ def contract() -> dict:
|
|||
"window": {
|
||||
"starts_at": "2026-08-22T12:00:00Z",
|
||||
"projection_cutoff": "2026-08-22T12:03:00Z",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"expires_at": "2026-08-22T12:15:00Z",
|
||||
},
|
||||
"authority": {
|
||||
"remote": "railiance01",
|
||||
|
|
@ -78,57 +92,8 @@ def contract() -> dict:
|
|||
}
|
||||
|
||||
|
||||
def projection_receipt() -> dict:
|
||||
return {
|
||||
"interface": PROJECTION_INTERFACE,
|
||||
"version": 1,
|
||||
"workplan_id": "RAILIANCE-WP-0025",
|
||||
"state": "projected",
|
||||
"receipt_id": "sha256:" + "e" * 64,
|
||||
"lease_id": "custody:" + "f" * 32,
|
||||
"engagement_id": "WH-ENG-FIXTURE-1",
|
||||
"target": {"id": "audit-core", "revision": "a" * 40, "image_digest": "sha256:" + "b" * 64},
|
||||
"projection_contract_digest": "1" * 64,
|
||||
"broker_receipt_digest": "2" * 64,
|
||||
"projected_at": "2026-08-22T12:02:00Z",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"identities": [
|
||||
{
|
||||
"handle": "token-a",
|
||||
"role": "attacker",
|
||||
"sender_name": "whitehat-e2-a",
|
||||
"mount_path": "/var/run/secrets/whitehat/token-a",
|
||||
},
|
||||
{
|
||||
"handle": "token-b",
|
||||
"role": "owner",
|
||||
"sender_name": "whitehat-e2-b",
|
||||
"mount_path": "/var/run/secrets/whitehat/token-b",
|
||||
},
|
||||
],
|
||||
"resources": {"names": {"store": "custody-example"}, "uids": {"mounted_secret": "uid"}},
|
||||
"cleanup_authority": "railiance-platform",
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
|
||||
|
||||
def cleanup_receipt() -> dict:
|
||||
return {
|
||||
"interface": CLEANUP_INTERFACE,
|
||||
"version": 1,
|
||||
"workplan_id": "RAILIANCE-WP-0025",
|
||||
"state": "cleaned",
|
||||
"lease_id": "custody:" + "f" * 32,
|
||||
"engagement_id": "WH-ENG-FIXTURE-1",
|
||||
"projection_receipt_id": "sha256:" + "e" * 64,
|
||||
"cleaned_at": "2026-08-22T12:14:00Z",
|
||||
"removed_resources": ["custody-example"],
|
||||
"target_ready": True,
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
|
||||
|
||||
def broker_readiness() -> dict:
|
||||
def broker_readiness(bound: dict | None = None) -> dict:
|
||||
bound = bound or contract()
|
||||
return {
|
||||
"interface": "railiance.custody-broker-readiness",
|
||||
"version": 1,
|
||||
|
|
@ -137,21 +102,13 @@ def broker_readiness() -> dict:
|
|||
"reviewer": "whitehat-owner",
|
||||
"decision": "approve",
|
||||
"created_at": "2026-08-22T12:00:00Z",
|
||||
"engagement_id": "WH-ENG-FIXTURE-1",
|
||||
"target_id": "audit-core",
|
||||
"projection_contract_digest": "1" * 64,
|
||||
"engagement_id": bound["engagement_id"],
|
||||
"target_id": bound["target"]["id"],
|
||||
"projection_contract_digest": contract_digest(bound),
|
||||
"projection_receipt_interface": PROJECTION_INTERFACE,
|
||||
"interface_artifacts": {
|
||||
"schemas/custody-projection-contract.schema.json": "a" * 64,
|
||||
"schemas/custody-broker-readiness.schema.json": "b" * 64,
|
||||
"schemas/custody-projection-receipt.schema.json": "c" * 64,
|
||||
"schemas/custody-cleanup-receipt.schema.json": "d" * 64,
|
||||
},
|
||||
"interface_artifacts": interface_artifacts(),
|
||||
"required_roles": ["attacker", "owner"],
|
||||
"mount_paths": [
|
||||
"/var/run/secrets/whitehat/token-a",
|
||||
"/var/run/secrets/whitehat/token-b",
|
||||
],
|
||||
"mount_paths": sorted(item["mount_path"] for item in bound["identities"]),
|
||||
"adapter": {
|
||||
"repo": "whitehat-security",
|
||||
"revision": "a" * 40,
|
||||
|
|
@ -164,11 +121,73 @@ def broker_readiness() -> dict:
|
|||
}
|
||||
|
||||
|
||||
def projection_receipt(bound: dict | None = None) -> dict:
|
||||
bound = bound or contract()
|
||||
base = {
|
||||
"interface": PROJECTION_INTERFACE,
|
||||
"version": 1,
|
||||
"workplan_id": "RAILIANCE-WP-0025",
|
||||
"state": "projected",
|
||||
"lease_id": "custody:" + "f" * 32,
|
||||
"engagement_id": bound["engagement_id"],
|
||||
"target": {
|
||||
"id": bound["target"]["id"],
|
||||
"revision": bound["target"]["revision"],
|
||||
"image_digest": bound["target"]["image_digest"],
|
||||
},
|
||||
"projection_contract_digest": contract_digest(bound),
|
||||
"broker_receipt_digest": digest(broker_readiness(bound)),
|
||||
"projected_at": "2026-08-22T12:02:00Z",
|
||||
"expires_at": bound["window"]["expires_at"],
|
||||
"identities": sorted(
|
||||
(
|
||||
{
|
||||
"handle": item["handle"],
|
||||
"role": item["role"],
|
||||
"sender_name": item["sender_name"],
|
||||
"mount_path": item["mount_path"],
|
||||
}
|
||||
for item in bound["identities"]
|
||||
),
|
||||
key=lambda item: item["handle"],
|
||||
),
|
||||
"resources": {
|
||||
"names": resource_names(bound),
|
||||
"uids": {
|
||||
"store": "uid-store",
|
||||
"external_secret": "uid-es",
|
||||
"mounted_secret": "uid-secret",
|
||||
},
|
||||
},
|
||||
"cleanup_authority": "railiance-platform",
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
return {**base, "receipt_id": "sha256:" + digest(base)}
|
||||
|
||||
|
||||
def cleanup_receipt(bound: dict | None = None, projection: dict | None = None) -> dict:
|
||||
bound = bound or contract()
|
||||
projection = projection or projection_receipt(bound)
|
||||
return {
|
||||
"interface": CLEANUP_INTERFACE,
|
||||
"version": 1,
|
||||
"workplan_id": "RAILIANCE-WP-0025",
|
||||
"state": "cleaned",
|
||||
"lease_id": projection["lease_id"],
|
||||
"engagement_id": bound["engagement_id"],
|
||||
"projection_receipt_id": projection["receipt_id"],
|
||||
"cleaned_at": "2026-08-22T12:14:00Z",
|
||||
"removed_resources": [resource_names(bound)["store"]],
|
||||
"target_ready": True,
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
|
||||
|
||||
def live_record():
|
||||
return {
|
||||
"engagement_id": "WH-ENG-FIXTURE-1", "authorization_id": "auth-1",
|
||||
"authorizer": "operator", "approved_at": "2026-08-22T11:00:00Z",
|
||||
"expires_at": "2099-01-01T00:00:00Z", "target": "https://fixture.invalid",
|
||||
"expires_at": "2026-08-22T12:15:00Z", "target": "https://fixture.invalid",
|
||||
"target_id": "audit-core", "target_owner": "audit-core",
|
||||
"environment": "build", "source": "runner", "approval_class": "live-e2",
|
||||
"plane_namespace": "whitehat", "runner_image_digest": "sha256:abc",
|
||||
|
|
@ -176,8 +195,8 @@ def live_record():
|
|||
"credential_lane": "receipt", "credential_role": "runtime",
|
||||
"credential_max_ttl_seconds": 900, "techniques": ["e2-differential"],
|
||||
"prohibited_techniques": ["saturation"], "rate_limit_per_minute": 10,
|
||||
"max_concurrency": 1, "window_start": "2026-08-22T11:00:00Z",
|
||||
"window_end": "2099-01-01T00:00:00Z", "operator_contact": "operator",
|
||||
"max_concurrency": 1, "window_start": "2026-08-22T12:00:00Z",
|
||||
"window_end": "2026-08-22T12:15:00Z", "operator_contact": "operator",
|
||||
"abort_contact": "operator", "posture_claim": "E2",
|
||||
"attacker_model": "E2-authenticated-tenant-a",
|
||||
"finding_destination": "risk-nexus",
|
||||
|
|
@ -200,26 +219,129 @@ def test_four_custody_schemas_are_published():
|
|||
|
||||
|
||||
def test_adapter_validates_all_four_document_kinds():
|
||||
assert validate_projection_contract(contract())["engagement_id"] == "WH-ENG-FIXTURE-1"
|
||||
assert validate_projection_receipt(projection_receipt(), contract=contract())["state"] == "projected"
|
||||
assert validate_cleanup_receipt(cleanup_receipt(), projection=projection_receipt())["state"] == "cleaned"
|
||||
assert validate_broker_readiness(broker_readiness())["owner"] == "whitehat-security"
|
||||
bound = contract()
|
||||
projection = projection_receipt(bound)
|
||||
assert validate_projection_contract(bound)["engagement_id"] == "WH-ENG-FIXTURE-1"
|
||||
assert validate_projection_receipt(projection, contract=bound)["state"] == "projected"
|
||||
assert validate_cleanup_receipt(
|
||||
cleanup_receipt(bound, projection), projection=projection, contract=bound
|
||||
)["state"] == "cleaned"
|
||||
assert validate_broker_readiness(
|
||||
broker_readiness(bound), contract=bound, now=NOW
|
||||
)["owner"] == "whitehat-security"
|
||||
|
||||
|
||||
def test_adapter_rejects_secret_material():
|
||||
tainted = projection_receipt()
|
||||
tainted["token"] = "never"
|
||||
with pytest.raises(AuthorizationError, match="secret material"):
|
||||
validate_projection_receipt(tainted)
|
||||
validate_projection_receipt(tainted, contract=contract())
|
||||
|
||||
|
||||
def test_receipt_without_contract_is_refused(tmp_path):
|
||||
path = tmp_path / "receipt.json"
|
||||
path.write_text(json.dumps(projection_receipt()), encoding="utf-8")
|
||||
with pytest.raises(AuthorizationError, match="requires a bound contract"):
|
||||
broker_from_receipt(path)
|
||||
|
||||
|
||||
def test_noncanonical_receipt_id_is_refused():
|
||||
receipt = projection_receipt()
|
||||
receipt["receipt_id"] = "sha256:" + "e" * 64
|
||||
with pytest.raises(AuthorizationError, match="canonical content digest"):
|
||||
validate_projection_receipt(receipt, contract=contract())
|
||||
|
||||
|
||||
def test_stale_contract_digest_is_refused():
|
||||
receipt = projection_receipt()
|
||||
receipt["projection_contract_digest"] = "1" * 64
|
||||
receipt["receipt_id"] = "sha256:" + digest(
|
||||
{key: value for key, value in receipt.items() if key != "receipt_id"}
|
||||
)
|
||||
with pytest.raises(AuthorizationError, match="contract digest"):
|
||||
validate_projection_receipt(receipt, contract=contract())
|
||||
|
||||
|
||||
def test_incomplete_resource_uids_are_refused():
|
||||
bound = contract()
|
||||
receipt = projection_receipt(bound)
|
||||
receipt["resources"]["uids"].pop("store")
|
||||
receipt["receipt_id"] = "sha256:" + digest(
|
||||
{key: value for key, value in receipt.items() if key != "receipt_id"}
|
||||
)
|
||||
with pytest.raises(AuthorizationError, match="exact Kubernetes UIDs"):
|
||||
validate_projection_receipt(receipt, contract=bound)
|
||||
|
||||
|
||||
def test_target_identity_resource_time_and_broker_digest_are_bound():
|
||||
bound = contract()
|
||||
receipt = projection_receipt(bound)
|
||||
mismatched = copy.deepcopy(receipt)
|
||||
mismatched["target"]["id"] = "other"
|
||||
mismatched["receipt_id"] = "sha256:" + digest(
|
||||
{key: value for key, value in mismatched.items() if key != "receipt_id"}
|
||||
)
|
||||
with pytest.raises(AuthorizationError, match="target"):
|
||||
validate_projection_receipt(mismatched, contract=bound)
|
||||
|
||||
mismatched = copy.deepcopy(receipt)
|
||||
mismatched["identities"][0]["sender_name"] = "wrong"
|
||||
mismatched["receipt_id"] = "sha256:" + digest(
|
||||
{key: value for key, value in mismatched.items() if key != "receipt_id"}
|
||||
)
|
||||
with pytest.raises(AuthorizationError, match="identities"):
|
||||
validate_projection_receipt(mismatched, contract=bound)
|
||||
|
||||
mismatched = copy.deepcopy(receipt)
|
||||
mismatched["resources"]["names"]["store"] = "custody-wrong"
|
||||
mismatched["receipt_id"] = "sha256:" + digest(
|
||||
{key: value for key, value in mismatched.items() if key != "receipt_id"}
|
||||
)
|
||||
with pytest.raises(AuthorizationError, match="resources"):
|
||||
validate_projection_receipt(mismatched, contract=bound)
|
||||
|
||||
mismatched = copy.deepcopy(receipt)
|
||||
mismatched["expires_at"] = "2026-08-22T12:14:00Z"
|
||||
mismatched["receipt_id"] = "sha256:" + digest(
|
||||
{key: value for key, value in mismatched.items() if key != "receipt_id"}
|
||||
)
|
||||
with pytest.raises(AuthorizationError, match="projection/expiry"):
|
||||
validate_projection_receipt(mismatched, contract=bound)
|
||||
|
||||
mismatched = copy.deepcopy(receipt)
|
||||
mismatched["broker_receipt_digest"] = "0" * 64
|
||||
mismatched["receipt_id"] = "sha256:" + digest(
|
||||
{key: value for key, value in mismatched.items() if key != "receipt_id"}
|
||||
)
|
||||
with pytest.raises(AuthorizationError, match="broker digest"):
|
||||
PlatformCustodyBroker(
|
||||
mismatched, contract=bound, broker=broker_readiness(bound), now=NOW
|
||||
)
|
||||
|
||||
|
||||
def test_platform_validator_accepts_the_canonical_fixture():
|
||||
scripts = Path.home() / "railiance-platform" / "scripts"
|
||||
if str(scripts) not in sys.path:
|
||||
sys.path.insert(0, str(scripts))
|
||||
import custody_contract
|
||||
|
||||
bound = contract()
|
||||
receipt = projection_receipt(bound)
|
||||
broker = broker_readiness(bound)
|
||||
assert receipt is custody_contract.validate_projection_receipt(receipt, bound)
|
||||
assert broker is custody_contract.validate_broker_receipt(broker, bound, now=NOW)
|
||||
|
||||
|
||||
def test_platform_broker_issues_handles_and_supports_cleanup(tmp_path):
|
||||
path = tmp_path / "engagement.json"
|
||||
path.write_text(json.dumps(live_record()), encoding="utf-8")
|
||||
engagement = Engagement.load(path, now=NOW)
|
||||
broker = PlatformCustodyBroker(projection_receipt(), contract=contract())
|
||||
bound = contract()
|
||||
receipt = projection_receipt(bound)
|
||||
broker = PlatformCustodyBroker(
|
||||
receipt, contract=bound, broker=broker_readiness(bound), now=NOW
|
||||
)
|
||||
assert broker.cleanup_request_supported is True
|
||||
from whitehat_security.targets import load_registration
|
||||
lease = admit(
|
||||
engagement=engagement,
|
||||
registration=load_registration("targets/audit-core-e2.json"),
|
||||
|
|
@ -233,13 +355,19 @@ def test_platform_broker_issues_handles_and_supports_cleanup(tmp_path):
|
|||
with pytest.raises(AuthorizationError, match="custody must revoke"):
|
||||
broker.revoke(lease.lease_id)
|
||||
cleaned = PlatformCustodyBroker(
|
||||
projection_receipt(), contract=contract(), cleanup=cleanup_receipt()
|
||||
receipt,
|
||||
contract=bound,
|
||||
cleanup=cleanup_receipt(bound, receipt),
|
||||
broker=broker_readiness(bound),
|
||||
now=NOW,
|
||||
)
|
||||
cleaned.revoke(lease.lease_id)
|
||||
|
||||
|
||||
def test_cleanup_receipt_must_match_lease():
|
||||
bound = contract()
|
||||
projection = projection_receipt(bound)
|
||||
with pytest.raises(AuthorizationError, match="lease_id"):
|
||||
bad = cleanup_receipt()
|
||||
bad = cleanup_receipt(bound, projection)
|
||||
bad["lease_id"] = "custody:" + "0" * 32
|
||||
validate_cleanup_receipt(bad, projection=projection_receipt())
|
||||
validate_cleanup_receipt(bad, projection=projection, contract=bound)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue