Add Railiance WP-0025 custody adapter
Implement src/whitehat_security/platform_custody.py and focused tests against the four custody schemas. Live admission still fails closed without a value-safe projection receipt. No new engagement or traffic. Assistant: grok Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb
This commit is contained in:
parent
5d2c9592e8
commit
1a38080cdd
5 changed files with 527 additions and 3 deletions
3
SCOPE.md
3
SCOPE.md
|
|
@ -65,7 +65,8 @@ boundary always holds.
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
- Repository status: active.
|
- Repository status: active.
|
||||||
- Active plan: `WHITEHAT-WP-0001`. Meantime polish: `WHITEHAT-WP-0002`.
|
- Active plan: `WHITEHAT-WP-0001`. Meantime polish: `WHITEHAT-WP-0002` and
|
||||||
|
`WHITEHAT-WP-0003` (Railiance WP-0025 custody adapter).
|
||||||
- `T01` is complete: the rules of engagement were accepted on 2026-08-21.
|
- `T01` is complete: the rules of engagement were accepted on 2026-08-21.
|
||||||
- `T02` is complete: the per-axis attacker model is recorded in
|
- `T02` is complete: the per-axis attacker model is recorded in
|
||||||
`docs/attacker-model.md`.
|
`docs/attacker-model.md`.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ from .e3 import CADENCE, PROBES, e3_calibration
|
||||||
from .engagement import AuthorizationError, Engagement
|
from .engagement import AuthorizationError, Engagement
|
||||||
from .fixtures import FixtureService, probe_suite
|
from .fixtures import FixtureService, probe_suite
|
||||||
from .model import RunReport, utc_now
|
from .model import RunReport, utc_now
|
||||||
from .plane import KillSwitch, ReceiptBroker, admit, default_broker, retired_ids
|
from .plane import KillSwitch, admit, default_broker, retired_ids
|
||||||
|
from .platform_custody import broker_from_receipt
|
||||||
from .reporting import queue_risk_nexus, risk_nexus_message
|
from .reporting import queue_risk_nexus, risk_nexus_message
|
||||||
from .targets import load_catalog, load_registration
|
from .targets import load_catalog, load_registration
|
||||||
|
|
||||||
|
|
@ -91,6 +92,7 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
admit_plane.add_argument("engagement")
|
admit_plane.add_argument("engagement")
|
||||||
admit_plane.add_argument("registration")
|
admit_plane.add_argument("registration")
|
||||||
admit_plane.add_argument("--receipt", help="value-safe custody projection receipt")
|
admit_plane.add_argument("--receipt", help="value-safe custody projection receipt")
|
||||||
|
admit_plane.add_argument("--contract", help="WP-0025 projection contract bound to the receipt")
|
||||||
commands.add_parser("kill-switch")
|
commands.add_parser("kill-switch")
|
||||||
deliver = commands.add_parser("deliver")
|
deliver = commands.add_parser("deliver")
|
||||||
deliver.add_argument("report")
|
deliver.add_argument("report")
|
||||||
|
|
@ -140,7 +142,8 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
record = Engagement.load(args.engagement)
|
record = Engagement.load(args.engagement)
|
||||||
registration = load_registration(args.registration)
|
registration = load_registration(args.registration)
|
||||||
broker = (
|
broker = (
|
||||||
ReceiptBroker.load(args.receipt) if args.receipt else default_broker(record)
|
broker_from_receipt(args.receipt, contract_path=args.contract)
|
||||||
|
if args.receipt else default_broker(record)
|
||||||
)
|
)
|
||||||
lease = admit(engagement=record, registration=registration,
|
lease = admit(engagement=record, registration=registration,
|
||||||
broker=broker, kill_switch=KillSwitch(),
|
broker=broker, kill_switch=KillSwitch(),
|
||||||
|
|
|
||||||
237
src/whitehat_security/platform_custody.py
Normal file
237
src/whitehat_security/platform_custody.py
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
"""Consume Railiance WP-0025 custody documents. Never reads secret values."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .engagement import AuthorizationError, Engagement
|
||||||
|
from .plane import IdentityHandle
|
||||||
|
|
||||||
|
CONTRACT_INTERFACE = "railiance.custody-projection-contract"
|
||||||
|
BROKER_INTERFACE = "railiance.custody-broker-readiness"
|
||||||
|
PROJECTION_INTERFACE = "railiance.custody-projection-receipt"
|
||||||
|
CLEANUP_INTERFACE = "railiance.custody-cleanup-receipt"
|
||||||
|
SCHEMA_FILES = (
|
||||||
|
"custody-projection-contract.schema.json",
|
||||||
|
"custody-broker-readiness.schema.json",
|
||||||
|
"custody-projection-receipt.schema.json",
|
||||||
|
"custody-cleanup-receipt.schema.json",
|
||||||
|
)
|
||||||
|
FORBIDDEN_KEYS = {
|
||||||
|
"token", "tokens", "password", "secret_value", "secret_values",
|
||||||
|
"bearer", "private_key", "credential",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def schema_dir() -> Path:
|
||||||
|
override = os.environ.get("WHITEHAT_CUSTODY_SCHEMA_DIR")
|
||||||
|
if override:
|
||||||
|
path = Path(override)
|
||||||
|
if path.is_dir():
|
||||||
|
return path
|
||||||
|
raise AuthorizationError("WHITEHAT_CUSTODY_SCHEMA_DIR is not a directory")
|
||||||
|
sibling = Path(__file__).resolve().parents[2].parent / "railiance-platform" / "schemas"
|
||||||
|
if sibling.is_dir():
|
||||||
|
return sibling
|
||||||
|
raise AuthorizationError("railiance custody schemas are not available")
|
||||||
|
|
||||||
|
|
||||||
|
def load_schemas() -> dict[str, dict[str, Any]]:
|
||||||
|
root = schema_dir()
|
||||||
|
loaded: dict[str, dict[str, Any]] = {}
|
||||||
|
for name in SCHEMA_FILES:
|
||||||
|
path = root / name
|
||||||
|
try:
|
||||||
|
loaded[name] = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as error:
|
||||||
|
raise AuthorizationError(f"custody schema unavailable: {name}") from error
|
||||||
|
if loaded[name].get("$schema") != "https://json-schema.org/draft/2020-12/schema":
|
||||||
|
raise AuthorizationError(f"custody schema {name} has the wrong $schema")
|
||||||
|
return loaded
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_value_safe(value: Any) -> None:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key, child in value.items():
|
||||||
|
if str(key).lower().replace("-", "_") in FORBIDDEN_KEYS:
|
||||||
|
raise AuthorizationError(f"custody document contains secret material: {key}")
|
||||||
|
_assert_value_safe(child)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for child in value:
|
||||||
|
_assert_value_safe(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _require(document: dict[str, Any], key: str) -> Any:
|
||||||
|
if key not in document:
|
||||||
|
raise AuthorizationError(f"custody document missing {key}")
|
||||||
|
return document[key]
|
||||||
|
|
||||||
|
|
||||||
|
def _timestamp(value: Any, field: str) -> datetime:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise AuthorizationError(f"{field} must be RFC3339")
|
||||||
|
try:
|
||||||
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
except ValueError as error:
|
||||||
|
raise AuthorizationError(f"{field} must be RFC3339") from error
|
||||||
|
if parsed.tzinfo is None:
|
||||||
|
raise AuthorizationError(f"{field} must include a timezone")
|
||||||
|
return parsed.astimezone(UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_projection_contract(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
_assert_value_safe(document)
|
||||||
|
if document.get("interface") != CONTRACT_INTERFACE or document.get("version") != 1:
|
||||||
|
raise AuthorizationError("unsupported projection contract")
|
||||||
|
if document.get("status") != "approved":
|
||||||
|
raise AuthorizationError("projection contract is not approved")
|
||||||
|
identities = document.get("identities")
|
||||||
|
if not isinstance(identities, list) or len(identities) != 2:
|
||||||
|
raise AuthorizationError("projection contract must name two identities")
|
||||||
|
roles = {item.get("role") for item in identities if isinstance(item, dict)}
|
||||||
|
handles = {item.get("handle") for item in identities if isinstance(item, dict)}
|
||||||
|
if roles != {"attacker", "owner"} or handles != {"token-a", "token-b"}:
|
||||||
|
raise AuthorizationError("projection contract identities are not attacker/owner token-a/token-b")
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def validate_projection_receipt(
|
||||||
|
document: dict[str, Any], *, contract: dict[str, Any] | None = None
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
_assert_value_safe(document)
|
||||||
|
if document.get("interface") != PROJECTION_INTERFACE or document.get("version") != 1:
|
||||||
|
raise AuthorizationError("unsupported projection receipt")
|
||||||
|
if document.get("state") != "projected":
|
||||||
|
raise AuthorizationError("projection receipt is not in projected state")
|
||||||
|
if document.get("secret_values_observed") is not False:
|
||||||
|
raise AuthorizationError("projection receipt did not prove secret values were unobserved")
|
||||||
|
if document.get("cleanup_authority") != "railiance-platform":
|
||||||
|
raise AuthorizationError("cleanup authority must be railiance-platform")
|
||||||
|
lease_id = _require(document, "lease_id")
|
||||||
|
if not isinstance(lease_id, str) or not lease_id.startswith("custody:"):
|
||||||
|
raise AuthorizationError("projection receipt lease_id is invalid")
|
||||||
|
identities = document.get("identities")
|
||||||
|
if not isinstance(identities, list) or len(identities) != 2:
|
||||||
|
raise AuthorizationError("projection receipt must name two identities")
|
||||||
|
roles = {item.get("role") for item in identities if isinstance(item, dict)}
|
||||||
|
if roles != {"attacker", "owner"}:
|
||||||
|
raise AuthorizationError("projection receipt must include attacker and owner")
|
||||||
|
if contract is not None:
|
||||||
|
validate_projection_contract(contract)
|
||||||
|
if document.get("engagement_id") != contract["engagement_id"]:
|
||||||
|
raise AuthorizationError("projection receipt engagement_id does not match contract")
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def validate_cleanup_receipt(
|
||||||
|
document: dict[str, Any], *, projection: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
_assert_value_safe(document)
|
||||||
|
if document.get("interface") != CLEANUP_INTERFACE or document.get("version") != 1:
|
||||||
|
raise AuthorizationError("unsupported cleanup receipt")
|
||||||
|
if document.get("state") != "cleaned":
|
||||||
|
raise AuthorizationError("cleanup receipt is not in cleaned state")
|
||||||
|
if document.get("secret_values_observed") is not False:
|
||||||
|
raise AuthorizationError("cleanup receipt did not prove secret values were unobserved")
|
||||||
|
if document.get("lease_id") != projection.get("lease_id"):
|
||||||
|
raise AuthorizationError("cleanup receipt lease_id does not match projection")
|
||||||
|
if document.get("engagement_id") != projection.get("engagement_id"):
|
||||||
|
raise AuthorizationError("cleanup receipt engagement_id does not match projection")
|
||||||
|
if document.get("projection_receipt_id") != projection.get("receipt_id"):
|
||||||
|
raise AuthorizationError("cleanup receipt is not bound to this projection receipt")
|
||||||
|
if document.get("target_ready") is not True:
|
||||||
|
raise AuthorizationError("cleanup receipt does not show a ready target")
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def validate_broker_readiness(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
_assert_value_safe(document)
|
||||||
|
if document.get("interface") != BROKER_INTERFACE or document.get("version") != 1:
|
||||||
|
raise AuthorizationError("unsupported broker readiness receipt")
|
||||||
|
if document.get("owner") != "whitehat-security":
|
||||||
|
raise AuthorizationError("broker readiness owner must be whitehat-security")
|
||||||
|
if document.get("secret_values_observed") is not False:
|
||||||
|
raise AuthorizationError("broker readiness receipt is not value-safe")
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformCustodyBroker:
|
||||||
|
"""Issue plane identity handles from a WP-0025 projection receipt."""
|
||||||
|
|
||||||
|
cleanup_request_supported = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
receipt: dict[str, Any],
|
||||||
|
*,
|
||||||
|
contract: dict[str, Any] | None = None,
|
||||||
|
cleanup: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.receipt = validate_projection_receipt(receipt, contract=contract)
|
||||||
|
self.contract = validate_projection_contract(contract) if contract is not None else None
|
||||||
|
self.cleanup = (
|
||||||
|
validate_cleanup_receipt(cleanup, projection=self.receipt) if cleanup is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(
|
||||||
|
cls,
|
||||||
|
receipt_path: str | Path,
|
||||||
|
*,
|
||||||
|
contract_path: str | Path | None = None,
|
||||||
|
cleanup_path: str | Path | None = None,
|
||||||
|
) -> "PlatformCustodyBroker":
|
||||||
|
receipt = json.loads(Path(receipt_path).read_text(encoding="utf-8"))
|
||||||
|
contract = (
|
||||||
|
json.loads(Path(contract_path).read_text(encoding="utf-8"))
|
||||||
|
if contract_path is not None else None
|
||||||
|
)
|
||||||
|
cleanup = (
|
||||||
|
json.loads(Path(cleanup_path).read_text(encoding="utf-8"))
|
||||||
|
if cleanup_path is not None else None
|
||||||
|
)
|
||||||
|
return cls(receipt, contract=contract, cleanup=cleanup)
|
||||||
|
|
||||||
|
def project(self, engagement: Engagement, registration: dict[str, Any]
|
||||||
|
) -> tuple[IdentityHandle, ...]:
|
||||||
|
if self.receipt.get("engagement_id") != engagement.raw["engagement_id"]:
|
||||||
|
raise AuthorizationError("projection receipt engagement_id does not match")
|
||||||
|
expiry = _timestamp(self.receipt["expires_at"], "expires_at")
|
||||||
|
if datetime.now(UTC) > expiry:
|
||||||
|
raise AuthorizationError("projection receipt has expired")
|
||||||
|
count = int(registration["identities"]["count"])
|
||||||
|
identities = self.receipt["identities"]
|
||||||
|
if len(identities) != count:
|
||||||
|
raise AuthorizationError("projection receipt identity count does not match registration")
|
||||||
|
handles = []
|
||||||
|
for item in sorted(identities, key=lambda row: str(row.get("handle"))):
|
||||||
|
role = item["role"]
|
||||||
|
mount_path = item["mount_path"]
|
||||||
|
if not str(mount_path).startswith("/var/run/secrets/whitehat/"):
|
||||||
|
raise AuthorizationError("identity mount_path is outside the whitehat mount")
|
||||||
|
handles.append(IdentityHandle(role, mount_path, self.receipt["lease_id"],
|
||||||
|
self.receipt["expires_at"]))
|
||||||
|
return tuple(handles)
|
||||||
|
|
||||||
|
def revoke(self, lease_id: str) -> None:
|
||||||
|
if self.cleanup is None:
|
||||||
|
raise AuthorizationError(
|
||||||
|
"receipt broker does not hold credentials; custody must revoke the projection"
|
||||||
|
)
|
||||||
|
if self.cleanup["lease_id"] != lease_id:
|
||||||
|
raise AuthorizationError("cleanup receipt lease_id does not match plane lease")
|
||||||
|
|
||||||
|
|
||||||
|
def broker_from_receipt(
|
||||||
|
path: str | Path, *, contract_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:
|
||||||
|
return PlatformCustodyBroker.load(path, contract_path=contract_path)
|
||||||
|
from .plane import ReceiptBroker
|
||||||
|
return ReceiptBroker(data)
|
||||||
245
tests/test_platform_custody_adapter.py
Normal file
245
tests/test_platform_custody_adapter.py
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
import json
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
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,
|
||||||
|
validate_projection_receipt,
|
||||||
|
)
|
||||||
|
from whitehat_security.plane import KillSwitch, admit
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 8, 22, 12, tzinfo=UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def contract() -> dict:
|
||||||
|
return {
|
||||||
|
"interface": CONTRACT_INTERFACE,
|
||||||
|
"version": 1,
|
||||||
|
"workplan_id": "RAILIANCE-WP-0025",
|
||||||
|
"engagement_id": "WH-ENG-FIXTURE-1",
|
||||||
|
"status": "approved",
|
||||||
|
"engagement_contract_sha256": "1" * 64,
|
||||||
|
"target": {
|
||||||
|
"id": "audit-core",
|
||||||
|
"namespace": "audit-core",
|
||||||
|
"deployment": "audit-core",
|
||||||
|
"container": "audit-core",
|
||||||
|
"sender_external_secret": "audit-core-senders",
|
||||||
|
"revision": "a" * 40,
|
||||||
|
"image_digest": "sha256:" + "b" * 64,
|
||||||
|
"contract_sha256": "c" * 64,
|
||||||
|
},
|
||||||
|
"runner": {
|
||||||
|
"namespace": "whitehat",
|
||||||
|
"service_account": "whitehat-runner",
|
||||||
|
"secret_name": "whitehat-e2-audit-credentials",
|
||||||
|
"mount_root": "/var/run/secrets/whitehat",
|
||||||
|
"manifest_sha256": "d" * 64,
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"starts_at": "2026-08-22T12:00:00Z",
|
||||||
|
"projection_cutoff": "2026-08-22T12:03:00Z",
|
||||||
|
"expires_at": "2099-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"remote": "railiance01",
|
||||||
|
"registry_path": "platform/workloads/audit-core/senders",
|
||||||
|
"registry_field": "senders.json",
|
||||||
|
"kv_mount": "platform",
|
||||||
|
"kv_prefix": "engagements/WH-ENG-FIXTURE-1/audit-core",
|
||||||
|
"eso_service_account": "external-secrets",
|
||||||
|
"eso_namespace": "external-secrets",
|
||||||
|
},
|
||||||
|
"identities": [
|
||||||
|
{
|
||||||
|
"handle": "token-a",
|
||||||
|
"role": "attacker",
|
||||||
|
"sender_name": "whitehat-e2-a",
|
||||||
|
"tenant": "tenant-a",
|
||||||
|
"mount_path": "/var/run/secrets/whitehat/token-a",
|
||||||
|
"may_read": True,
|
||||||
|
"may_write": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": "token-b",
|
||||||
|
"role": "owner",
|
||||||
|
"sender_name": "whitehat-e2-b",
|
||||||
|
"tenant": "tenant-b",
|
||||||
|
"mount_path": "/var/run/secrets/whitehat/token-b",
|
||||||
|
"may_read": True,
|
||||||
|
"may_write": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
return {
|
||||||
|
"interface": "railiance.custody-broker-readiness",
|
||||||
|
"version": 1,
|
||||||
|
"workplan_id": "RAILIANCE-WP-0025",
|
||||||
|
"owner": "whitehat-security",
|
||||||
|
"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,
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
"required_roles": ["attacker", "owner"],
|
||||||
|
"mount_paths": [
|
||||||
|
"/var/run/secrets/whitehat/token-a",
|
||||||
|
"/var/run/secrets/whitehat/token-b",
|
||||||
|
],
|
||||||
|
"adapter": {
|
||||||
|
"repo": "whitehat-security",
|
||||||
|
"revision": "a" * 40,
|
||||||
|
"path": "src/whitehat_security/platform_custody.py",
|
||||||
|
"sha256": "b" * 64,
|
||||||
|
"tests_passed": True,
|
||||||
|
},
|
||||||
|
"cleanup_request_supported": 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",
|
||||||
|
"target_id": "audit-core", "target_owner": "audit-core",
|
||||||
|
"environment": "build", "source": "runner", "approval_class": "live-e2",
|
||||||
|
"plane_namespace": "whitehat", "runner_image_digest": "sha256:abc",
|
||||||
|
"routes": ["POST /v1/events"], "fixture_ids": ["a", "b"],
|
||||||
|
"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",
|
||||||
|
"abort_contact": "operator", "posture_claim": "E2",
|
||||||
|
"attacker_model": "E2-authenticated-tenant-a",
|
||||||
|
"finding_destination": "risk-nexus",
|
||||||
|
"target_owner_acknowledged_at": "2026-08-22T11:01:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_four_custody_schemas_are_published():
|
||||||
|
schemas = load_schemas()
|
||||||
|
assert set(schemas) == {
|
||||||
|
"custody-projection-contract.schema.json",
|
||||||
|
"custody-broker-readiness.schema.json",
|
||||||
|
"custody-projection-receipt.schema.json",
|
||||||
|
"custody-cleanup-receipt.schema.json",
|
||||||
|
}
|
||||||
|
assert "custody-projection-contract" in schemas["custody-projection-contract.schema.json"]["$id"]
|
||||||
|
assert "custody-broker-readiness" in schemas["custody-broker-readiness.schema.json"]["$id"]
|
||||||
|
assert "custody-projection-receipt" in schemas["custody-projection-receipt.schema.json"]["$id"]
|
||||||
|
assert "custody-cleanup-receipt" in schemas["custody-cleanup-receipt.schema.json"]["$id"]
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapter_rejects_secret_material():
|
||||||
|
tainted = projection_receipt()
|
||||||
|
tainted["token"] = "never"
|
||||||
|
with pytest.raises(AuthorizationError, match="secret material"):
|
||||||
|
validate_projection_receipt(tainted)
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
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"),
|
||||||
|
broker=broker,
|
||||||
|
kill_switch=KillSwitch(tmp_path / "KILL"),
|
||||||
|
now=NOW,
|
||||||
|
retired=set(),
|
||||||
|
)
|
||||||
|
assert {handle.role for handle in lease.identities} == {"attacker", "owner"}
|
||||||
|
assert lease.lease_id.startswith("custody:")
|
||||||
|
with pytest.raises(AuthorizationError, match="custody must revoke"):
|
||||||
|
broker.revoke(lease.lease_id)
|
||||||
|
cleaned = PlatformCustodyBroker(
|
||||||
|
projection_receipt(), contract=contract(), cleanup=cleanup_receipt()
|
||||||
|
)
|
||||||
|
cleaned.revoke(lease.lease_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_receipt_must_match_lease():
|
||||||
|
with pytest.raises(AuthorizationError, match="lease_id"):
|
||||||
|
bad = cleanup_receipt()
|
||||||
|
bad["lease_id"] = "custody:" + "0" * 32
|
||||||
|
validate_cleanup_receipt(bad, projection=projection_receipt())
|
||||||
38
workplans/WHITEHAT-WP-0003-platform-custody-adapter.md
Normal file
38
workplans/WHITEHAT-WP-0003-platform-custody-adapter.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
id: WHITEHAT-WP-0003
|
||||||
|
type: workplan
|
||||||
|
title: "Consume the Railiance WP-0025 custody broker interface"
|
||||||
|
domain: infotech
|
||||||
|
repo: whitehat-security
|
||||||
|
status: finished
|
||||||
|
owner: net-kingdom
|
||||||
|
topic_slug: whitehat-security
|
||||||
|
created: "2026-08-22"
|
||||||
|
updated: "2026-08-22"
|
||||||
|
---
|
||||||
|
|
||||||
|
# WHITEHAT-WP-0003 — platform custody adapter
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement the consumer Railiance asked for after `-02` aborted: a value-safe
|
||||||
|
adapter at `src/whitehat_security/platform_custody.py` that understands the
|
||||||
|
four WP-0025 custody schemas and can issue a plane lease from a projection
|
||||||
|
receipt without seeing token values.
|
||||||
|
|
||||||
|
This plan authorizes no engagement, runner, credential, or traffic.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### T01 — Schema-bound adapter and focused tests
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: WHITEHAT-WP-0003-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Adapter plus `tests/test_platform_custody_adapter.py`. Cleanup receipts can
|
||||||
|
acknowledge a lease. Secret material is refused. `admit-plane --receipt`
|
||||||
|
selects this adapter when the document uses
|
||||||
|
`railiance.custody-projection-receipt`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue