Admit live E2 from a value-safe custody receipt

WH-ENG-20260822-AUDIT-E2-02 projected and then aborted: admit-plane had no
receipt adapter, so the runner sent zero packets. Consume custody receipts
as handles only, keep unconnected admission fail-closed, and retire -02.

Assistant: grok
Assistant-Session: 01a02670-3345-76f2-a014-70fde8e2a2bb
This commit is contained in:
tegwick 2026-08-22 21:31:49 +02:00
parent b6c1806680
commit 45548e44a2
14 changed files with 221 additions and 42 deletions

View file

@ -13,7 +13,7 @@ from .e3 import CADENCE, PROBES, e3_calibration
from .engagement import AuthorizationError, Engagement
from .fixtures import FixtureService, probe_suite
from .model import RunReport, utc_now
from .plane import KillSwitch, admit, default_broker, retired_ids
from .plane import KillSwitch, ReceiptBroker, admit, default_broker, retired_ids
from .reporting import queue_risk_nexus, risk_nexus_message
from .targets import load_catalog, load_registration
@ -90,6 +90,7 @@ def main(argv: list[str] | None = None) -> None:
admit_plane = commands.add_parser("admit-plane")
admit_plane.add_argument("engagement")
admit_plane.add_argument("registration")
admit_plane.add_argument("--receipt", help="value-safe custody projection receipt")
commands.add_parser("kill-switch")
deliver = commands.add_parser("deliver")
deliver.add_argument("report")
@ -138,8 +139,11 @@ def main(argv: list[str] | None = None) -> None:
try:
record = Engagement.load(args.engagement)
registration = load_registration(args.registration)
broker = (
ReceiptBroker.load(args.receipt) if args.receipt else default_broker(record)
)
lease = admit(engagement=record, registration=registration,
broker=default_broker(record), kill_switch=KillSwitch(),
broker=broker, kill_switch=KillSwitch(),
retired=retired_ids())
except (AuthorizationError, OSError, ValueError, json.JSONDecodeError) as error:
print(f"not authorized: {error}", file=sys.stderr)

View file

@ -43,6 +43,10 @@ class Engagement:
raise AuthorizationError(
"engagement window elapsed with no live run; identifier must not be reused"
)
if self.raw.get("status") == "aborted":
raise AuthorizationError(
"engagement aborted without target evidence; identifier must not be reused"
)
if self.raw.get("status") == "proposed":
raise AuthorizationError(
"engagement is proposed; operator approval and owner acknowledgement are pending"

View file

@ -16,6 +16,7 @@ TERMINAL_IDS = frozenset({
"WH-ENG-20260821-AUDIT-E2",
"WH-ENG-20260821-TENANT-E2",
"WH-ENG-20260822-AUDIT-E2-01",
"WH-ENG-20260822-AUDIT-E2-02",
})
APPROVAL_CLASSES = {
"fixture-e2": frozenset({"e2-differential"}),
@ -42,7 +43,7 @@ def retired_ids(engagements_dir: str | Path | None = None) -> set[str]:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError, ValueError):
continue
if data.get("status") in {"cancelled", "expired"} and data.get("engagement_id"):
if data.get("status") in {"cancelled", "expired", "aborted"} and data.get("engagement_id"):
retired.add(data["engagement_id"])
return retired
@ -113,6 +114,82 @@ class CredentialBroker(Protocol):
...
FORBIDDEN_RECEIPT_KEYS = {
"token", "token_a", "token_b", "password", "secret", "secret_value",
"bearer", "credential", "value", "senders.json",
}
def _receipt_contains_secret_material(value: Any) -> bool:
if isinstance(value, dict):
for key, child in value.items():
lowered = str(key).lower().replace("-", "_")
if lowered in FORBIDDEN_RECEIPT_KEYS:
return True
if _receipt_contains_secret_material(child):
return True
return False
if isinstance(value, list):
return any(_receipt_contains_secret_material(item) for item in value)
return False
class ReceiptBroker:
"""Consume a value-safe custody receipt. Never reads or stores secret bytes."""
def __init__(self, receipt: dict[str, Any]) -> None:
if _receipt_contains_secret_material(receipt):
raise AuthorizationError("projection receipt contains secret material")
if receipt.get("secret_values_observed") is not False:
raise AuthorizationError("projection receipt did not prove secret values were unobserved")
self.receipt = receipt
@classmethod
def load(cls, path: str | Path) -> "ReceiptBroker":
return cls(json.loads(Path(path).read_text(encoding="utf-8")))
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")
if not self.receipt.get("target_ready"):
raise AuthorizationError("projection receipt does not show a ready target")
expires_at = self.receipt.get("expires_at")
if not expires_at:
raise AuthorizationError("projection receipt missing expires_at")
expiry = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00"))
if expiry.tzinfo is None:
raise AuthorizationError("projection receipt expiry must include a timezone")
if datetime.now(UTC) > expiry.astimezone(UTC):
raise AuthorizationError("projection receipt has expired")
keys = list(self.receipt.get("mounted_keys") or [])
count = int(registration["identities"]["count"])
if count == 2 and keys != ["token-a", "token-b"]:
raise AuthorizationError("E2 receipt must mount exactly token-a and token-b")
names = list(self.receipt.get("identities") or [])
if len(names) != count:
raise AuthorizationError("projection receipt identity count does not match registration")
secret = str(self.receipt.get("mounted_secret") or "")
if not secret.startswith("whitehat/"):
raise AuthorizationError("projection receipt must mount in namespace whitehat")
lease_id = f"{engagement.raw['engagement_id']}:{self.receipt.get('projected_at', '')}"
if count == 2:
return (
IdentityHandle("owner", "/var/run/secrets/whitehat/token-b", lease_id, expires_at),
IdentityHandle("attacker", "/var/run/secrets/whitehat/token-a", lease_id, expires_at),
)
if count == 1:
return (
IdentityHandle("runtime", "/var/run/secrets/whitehat/runtime", lease_id, expires_at),
)
return ()
def revoke(self, lease_id: str) -> None:
raise AuthorizationError(
"receipt broker does not hold credentials; custody must revoke the projection"
)
class UnconnectedCustodyBroker:
"""Fail-closed live broker. It never requests or returns credential values."""