RPF-WP-0064-T10: break-glass drill mode for the isolated restore adapter

Adds --mode break-glass-drill with its own signed admission (drill:
"break-glass", attempt id RPF-WP-0064-T10-*, snapshot_label). After the
phase A restore and unseal it checks the held root token with lookup-self
only, runs a fresh generate-root ceremony with re-entered shares, does one
read-only sys/auth action, revokes the generated token (204 then 403) and
drills a cancelled attempt. Phase A is unchanged. The verifier types the
receipt extension and rejects token, accessor, OTP, nonce, encoded-token
and share shapes. Offline 34 and native 16 pass. Founder runbooks for the
drill and the read-only production SSO-down walk are in the T10 section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4515@bnt-lap001
Assistant-Session: 0bf96127-6e04-41be-bfda-d11717c3f354
This commit is contained in:
codex 2026-10-28 00:02:10 +01:00
parent 05ff9f8678
commit a2da4085e0
11 changed files with 867 additions and 49 deletions

View file

@ -47,10 +47,11 @@ def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
for name in ("admission", "admission-sig", "admission-sha256", "ciphertext", "receipt"):
parser.add_argument("--" + name, required=True)
parser.add_argument("--mode", choices=(adapter.PHASE_A, adapter.DRILL_MODE), default=adapter.PHASE_A)
opts = parser.parse_args(argv)
try:
adm, _raw, _signer, _protected = adapter.load_admission(
Path(opts.admission), Path(opts.admission_sig), opts.admission_sha256, adapter.anchor_path())
Path(opts.admission), Path(opts.admission_sig), opts.admission_sha256, adapter.anchor_path(), opts.mode)
require(adm["material"] == "original", "admission_refused")
adapter.check_window(adm)
adapter.check_source_closure(adm, adapter.ROOT)

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Isolated original-OpenBao restore adapter (RPF-WP-0064-T02, phase A only).
"""Isolated original-OpenBao restore adapter (RPF-WP-0064-T02 phase A; T10 break-glass drill).
Restores one age-encrypted Raft snapshot into a disposable, networkless Docker
container on the founder workstation, unseals it with the original Shamir
@ -22,6 +22,15 @@ Outcomes: ``restored`` | ``refused`` (required object missing or not matching
its source; never repaired) | ``aborted`` (any other failed check, timeout,
operator abort). Only the launcher ``run-isolated-restore.sh`` is the attended
entry point; ``Session`` is importable for the synthetic rehearsal tests.
``--mode break-glass-drill`` (RPF-WP-0064-T10, NK-WP-0055-T13) needs its own
signed admission (``drill: "break-glass"``, attempt id ``RPF-WP-0064-T10-*``).
It runs the same restore and unseal, forgets the unseal shares, then: checks a
held root token with ``lookup-self`` only (never revoked or otherwise used);
runs a fresh generate-root ceremony with a server OTP and two re-entered
shares; does one read-only action (``sys/auth``); revokes the generated token
(204) and confirms ``lookup-self`` is refused (403); starts and cancels one
more attempt. Outcomes: ``drilled`` | ``aborted``. No object readback.
"""
from __future__ import annotations
@ -127,13 +136,27 @@ ALLOWED_READS = frozenset({"sys/seal-status", "sys/leader", "auth/token/lookup-s
*(o["path"] for o in OBJECTS)})
ALLOWED_WRITES = frozenset({"sys/unseal", "sys/generate-root/attempt", "sys/generate-root/update",
"auth/token/revoke-self"})
# --------------------------------------------------------------------------- T10 break-glass drill
PHASE_A, DRILL_MODE = "phase-a", "break-glass-drill"
DRILL = "break-glass"
GENERATE_ROOT_ATTEMPT = "sys/generate-root/attempt"
MINIMAL_ACTION_PATH = "sys/auth"
# T10 extensions: generate-root status (GET) and cancel (DELETE); sys/auth and
# auth/token/lookup-self are already allowed reads, sys/generate-root/update an allowed write.
ALLOWED_READS = ALLOWED_READS | {GENERATE_ROOT_ATTEMPT}
ALLOWED_DELETES = frozenset({GENERATE_ROOT_ATTEMPT})
DRILL_READBACK = "generate-root-from-reentered-shares"
DRILL_CLAIMS_NOT_MADE = ("production_root_token_validity", "production_generate_root_availability",
"sso_down_access_path")
DRILL_ABORT_REASONS = frozenset({"held_token_refused", "generate_root_failed", "minimal_action_failed",
"revocation_failed", "cancel_drill_failed"})
ABORT_REASONS = frozenset({
"admission_refused", "preflight_refused", "custody_identity_refused", "ciphertext_mismatch",
"decrypt_failed", "plaintext_mismatch", "target_start_failed", "network_containment_failed",
"throwaway_init_failed", "restore_failed", "unseal_refused", "leader_not_elected", "cluster_mismatch",
"applied_index_behind", "readback_credential_failed", "readback_failed", "deadline_exceeded",
"operator_abort", "parent_lost", "internal_error",
})
}) | DRILL_ABORT_REASONS
# Production runs the chart with setNodeId, so the snapshot's voter is the pod
# name. The target takes the same id; a native rehearsal showed a different id
# still elects a leader after a forced restore, so this is alignment, not the
@ -167,6 +190,12 @@ AGE_IDENTITY_RE = re.compile(r"^AGE-SECRET-KEY-1[0-9A-Z]{58}$")
AGE_RECIPIENT_RE = re.compile(r"^age1[023456789acdefghjklmnpqrstuvwxyz]{58}$")
SHARE_RE = re.compile(r"^(?:[A-Za-z0-9+/]{40,100}={0,2}|[a-f0-9]{60,130})$")
ATTEMPT_RE = re.compile(r"^RPF-WP-0064-(?:T07|T04)-[A-Za-z0-9-]{3,64}$")
DRILL_ATTEMPT_RE = re.compile(r"^RPF-WP-0064-T10-[A-Za-z0-9-]{3,64}$")
# Snapshot name for the claim limit, e.g. openbao-raft-20261014T1532Z.snap.age.
SNAPSHOT_LABEL_RE = re.compile(r"^(?=.*20\d{6})[a-z0-9][a-z0-9._-]{2,95}$")
HELD_TOKEN_RE = re.compile(r"^[A-Za-z0-9._-]{16,256}$")
POLICY_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,63}$")
HTTP_CODE_RE = re.compile(rb"Code: (\d{3})\.")
COMMIT_RE = re.compile(r"^[a-f0-9]{40}$")
DURATION_RE = re.compile(r"^(\d+)([smh]?)$")
@ -229,14 +258,23 @@ SNAPSHOT_FIELDS = {"ciphertext_sha256", "ciphertext_bytes", "plaintext_sha256",
"source_cluster_id", "source_applied_index", "source_receipt_sha256"}
def validate_admission(adm: Any) -> dict[str, Any]:
"""Shape-only validation of the signed admission (no I/O)."""
require(isinstance(adm, dict) and set(adm) == ADMISSION_FIELDS, "admission_refused")
def validate_admission(adm: Any, mode: str = PHASE_A) -> dict[str, Any]:
"""Shape-only validation of the signed admission (no I/O); phase-A and drill admissions are disjoint."""
require(mode in (PHASE_A, DRILL_MODE), "admission_refused")
drill = mode == DRILL_MODE
fields = ADMISSION_FIELDS | {"drill", "snapshot_label"} if drill else ADMISSION_FIELDS
require(isinstance(adm, dict) and set(adm) == fields, "admission_refused")
require(adm["schema"] == ADMISSION_SCHEMA and adm["namespace"] == NAMESPACE, "admission_refused")
require(isinstance(adm["attempt_id"], str) and ATTEMPT_RE.fullmatch(adm["attempt_id"]), "admission_refused")
attempt_re = DRILL_ATTEMPT_RE if drill else ATTEMPT_RE
require(isinstance(adm["attempt_id"], str) and attempt_re.fullmatch(adm["attempt_id"]), "admission_refused")
require(adm["phase"] == "A" and adm["phase_b_admitted"] is False, "admission_refused")
require(adm["material"] in ("original", "synthetic"), "admission_refused")
require(adm["readback_credential"] == "generate-root-from-entered-shares", "admission_refused")
require(adm["readback_credential"] == (DRILL_READBACK if drill else "generate-root-from-entered-shares"),
"admission_refused")
if drill:
require(adm["drill"] == DRILL, "admission_refused")
require(isinstance(adm["snapshot_label"], str) and SNAPSHOT_LABEL_RE.fullmatch(adm["snapshot_label"]),
"admission_refused")
nb, na = parse_iso(adm["not_before"]), parse_iso(adm["not_after"])
require(nb < na and (na - nb).total_seconds() <= 24 * 3600, "admission_refused")
dur = adm["max_duration_seconds"]
@ -338,11 +376,11 @@ def read_owner_file(path: Path, limit: int) -> bytes:
return raw
def load_admission(admission: Path, signature: Path, admission_sha256: str,
signers: Path) -> tuple[dict[str, Any], bytes, dict[str, str], bool]:
def load_admission(admission: Path, signature: Path, admission_sha256: str, signers: Path,
mode: str = PHASE_A) -> tuple[dict[str, Any], bytes, dict[str, str], bool]:
raw = read_owner_file(admission, 64 * 1024)
require(DIGEST_RE.fullmatch(admission_sha256) and sha256_bytes(raw) == admission_sha256, "admission_refused")
adm = validate_admission(strict_json(raw))
adm = validate_admission(strict_json(raw), mode)
signer = verify_signature(raw, read_owner_file(signature, 8192), signers)
protected = anchor_protected(signers)
if adm["material"] == "original":
@ -401,6 +439,18 @@ def wipe(buf: bytearray | None) -> None:
buf[i] = 0
def http_code(stderr: bytes) -> int | None:
"""HTTP status from the bao CLI error text (``Code: 403.``); None if absent."""
match = HTTP_CODE_RE.search(stderr)
return int(match.group(1)) if match else None
def policy_names(value: Any) -> list[str]:
"""Policy names only, sorted; anything not shaped like a policy name is dropped."""
return sorted(p for p in (value if isinstance(value, list) else [])
if isinstance(p, str) and POLICY_NAME_RE.fullmatch(p))[:32]
def tty_prompt(label: str) -> bytearray:
"""Hidden prompt on the controlling terminal; the value never touches argv/env/disk."""
require(sys.stdin.isatty(), "preflight_refused")
@ -415,7 +465,10 @@ class Session:
def __init__(self, *, admission: dict[str, Any], admission_sha256: str, signer: dict[str, str],
anchor_is_protected: bool, ciphertext: Path, receipt_path: Path, root: Path = ROOT,
prompt: Callable[[str], bytearray] = tty_prompt, docker: str = "docker",
say: Callable[[str], None] = lambda m: print(m, flush=True), start_reaper: bool = True):
say: Callable[[str], None] = lambda m: print(m, flush=True), start_reaper: bool = True,
mode: str = PHASE_A):
require(mode in (PHASE_A, DRILL_MODE) and (mode == DRILL_MODE) == ("drill" in admission), "admission_refused")
self.mode = mode
self.adm, self.root, self.prompt, self.docker, self.say = admission, root, prompt, docker, say
self.ciphertext, self.receipt_path, self.start_reaper = ciphertext, receipt_path, start_reaper
self.name = "rpf64-restore-" + secrets.token_hex(6)
@ -458,6 +511,22 @@ class Session:
"kv_data_read": False, "no_secret_material_recorded": True,
"claims_not_made": list(CLAIMS_NOT_MADE),
}
if mode == DRILL_MODE:
self.receipt["claims_not_made"] += list(DRILL_CLAIMS_NOT_MADE)
self.receipt.update({
"drill": DRILL,
"held_root_token": {"valid": None, "policies": [], "ttl_zero": None, "expire_time_null": None,
"orphan": None},
"generate_root": {"started": False, "shares_entered": 0, "complete": False, "otp_decode_ok": False,
"policies": [], "minimal_action_path": MINIMAL_ACTION_PATH,
"minimal_action_ok": False},
"revocation": {"revoke_status": None, "post_revoke_lookup": None},
"cancel_drill": {"started_then_cancelled": False},
# The held-token result is valid only as of this snapshot, never for production now.
"claim_limits": {"production_root_token_validity": {
"as_of_snapshot": admission["snapshot_label"],
"snapshot_plaintext_sha256": snap["plaintext_sha256"]}},
})
# ---- infrastructure
def remaining(self) -> float:
@ -501,6 +570,10 @@ class Session:
body = json.loads(done.stdout or b"{}")
return body.get("data", body) if isinstance(body, dict) and isinstance(body.get("data"), dict) else body
def delete(self, path: str, reason: str = "internal_error") -> None:
require(path in ALLOWED_DELETES, "internal_error")
self.bao(["delete", path], reason=reason)
def tell_reaper(self, line: str) -> None:
if self.reaper and self.reaper.stdin:
try:
@ -778,6 +851,109 @@ class Session:
self.receipt["objects"] = rows
return rows
# ---- T10 break-glass drill steps (after restore + unseal)
def forget_unseal_shares(self) -> None:
"""The drill re-enters shares for generate-root; the unseal entries are not reused."""
for share in self.shares:
wipe(share)
self.shares.clear()
def drill_held_token(self) -> None:
self.say("Drill 1/5: held root-token check (hidden prompt; lookup-self only, never revoked).")
held = self.prompt("Held initial root token")
try:
require(HELD_TOKEN_RE.fullmatch(held.decode("ascii", "replace")), "held_token_refused")
done = self.bao(["read", "-format=json", "auth/token/lookup-self"], credential=held.decode(),
check=False, reason="held_token_refused")
finally:
wipe(held)
record = self.receipt["held_root_token"]
if done.returncode == 0:
body = json.loads(done.stdout)
data = body.get("data") if isinstance(body, dict) and isinstance(body.get("data"), dict) else {}
record.update(valid=True, policies=policy_names(data.get("policies")),
ttl_zero=type(data.get("ttl")) is int and data["ttl"] == 0,
expire_time_null="expire_time" in data and data["expire_time"] is None,
orphan=data["orphan"] if type(data.get("orphan")) is bool else None)
body.clear()
data = {}
elif http_code(done.stderr) == 403:
record.update(valid=False)
else:
raise Refused("held_token_refused")
def drill_generate_root(self) -> None:
self.say("Drill 2/5: fresh generate-root ceremony; re-enter two shares (hidden prompts).")
record = self.receipt["generate_root"]
status = self.read(GENERATE_ROOT_ATTEMPT, reason="generate_root_failed") or {}
require(status.get("started") is False, "generate_root_failed")
attempt = self.write(GENERATE_ROOT_ATTEMPT, {}, reason="generate_root_failed")
otp, nonce, encoded, update = attempt.get("otp"), attempt.get("nonce"), None, {}
attempt.clear()
require(isinstance(otp, str) and otp and isinstance(nonce, str) and nonce, "generate_root_failed")
record["started"] = True
for index in (1, 2):
share = self.prompt(f"Re-enter unseal share {index} of 2 for generate-root")
try:
require(SHARE_RE.fullmatch(share.decode("ascii", "replace")), "generate_root_failed")
update = self.write("sys/generate-root/update", {"key": share.decode(), "nonce": nonce},
reason="generate_root_failed")
finally:
wipe(share)
record["shares_entered"] = index
encoded = update.get("encoded_token") or update.get("encoded_root_token") or encoded
record["complete"] = update.get("complete") is True and bool(encoded)
require(record["complete"], "generate_root_failed")
try:
raw = base64.b64decode(encoded + "=" * (-len(encoded) % 4))
require(len(raw) == len(otp), "generate_root_failed")
self.credential = bytes(a ^ b for a, b in zip(raw, otp.encode())).decode("ascii")
except (ValueError, UnicodeDecodeError) as exc:
raise Refused("generate_root_failed") from exc
finally:
otp = nonce = encoded = None
update.clear()
record["otp_decode_ok"] = True
lookup = self.read("auth/token/lookup-self", self.credential, reason="generate_root_failed") or {}
record["policies"] = policy_names(lookup.get("policies"))
lookup.clear()
require(record["policies"] == ["root"], "generate_root_failed")
def drill_minimal_action(self) -> None:
self.say("Drill 3/5: one read-only action (sys/auth metadata).")
mounts = self.read(MINIMAL_ACTION_PATH, self.credential, reason="minimal_action_failed")
ok = isinstance(mounts, dict) and isinstance(mounts.get("token/"), dict)
self.receipt["generate_root"]["minimal_action_ok"] = ok
require(ok, "minimal_action_failed")
def drill_revoke(self) -> None:
self.say("Drill 4/5: revoke-self the generated token, then confirm lookup-self is refused.")
record = self.receipt["revocation"]
require("auth/token/revoke-self" in ALLOWED_WRITES, "internal_error")
done = self.bao(["write", "-format=json", "auth/token/revoke-self", "-"], credential=self.credential,
body=b"{}", check=False, reason="revocation_failed")
# The CLI prints nothing for 204 No Content; any response body means another status.
record["revoke_status"] = (204 if not done.stdout.strip() else 200) if done.returncode == 0 \
else http_code(done.stderr)
check = self.bao(["read", "-format=json", "auth/token/lookup-self"], credential=self.credential,
check=False, reason="revocation_failed")
record["post_revoke_lookup"] = 200 if check.returncode == 0 else http_code(check.stderr)
revoked = record["revoke_status"] == 204 and record["post_revoke_lookup"] == 403
self.receipt["teardown"]["readback_credential_revoked"] = revoked
require(revoked, "revocation_failed")
self.credential = None
def drill_cancel(self) -> None:
self.say("Drill 5/5: start one more generate-root attempt and cancel it.")
attempt = self.write(GENERATE_ROOT_ATTEMPT, {}, reason="cancel_drill_failed")
started = attempt.get("started") is True
attempt.clear()
self.delete(GENERATE_ROOT_ATTEMPT, reason="cancel_drill_failed")
status = self.read(GENERATE_ROOT_ATTEMPT, reason="cancel_drill_failed") or {}
ok = started and status.get("started") is False and status.get("progress") == 0
self.receipt["cancel_drill"]["started_then_cancelled"] = ok
require(ok, "cancel_drill_failed")
def revoke(self) -> None:
if not self.credential or not self.container_started:
return
@ -844,16 +1020,25 @@ class Session:
self.start_target()
self.restore()
self.unseal_original()
self.generate_credential()
rows = self.readback()
missing = [r for r in rows if not r["present"]]
mismatch = [r for r in rows if r["present"] and not r["source_match"]]
if missing:
result, reason = "refused", "object_missing"
elif mismatch:
result, reason = "refused", "object_mismatch"
if self.mode == DRILL_MODE:
self.forget_unseal_shares()
self.drill_held_token()
self.drill_generate_root()
self.drill_minimal_action()
self.drill_revoke()
self.drill_cancel()
result, reason = "drilled", "break_glass_drill_complete"
else:
result, reason = "restored", "all_required_objects_match"
self.generate_credential()
rows = self.readback()
missing = [r for r in rows if not r["present"]]
mismatch = [r for r in rows if r["present"] and not r["source_match"]]
if missing:
result, reason = "refused", "object_missing"
elif mismatch:
result, reason = "refused", "object_mismatch"
else:
result, reason = "restored", "all_required_objects_match"
except Refused as exc:
result, reason = "aborted", exc.reason if exc.reason in ABORT_REASONS else "internal_error"
except Exception:
@ -953,11 +1138,12 @@ def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
for name in ("admission", "admission-sig", "admission-sha256", "ciphertext", "receipt"):
parser.add_argument("--" + name, required=True)
parser.add_argument("--mode", choices=(PHASE_A, DRILL_MODE), default=PHASE_A)
opts = parser.parse_args(args)
os.umask(0o077)
try:
adm, _raw, signer, protected = load_admission(Path(opts.admission), Path(opts.admission_sig),
opts.admission_sha256, anchor_path())
opts.admission_sha256, anchor_path(), opts.mode)
require(adm["material"] == "original", "admission_refused")
check_source_closure(adm, ROOT)
receipt = Path(opts.receipt)
@ -966,12 +1152,13 @@ def main(argv: list[str] | None = None) -> int:
print(f"ERROR: {exc.reason}; no custody input requested.", file=sys.stderr)
return 1
session = Session(admission=adm, admission_sha256=opts.admission_sha256, signer=signer,
anchor_is_protected=protected, ciphertext=Path(opts.ciphertext), receipt_path=receipt)
anchor_is_protected=protected, ciphertext=Path(opts.ciphertext), receipt_path=receipt,
mode=opts.mode)
result = session.execute()
verify = subprocess.run([sys.executable, str(ROOT / HERE / "verify-restore-receipt.py"), "--receipt",
str(receipt), "--source-root", str(ROOT)], capture_output=True, timeout=60)
print("Independent verifier: " + ("PASS" if verify.returncode == 0 else "FAIL"))
return 0 if result["result"] in ("restored", "refused") and verify.returncode == 0 else 2
return 0 if result["result"] in ("restored", "refused", "drilled") and verify.returncode == 0 else 2
if __name__ == "__main__":

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Attended launcher: isolated original-OpenBao restore, phase A (RPF-WP-0064).
# Attended launcher: isolated original-OpenBao restore, phase A or T10 break-glass drill (RPF-WP-0064).
# Founder workstation only. Never railiance01, never a kubeconfig, no network.
set +x
set -euo pipefail
@ -7,7 +7,9 @@ ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd -- "$ROOT"
umask 077
unset KUBECONFIG BAO_TOKEN VAULT_TOKEN BAO_ADDR VAULT_ADDR
usage='Usage: run-isolated-restore.sh ADMISSION ADMISSION_SIG ADMISSION_SHA256 CIPHERTEXT RECEIPT_DIR'
usage='Usage: run-isolated-restore.sh [--break-glass-drill] ADMISSION ADMISSION_SIG ADMISSION_SHA256 CIPHERTEXT RECEIPT_DIR'
MODE=phase-a
if [[ "${1:-}" == --break-glass-drill ]]; then MODE=break-glass-drill; shift; fi
[[ $# == 5 ]] || { printf '%s\n' "$usage"; exit 1; }
[[ "$3" =~ ^sha256:[a-f0-9]{64}$ ]] || { printf '%s\n' 'ERROR: exact sha256:<hex> admission digest required.'; exit 1; }
for index in 1 2 4; do
@ -19,13 +21,20 @@ done
for tool in python3 age age-keygen docker ssh-keygen git; do
command -v "$tool" >/dev/null || { printf 'ERROR: required tool unavailable: %s\n' "$tool"; exit 1; }
done
RECEIPT="$5/rpf64-restore-$(date -u +%Y%m%dT%H%M%SZ).json"
python3 scripts/openbao-isolated-restore/preflight.py --admission "$1" --admission-sig "$2" \
RECEIPT="$5/rpf64-${MODE}-$(date -u +%Y%m%dT%H%M%SZ).json"
python3 scripts/openbao-isolated-restore/preflight.py --mode "$MODE" --admission "$1" --admission-sig "$2" \
--admission-sha256 "$3" --ciphertext "$4" --receipt "$RECEIPT"
printf '%s\n' 'Phase A only: networkless restore and metadata readback. No consumer continuity,'
printf '%s\n' 'live JWT login, phase B network or owner acceptance is claimed.'
printf '%s\n' 'You will be asked for the backup age identity, then two original unseal shares.'
if [[ "$MODE" == break-glass-drill ]]; then
printf '%s\n' 'Break-glass drill (RPF-WP-0064-T10) on an isolated, networkless restore. Never production.'
printf '%s\n' 'Prompts: backup age identity, two unseal shares, the held initial root token (lookup-self only),'
printf '%s\n' 'then two shares AGAIN for a fresh generate-root ceremony. Production root-token validity,'
printf '%s\n' 'production generate-root availability and the SSO-down access path are not claimed.'
else
printf '%s\n' 'Phase A only: networkless restore and metadata readback. No consumer continuity,'
printf '%s\n' 'live JWT login, phase B network or owner acceptance is claimed.'
printf '%s\n' 'You will be asked for the backup age identity, then two original unseal shares.'
fi
printf '%s\n' 'Press Ctrl-C at any time to abort; the container is removed and an aborted receipt written.'
python3 scripts/openbao-isolated-restore/restore_adapter.py --admission "$1" --admission-sig "$2" \
python3 scripts/openbao-isolated-restore/restore_adapter.py --mode "$MODE" --admission "$1" --admission-sig "$2" \
--admission-sha256 "$3" --ciphertext "$4" --receipt "$RECEIPT"
printf 'Safe receipt: %s\n' "$RECEIPT"

View file

@ -6,6 +6,12 @@ consistency, deadline, required teardown evidence, claims not made, refuses
placeholders and anything shaped like a credential, share, key or KV value,
and compares ``source_sha256`` against a clean checkout at ``adapter_commit``.
RPF-WP-0064-T10 break-glass drill receipts (``drill: "break-glass"``) carry a
typed extension: held-token facts, generate-root ceremony facts, revocation
status codes, the cancel drill and the snapshot the held-token result is
limited to. Every extension value is an enumerated, typed fact; anything else
(token, accessor, OTP, nonce, encoded token, share) is rejected.
Exit 0 = receipt accepted. Prints only PASS/FAIL and a reason code.
"""
from __future__ import annotations
@ -52,6 +58,17 @@ REQUIRED_OBJECTS = (
"secrets_engine_jwt_role", "netkingdom_oidc_config", "platform_admin_role",
"policy_secrets_engine_login_self", "kv_registration_metadata",
)
DRILL = "break-glass"
DRILL_TOP = {"drill", "held_root_token", "generate_root", "revocation", "cancel_drill", "claim_limits"}
HELD = {"valid", "policies", "ttl_zero", "expire_time_null", "orphan"}
GENERATE_ROOT = {"started", "shares_entered", "complete", "otp_decode_ok", "policies", "minimal_action_path",
"minimal_action_ok"}
REVOCATION = {"revoke_status", "post_revoke_lookup"}
DRILL_CLAIMS = {"production_root_token_validity", "production_generate_root_availability", "sso_down_access_path"}
DRILL_ABORT = {"held_token_refused", "generate_root_failed", "minimal_action_failed", "revocation_failed",
"cancel_drill_failed"}
POLICY_NAME = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,63}$")
SNAPSHOT_LABEL = re.compile(r"^(?=.*20\d{6})[a-z0-9][a-z0-9._-]{2,95}$")
CLAIMS = {"consumer_continuity", "live_jwt_login", "phase_b_network", "owner_acceptance",
"recurring_restore_assurance", "production_redirect"}
ABORT = {"admission_refused", "preflight_refused", "custody_identity_refused", "ciphertext_mismatch",
@ -72,6 +89,9 @@ SECRET_SHAPES = (
re.compile(r"(?<![a-f0-9])[a-f0-9]{32,}(?![a-f0-9])"),
re.compile(r"eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\."),
)
# Tokens without a prefix, accessors, OTPs, encoded tokens: a run of 20+ base62/64
# characters mixing upper case, lower case and digits. Paths, ids and labels do not.
MIXED_RUN = re.compile(r"[A-Za-z0-9+/=_]{20,}")
BASE64_BLOB = re.compile(r"(?<![:A-Za-z0-9+/])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])")
@ -79,9 +99,14 @@ def base64_blob(text: str) -> bool:
"""Random base64 (shares, keys) mixes cases and digits; repo paths do not."""
return any(re.search(r"[A-Z]", m) and re.search(r"[a-z]", m) and re.search(r"[0-9+]", m)
for m in BASE64_BLOB.findall(text))
def mixed_run(text: str) -> bool:
return any(re.search(r"[A-Z]", m) and re.search(r"[a-z]", m) and re.search(r"[0-9]", m)
for m in MIXED_RUN.findall(text))
PLACEHOLDERS = re.compile(r"(?i)replace-with|placeholder|todo|changeme|<[^>]*>|example\.invalid|YYYY")
FORBIDDEN_KEYS = re.compile(r"(?i)^(?:data|value|values|secret|secrets|token|root_token|unseal_keys.*|keys?|"
r"otp|nonce|encoded_.*|password|plaintext)$")
r"otp|nonce|encoded_.*|password|plaintext|accessor|client_token|shares?|id)$")
class Reject(Exception):
@ -137,6 +162,8 @@ def check_content(receipt: dict) -> None:
for shape in SECRET_SHAPES:
need(not shape.search(stripped), "secret_shaped_value")
need(not base64_blob(stripped), "secret_shaped_value")
if path != "$.signer_fingerprint": # SHA256:<base64> is regex-checked above
need(not mixed_run(stripped), "secret_shaped_value")
def check_sources(receipt: dict, root: Path) -> None:
@ -154,14 +181,58 @@ def check_sources(receipt: dict, root: Path) -> None:
need("sha256:" + hashlib.sha256(path.read_bytes()).hexdigest() == expected, "source_hash_mismatch")
def check_drill(receipt: dict, result: str) -> None:
"""Typed T10 extension; every value is an enumerated fact, never a credential."""
need(receipt["drill"] == DRILL, "drill")
held = receipt["held_root_token"]
need(isinstance(held, dict) and set(held) == HELD, "drill_fields")
need(all(held[k] in (True, False, None) for k in ("valid", "ttl_zero", "expire_time_null", "orphan")),
"drill_fields")
need(isinstance(held["policies"], list) and len(held["policies"]) <= 32
and all(isinstance(p, str) and POLICY_NAME.fullmatch(p) for p in held["policies"]), "drill_fields")
if held["valid"] is not True:
need(held["policies"] == [] and held["ttl_zero"] is None and held["expire_time_null"] is None
and held["orphan"] is None, "drill_inconsistent")
gen = receipt["generate_root"]
need(isinstance(gen, dict) and set(gen) == GENERATE_ROOT, "drill_fields")
need(all(type(gen[k]) is bool for k in ("started", "complete", "otp_decode_ok", "minimal_action_ok")),
"drill_fields")
need(type(gen["shares_entered"]) is int and 0 <= gen["shares_entered"] <= 2, "drill_fields")
need(isinstance(gen["policies"], list) and gen["policies"] in ([], ["root"]), "drill_fields")
need(gen["minimal_action_path"] == "sys/auth", "drill_fields")
rev = receipt["revocation"]
need(isinstance(rev, dict) and set(rev) == REVOCATION, "drill_fields")
need(all(rev[k] is None or (type(rev[k]) is int and 100 <= rev[k] <= 599) for k in REVOCATION), "drill_fields")
cancel = receipt["cancel_drill"]
need(isinstance(cancel, dict) and set(cancel) == {"started_then_cancelled"}
and type(cancel["started_then_cancelled"]) is bool, "drill_fields")
limits = receipt["claim_limits"]
need(isinstance(limits, dict) and set(limits) == {"production_root_token_validity"}, "claim_limits")
basis = limits["production_root_token_validity"]
need(isinstance(basis, dict) and set(basis) == {"as_of_snapshot", "snapshot_plaintext_sha256"}
and isinstance(basis["as_of_snapshot"], str) and SNAPSHOT_LABEL.fullmatch(basis["as_of_snapshot"])
and basis["snapshot_plaintext_sha256"] == receipt["snapshot"]["plaintext_sha256"], "claim_limits")
need(receipt["objects"] == [], "objects")
if result == "drilled":
need(held["valid"] in (True, False), "drill_inconsistent")
need(gen["started"] and gen["shares_entered"] == 2 and gen["complete"] and gen["otp_decode_ok"]
and gen["policies"] == ["root"] and gen["minimal_action_ok"], "drill_inconsistent")
need(rev["revoke_status"] == 204 and rev["post_revoke_lookup"] == 403, "drill_inconsistent")
need(cancel["started_then_cancelled"] is True, "drill_inconsistent")
need(receipt["teardown"]["readback_credential_revoked"] is True, "drill_inconsistent")
elif gen["policies"]:
need(gen["otp_decode_ok"], "drill_inconsistent")
def verify(receipt: object, *, source_root: Path, not_after: datetime | None = None,
now: datetime | None = None) -> str:
need(isinstance(receipt, dict) and set(receipt) == TOP, "schema_fields")
drill = isinstance(receipt, dict) and "drill" in receipt
need(isinstance(receipt, dict) and set(receipt) == (TOP | DRILL_TOP if drill else TOP), "schema_fields")
need(receipt["schema"] == SCHEMA and receipt["admission_namespace"] == NAMESPACE, "schema")
need(receipt["extends"] == "docs/openbao-restore-drill-evidence.example.json", "schema")
need(receipt["phase"] == "A" and receipt["material"] in ("original", "synthetic"), "phase")
need(isinstance(receipt["attempt_id"], str) and re.fullmatch(r"RPF-WP-0064-T0[47]-[A-Za-z0-9-]{3,64}",
receipt["attempt_id"]), "attempt_id")
attempt = r"RPF-WP-0064-T10-[A-Za-z0-9-]{3,64}" if drill else r"RPF-WP-0064-T0[47]-[A-Za-z0-9-]{3,64}"
need(isinstance(receipt["attempt_id"], str) and re.fullmatch(attempt, receipt["attempt_id"]), "attempt_id")
need(digest_ok(receipt["admission_sha256"]), "admission_digest")
need(isinstance(receipt["signer_principal"], str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._@-]{0,127}",
receipt["signer_principal"]), "signer")
@ -175,8 +246,9 @@ def verify(receipt: object, *, source_root: Path, not_after: datetime | None = N
and digest_ok(image.split("@", 1)[1]), "image")
need(receipt["image_id"] is None or digest_ok(receipt["image_id"]), "image")
need(receipt["no_secret_material_recorded"] is True and receipt["kv_data_read"] is False, "secret_policy")
need(isinstance(receipt["claims_not_made"], list) and set(receipt["claims_not_made"]) == CLAIMS
and len(receipt["claims_not_made"]) == len(CLAIMS), "claims_not_made")
claims = CLAIMS | DRILL_CLAIMS if drill else CLAIMS
need(isinstance(receipt["claims_not_made"], list) and set(receipt["claims_not_made"]) == claims
and len(receipt["claims_not_made"]) == len(claims), "claims_not_made")
started, finished = parse_time(receipt["started_at"]), parse_time(receipt["finished_at"])
deadline = receipt["deadline_seconds"]
@ -237,7 +309,22 @@ def verify(receipt: object, *, source_root: Path, not_after: datetime | None = N
names = [row["object"] for row in objects]
result, reason = receipt["result"], receipt["reason"]
if result in ("restored", "refused"):
if drill:
check_drill(receipt, result)
if drill and result == "drilled":
need(reason == "break_glass_drill_complete", "result_inconsistent")
need(snap["ciphertext_verified"] and snap["plaintext_verified"], "restore_inconsistent")
need(restored["sealed_after_restore"] is True and restored["unsealed_with_original_shares"] is True
and restored["cluster_id_match"] is True and restored["applied_index_not_behind"] is True
and restored["throwaway_init_invalidated"] is True
and restored["cluster_id"] == snap["source_cluster_id"]
and restored["applied_index"] >= snap["source_applied_index"], "restore_inconsistent")
need(network["network_mode"] == "none" and network["docker_networks"] == ["none"]
and network["interfaces"] == ["lo"] and network["published_ports"] == [] and network["mounts"] == []
and network["egress_refused"] is True, "network_not_contained")
need(teardown["performed_by"] == "session", "teardown_missing")
need(digest_ok(receipt["image_id"]), "image")
elif result in ("restored", "refused") and not drill:
need(tuple(names) == REQUIRED_OBJECTS, "objects_incomplete")
need(snap["ciphertext_verified"] and snap["plaintext_verified"], "restore_inconsistent")
need(restored["sealed_after_restore"] is True and restored["unsealed_with_original_shares"] is True
@ -258,7 +345,7 @@ def verify(receipt: object, *, source_root: Path, not_after: datetime | None = N
missing = any(not r["present"] for r in objects)
need(not all_ok and reason == ("object_missing" if missing else "object_mismatch"), "result_inconsistent")
elif result == "aborted":
need(reason in ABORT, "result_inconsistent")
need(reason in (ABORT | DRILL_ABORT if drill else ABORT), "result_inconsistent")
need(not objects or len(objects) == len(REQUIRED_OBJECTS), "objects")
if network["docker_networks"]:
need(network["docker_networks"] == ["none"] and network["published_ports"] == [], "network_not_contained")
@ -274,7 +361,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--receipt", required=True)
parser.add_argument("--source-root", required=True, help="clean checkout at the receipt's adapter_commit")
parser.add_argument("--not-after", help="refuse after this UTC time (YYYY-MM-DDTHH:MM:SSZ)")
parser.add_argument("--expect-result", choices=("restored", "refused", "aborted"))
parser.add_argument("--expect-result", choices=("restored", "refused", "aborted", "drilled"))
opts = parser.parse_args(argv)
try:
path = Path(opts.receipt)