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

@ -69,8 +69,10 @@ boundary always holds.
- `T01` is complete: the rules of engagement were accepted on 2026-08-21.
- `T02` is complete: the per-axis attacker model is recorded in
`docs/attacker-model.md`.
- `T03` is in progress: `WH-ENG-20260822-AUDIT-E2-01` expired at 18:15Z with
no live run. A new engagement ID is required for a retry.
- `T03` is in progress: `WH-ENG-20260822-AUDIT-E2-01` expired unused;
`WH-ENG-20260822-AUDIT-E2-02` aborted after projection because admission
had no receipt adapter. Zero packets. A later live run needs a new ID and
`admit-plane --receipt`.
- `T04` is complete for every applicable E2 probe: generic and audit-core
shaped fixtures fail known-bad and pass known-good in-process.
- `T05` is in progress: 24-hour cadence, offline evaluator and in-process

View file

@ -50,7 +50,7 @@ retired.
```sh
PYTHONPATH=src python3 -m whitehat_security.cli validate-engagement engagements/<record>.json
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json --receipt <custody-receipt.json>
```
7. Confirm target identity/revision and probe attribution before the first

View file

@ -16,10 +16,10 @@ A live E2 run is admitted only when every item below is true:
5. The runner namespace is `whitehat`, the image digest is pinned, and
engagement labels are present. Live E3 also names a `database`. Live
capacity also names an `aggressor_ceiling`.
6. The custody broker projected the registered number of identity *handles*
(two for E2, one runtime for E3, none for capacity fixtures). Credential
values are not returned to the operator or agent. Live classes fail closed
while the custody broker is unconnected.
6. A value-safe custody **receipt** is supplied to `admit-plane --receipt`.
The receipt names handles, mount keys, expiry and `secret_values_observed:
false`. It must not contain token values. Without a receipt, live admission
fails closed and requests no credential.
7. The kill switch is clear.
8. The rate watcher is attached. Cleanup revokes the lease.
@ -38,7 +38,7 @@ requested.
```sh
PYTHONPATH=src python3 -m whitehat_security.cli validate-targets targets
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json
PYTHONPATH=src python3 -m whitehat_security.cli admit-plane engagements/<record>.json targets/<target>.json --receipt <custody-receipt.json>
PYTHONPATH=src python3 -m whitehat_security.cli kill-switch
PYTHONPATH=src python3 -m whitehat_security.cli deliver evidence/<run>.json --outbox outbox
```
@ -47,7 +47,8 @@ Create `plane/KILL` to abort independently of the runner process.
## Retired identifiers
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`, and
`WH-ENG-20260822-AUDIT-E2-01` are terminal. The last of those expired at
18:15Z with no projection-ready notice and zero packets. A later live E2 run
needs a new ID, a new window, and a fresh attended projection.
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`,
`WH-ENG-20260822-AUDIT-E2-01`, and `WH-ENG-20260822-AUDIT-E2-02` are terminal.
`-02` projected and then aborted: admission had no receipt adapter, so the
runner sent zero packets. A later live E2 run needs a new ID, a new window,
a custody receipt, and `admit-plane --receipt`.

View file

@ -1,6 +1,8 @@
{
"engagement_id": "WH-ENG-20260822-AUDIT-E2-02",
"status": "approved",
"status": "aborted",
"aborted_at": "2026-08-22T19:21:39Z",
"abort_reason": "Attended projection succeeded at 19:17:54Z and the runner became Ready, but admit-plane failed closed: no live adapter consumed the value-safe custody receipt. Zero packets. Runner deleted 19:21:07Z; custody cleanup 19:21:39Z. Not E2 evidence. Identifier is terminal.",
"proposal_at": "2026-08-22T18:36:50Z",
"proposal_reason": "Fresh attended retry after WH-ENG-20260822-AUDIT-E2-01 expired with zero packets. The terminal identifier, identities and fixtures are not reused.",
"authorization_id": "operator-session-2026-08-22-e2-02-approval",

View file

@ -1,21 +1,13 @@
# WH-ENG-20260822-AUDIT-E2-02
Status: **approved and waiting**. The engagement is fail-closed until
`2026-08-22T19:15:00Z` and until custody reports projection ready.
Status: **aborted**. Not E2 evidence.
| Gate | State |
| --- | --- |
| Operator approval | `2026-08-22T18:42:50Z` in the coordinating session |
| Owner acknowledgement | `2026-08-22T18:45:39Z` after direct artifact review |
| Foundational plane | applied by ops-mason `c26a6e5` |
| Window | `2026-08-22T19:15:00Z``19:30:00Z` |
| Image | `sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6` |
| Custody projection | pending; do not mint before 19:15Z |
| Runner pod | not applied |
| Packets | none |
Attended projection succeeded at `2026-08-22T19:17:54Z` and the runner pod
became Ready. Target validation, engagement validation and the kill switch
passed. `admit-plane` then failed closed because this repository had no
adapter for a value-safe custody receipt. The runner was never invoked.
Zero packets. Runner deleted `19:21:07Z`; custody cleanup `19:21:39Z`.
Identifier is terminal.
Offline preflight must fail closed before the window and until the live custody
broker confirms the two exact mounted identities.
Invocation, including required `--forged-event whitehat-e2-forged-b-20260822-02`:
`engagements/runtime/WH-ENG-20260822-AUDIT-E2-02-invocation.md`.
The fix is `admit-plane --receipt <projection-receipt.json>`, which issues a
plane lease from handles only.

View file

@ -14,6 +14,8 @@ from `whitehat admit-plane`. `WH-ENG-20260821-AUDIT-E2` and
`WH-ENG-20260821-TENANT-E2` are cancelled and must not be reused. The pod
manifest under `runtime/` is bound to a cancelled ID and must not be applied.
`WH-ENG-20260822-AUDIT-E2-01` expired at `2026-08-22T18:15:00Z` with no live
run. Do not reuse it. A later attempt needs a new identifier and window.
`WH-ENG-20260822-AUDIT-E2-01` expired unused. `WH-ENG-20260822-AUDIT-E2-02`
aborted after projection because `admit-plane` had no receipt adapter; zero
packets. Neither identifier may be reused. Live admission requires
`--receipt` of a value-safe custody projection.

View file

@ -3,8 +3,8 @@
This repository does not apply cluster objects, request credentials, or send
packets. `ops-mason` provisions the foundational plane from these files
**before** a fresh engagement is proposed. Cancelled or expired IDs
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`, and
`WH-ENG-20260822-AUDIT-E2-01` are terminal.
`WH-ENG-20260821-AUDIT-E2`, `WH-ENG-20260821-TENANT-E2`,
`WH-ENG-20260822-AUDIT-E2-01`, and `WH-ENG-20260822-AUDIT-E2-02` are terminal.
## Ordering

View file

@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "whitehat-projection-receipt/v1",
"type": "object",
"additionalProperties": false,
"required": [
"engagement_id", "projected_at", "expires_at", "identities",
"mounted_secret", "mounted_keys", "target_ready", "secret_values_observed"
],
"properties": {
"engagement_id": {"type": "string", "minLength": 1},
"projected_at": {"type": "string", "format": "date-time"},
"expires_at": {"type": "string", "format": "date-time"},
"identities": {"type": "array", "minItems": 1, "items": {"type": "string"}},
"mounted_secret": {"type": "string", "pattern": "^whitehat/"},
"mounted_keys": {"type": "array", "items": {"type": "string"}},
"target_image_matches": {"type": "boolean"},
"target_ready": {"const": true},
"secret_values_observed": {"const": false}
}
}

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."""

View file

@ -1,5 +1,6 @@
import json
from datetime import UTC, datetime
from pathlib import Path
import pytest
@ -63,8 +64,13 @@ def test_pending_owner_acknowledgement_fails_closed(tmp_path):
load(tmp_path, data)
def test_retry_record_is_authorized_only_inside_its_window():
path = "engagements/2026-08-22-audit-core-e2-02.json"
def test_retry_record_is_authorized_only_inside_its_window(tmp_path):
data = json.loads(Path("engagements/2026-08-22-audit-core-e2-02.json").read_text())
data["status"] = "approved"
data.pop("aborted_at", None)
data.pop("abort_reason", None)
path = tmp_path / "retry.json"
path.write_text(json.dumps(data), encoding="utf-8")
with pytest.raises(AuthorizationError, match="has not started"):
Engagement.load(path, now=datetime(2026, 8, 22, 19, 14, tzinfo=UTC))
engagement = Engagement.load(path, now=datetime(2026, 8, 22, 19, 15, tzinfo=UTC))
@ -73,6 +79,14 @@ def test_retry_record_is_authorized_only_inside_its_window():
Engagement.load(path, now=datetime(2026, 8, 22, 19, 31, tzinfo=UTC))
def test_aborted_record_fails_closed():
with pytest.raises(AuthorizationError, match="aborted"):
Engagement.load(
"engagements/2026-08-22-audit-core-e2-02.json",
now=datetime(2026, 8, 22, 19, 20, tzinfo=UTC),
)
def test_elapsed_record_fails_closed_even_inside_old_window(tmp_path):
data = record()
data["status"] = "expired"

View file

@ -10,7 +10,7 @@ from whitehat_security.fixtures import FixtureService, probe_suite
from whitehat_security.model import RunReport
from whitehat_security.e3 import e3_calibration
from whitehat_security.plane import (
KillSwitch, LocalBroker, RateWatcher, UnconnectedCustodyBroker,
KillSwitch, LocalBroker, RateWatcher, ReceiptBroker, UnconnectedCustodyBroker,
admit, cleanup, retired_ids,
)
from whitehat_security.reporting import queue_risk_nexus
@ -71,6 +71,7 @@ def test_retired_ids_include_cancelled_records():
assert "WH-ENG-20260821-AUDIT-E2" in ids
assert "WH-ENG-20260821-TENANT-E2" in ids
assert "WH-ENG-20260822-AUDIT-E2-01" in ids
assert "WH-ENG-20260822-AUDIT-E2-02" in ids
def test_fixture_plane_admits_and_projects_handles_without_secrets(tmp_path):
@ -165,6 +166,63 @@ def test_unconnected_broker_requests_no_credential(tmp_path):
)
def test_receipt_broker_issues_lease_without_secret_values(tmp_path):
engagement = load_engagement(tmp_path, fixture_record(
approval_class="live-e2", environment="build",
target_id="audit-core",
routes=["POST /v1/events"],
plane_namespace="whitehat",
runner_image_digest="sha256:abc",
))
receipt = {
"engagement_id": "WH-ENG-FIXTURE-1",
"projected_at": "2026-08-22T12:00:00Z",
"expires_at": "2099-01-01T00:00:00Z",
"identities": ["whitehat-e2-a", "whitehat-e2-b"],
"mounted_secret": "whitehat/whitehat-e2-audit-credentials",
"mounted_keys": ["token-a", "token-b"],
"target_ready": True,
"secret_values_observed": False,
}
lease = admit(
engagement=engagement, registration=load_registration("targets/audit-core-e2.json"),
broker=ReceiptBroker(receipt), kill_switch=KillSwitch(tmp_path / "KILL"),
now=NOW, retired=set(),
)
assert {handle.role for handle in lease.identities} == {"owner", "attacker"}
rendered = repr(lease) + repr(lease.identities) + json.dumps(receipt)
assert "token_urlsafe" not in rendered
with pytest.raises(AuthorizationError, match="custody must revoke"):
cleanup(lease, ReceiptBroker(receipt))
def test_receipt_broker_rejects_secret_material(tmp_path):
engagement = load_engagement(tmp_path, fixture_record(
approval_class="live-e2", environment="build",
target_id="audit-core",
routes=["POST /v1/events"],
plane_namespace="whitehat",
runner_image_digest="sha256:abc",
))
receipt = {
"engagement_id": "WH-ENG-FIXTURE-1",
"projected_at": "2026-08-22T12:00:00Z",
"expires_at": "2099-01-01T00:00:00Z",
"identities": ["whitehat-e2-a", "whitehat-e2-b"],
"mounted_secret": "whitehat/whitehat-e2-audit-credentials",
"mounted_keys": ["token-a", "token-b"],
"target_ready": True,
"secret_values_observed": False,
"token": "must-not-appear",
}
with pytest.raises(AuthorizationError, match="secret material"):
admit(
engagement=engagement, registration=load_registration("targets/audit-core-e2.json"),
broker=ReceiptBroker(receipt), kill_switch=KillSwitch(tmp_path / "KILL"),
now=NOW, retired=set(),
)
def test_e3_is_not_admitted_by_the_e2_plane(tmp_path):
engagement = load_engagement(tmp_path, fixture_record(
approval_class="e3", techniques=["e3-rls"],

View file

@ -163,8 +163,10 @@ target request and are retired. ops-mason applied the foundational plane on
2026-08-22T09:52:55Z; audit-core acknowledged at 2026-08-22T10:04:56Z
(`32afceb`) after the fixture/digest correction `1418dac`. Offline preflight
recorded a window that elapsed at 18:15Z with no projection-ready notice and
zero packets. That identifier is expired and must not be reused. A later live
E2 run needs a new ID, a new window, and a fresh attended projection.
zero packets. `WH-ENG-20260822-AUDIT-E2-02` projected at 19:17:54Z and the
runner became Ready, then `admit-plane` failed closed because no receipt
adapter existed; zero packets, cleanup complete 19:21:39Z. Both identifiers
are terminal. Live admission now consumes a value-safe custody receipt.
Whitehat will not relabel tenant-engine to finish this task.
### T04 — Prove the probes fail