railiance-platform/tests/test_openbao_break_glass_drill.py
codex a2da4085e0 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
2026-10-28 00:02:10 +01:00

372 lines
20 KiB
Python

"""RPF-WP-0064-T10 break-glass drill: offline tests with a fake OpenBao (no Docker).
The fake answers the bao CLI calls the drill makes, keeps generated tokens,
OTPs and nonces in memory only, and records which credential reached which
path, so the tests can show the held token is used for ``lookup-self`` only.
"""
from __future__ import annotations
import base64
import copy
import json
import secrets
import subprocess
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import test_openbao_isolated_restore as U # noqa: E402
A, V, R = U.A, U.V, U.R
LABEL = "openbao-raft-20261014t153200z.snap.age"
def b64share() -> str:
return base64.b64encode(secrets.token_bytes(33)).decode()
class FakeBao:
"""Just enough OpenBao for the drill steps; failure switches per test."""
def __init__(self, held_valid=True):
self.held = "s." + secrets.token_urlsafe(18)[:24]
self.held_valid = held_valid
self.generated: str | None = None
self.revoked = False
self.attempt: dict | None = None
self.calls: list[tuple[str, ...]] = []
self.held_paths: list[str] = []
self.generated_policies = ["root"]
self.revoke_body = b""
self.cancel_works = True
@staticmethod
def ok(body=b""):
return subprocess.CompletedProcess([], 0, body if isinstance(body, bytes) else json.dumps(body).encode(), b"")
@staticmethod
def code(status):
return subprocess.CompletedProcess([], 2, b"", f"Error: Error making API request.\n\nCode: {status}. "
"Errors:\n\n* permission denied\n".encode())
def __call__(self, args, *, credential=None, body=b"", limit=30, check=True, reason="internal_error"):
verb, path = args[0], args[-2] if args[-1] == "-" else args[-1]
self.calls.append((verb, path))
if credential is not None and credential == self.held:
self.held_paths.append(f"{verb} {path}")
done = self.answer(verb, path, credential, body)
if check and done.returncode != 0:
raise A.Refused(reason)
return done
def status(self):
att = self.attempt or {}
return {"started": bool(self.attempt), "progress": att.get("progress", 0), "required": 2,
"complete": False, "otp": "", "nonce": "", "encoded_token": "", "otp_length": 26}
def answer(self, verb, path, credential, body):
if path == "auth/token/lookup-self" and verb == "read":
if credential == self.held and self.held_valid:
return self.ok({"data": {"policies": ["root"], "ttl": 0, "expire_time": None, "orphan": True,
"id": self.held, "accessor": "Ab3" * 8}})
if credential and credential == self.generated and not self.revoked:
return self.ok({"data": {"policies": self.generated_policies, "ttl": 0, "expire_time": None,
"id": self.generated}})
return self.code(403)
if path == "sys/generate-root/attempt" and verb == "read":
return self.ok({"data": self.status()})
if path == "sys/generate-root/attempt" and verb == "write":
self.attempt = {"otp": secrets.token_urlsafe(30)[:26].replace("-", "a").replace("_", "b"),
"nonce": "0b0e" + secrets.token_hex(14), "progress": 0}
return self.ok({"data": {**self.status(), "otp": self.attempt["otp"], "nonce": self.attempt["nonce"]}})
if path == "sys/generate-root/attempt" and verb == "delete":
if self.cancel_works:
self.attempt = None
return self.ok(b"Success! Data deleted\n")
if path == "sys/generate-root/update":
payload = json.loads(body)
assert payload["nonce"] == self.attempt["nonce"]
self.attempt["progress"] += 1
if self.attempt["progress"] < 2:
return self.ok({"data": {**self.status(), "progress": 1}})
self.generated = "s." + secrets.token_urlsafe(18)[:24]
otp = self.attempt["otp"].encode()
encoded = base64.b64encode(bytes(a ^ b for a, b in zip(self.generated.encode(), otp))).decode()
self.attempt = None
return self.ok({"data": {"complete": True, "encoded_token": encoded, "progress": 2}})
if path == "sys/auth" and credential == self.generated and not self.revoked:
return self.ok({"data": {"token/": {"type": "token"}, "netkingdom/": {"type": "oidc"}}})
if path == "auth/token/revoke-self" and credential == self.generated:
self.revoked = True
return self.ok(self.revoke_body)
return self.code(403)
class DrillBase(U.Base):
def drill_admission(self, **override):
adm = self.admission(attempt_id="RPF-WP-0064-T10-unit-01", drill="break-glass", snapshot_label=LABEL,
readback_credential=A.DRILL_READBACK)
adm.update(override)
return adm
def session(self, fake, answers, **adm_override):
queue = list(answers)
self.prompts: list[str] = []
def prompt(label):
self.prompts.append(label)
return bytearray(queue.pop(0).encode())
adm = self.drill_admission(**adm_override)
session = A.Session(admission=adm, admission_sha256=U.digest("adm"),
signer={"principal": "founder@rehearsal", "fingerprint": "SHA256:" + "AbCd" * 10 + "AbC"},
anchor_is_protected=False, ciphertext=self.work / "none", receipt_path=self.work / "r",
root=self.root, prompt=prompt, say=lambda _m: None, start_reaper=False,
mode=A.DRILL_MODE)
session.bao = fake
return session
def run_drill(self, fake, answers=None):
answers = answers if answers is not None else [fake.held, b64share(), b64share()]
session = self.session(fake, answers)
session.drill_held_token()
session.drill_generate_root()
session.drill_minimal_action()
session.drill_revoke()
session.drill_cancel()
return session
def drill_receipt(self, session=None):
"""A complete ``drilled`` receipt from the adapter's own drill record plus a valid restore."""
session = session or self.run_drill(FakeBao())
base = self.receipt()
rc = copy.deepcopy(session.receipt)
for key in ("started_at", "finished_at", "elapsed_seconds", "image_id", "network", "restored", "snapshot"):
rc[key] = base[key]
rc["claim_limits"]["production_root_token_validity"]["snapshot_plaintext_sha256"] = \
rc["snapshot"]["plaintext_sha256"]
rc["image"] = base["image"]
rc["teardown"].update(container_removed=True, container_absent_verified=True)
rc.update(result="drilled", reason="break_glass_drill_complete")
return rc
class DrillAdmissionTests(DrillBase):
def test_valid_drill_admission(self):
A.validate_admission(self.drill_admission(), A.DRILL_MODE)
def test_phase_a_and_drill_admissions_are_disjoint(self):
with self.assertRaises(A.Refused):
A.validate_admission(self.drill_admission(), A.PHASE_A)
with self.assertRaises(A.Refused):
A.validate_admission(self.admission(), A.DRILL_MODE)
with self.assertRaises(A.Refused):
A.Session(admission=self.admission(), admission_sha256=U.digest("a"), signer={}, anchor_is_protected=False,
ciphertext=self.work, receipt_path=self.work / "x", mode=A.DRILL_MODE, start_reaper=False)
def test_drill_refusals(self):
cases = [{"drill": "phase-a"}, {"attempt_id": "RPF-WP-0064-T07-01"}, {"snapshot_label": "latest"},
{"snapshot_label": "Openbao-20261014"}, {"snapshot_label": "openbao-raft-snapshot"},
{"readback_credential": "generate-root-from-entered-shares"}, {"phase_b_admitted": True},
{"material": "original", "max_duration_seconds": 600}]
for override in cases:
with self.subTest(override=override), self.assertRaises(A.Refused):
A.validate_admission(self.drill_admission(**override), A.DRILL_MODE)
def test_allowed_surfaces(self):
self.assertIn("sys/generate-root/attempt", A.ALLOWED_READS)
self.assertIn("sys/generate-root/attempt", A.ALLOWED_WRITES)
self.assertIn("sys/generate-root/update", A.ALLOWED_WRITES)
self.assertEqual(A.ALLOWED_DELETES, frozenset({"sys/generate-root/attempt"}))
self.assertIn("auth/token/lookup-self", A.ALLOWED_READS)
self.assertIn("sys/auth", A.ALLOWED_READS)
self.assertFalse(any("/data/" in p for p in A.ALLOWED_READS))
self.assertEqual(A.MAX_DURATION_SECONDS, 2700)
listener = json.loads(A.SERVER_CONFIG)["listener"]["tcp"]
self.assertEqual(listener["address"], "127.0.0.1:8200")
self.assertIs(listener["disable_unauthed_generate_root_endpoints"], False)
with self.assertRaises(A.Refused):
self.session(FakeBao(), []).delete("sys/unseal")
class DrillStepTests(DrillBase):
def test_full_drill_records_only_facts(self):
fake = FakeBao()
shares = [b64share(), b64share()]
session = self.run_drill(fake, [fake.held, *shares])
rc = session.receipt
self.assertEqual(self.prompts, ["Held initial root token", "Re-enter unseal share 1 of 2 for generate-root",
"Re-enter unseal share 2 of 2 for generate-root"])
self.assertEqual(rc["held_root_token"], {"valid": True, "policies": ["root"], "ttl_zero": True,
"expire_time_null": True, "orphan": True})
self.assertEqual(rc["generate_root"], {"started": True, "shares_entered": 2, "complete": True,
"otp_decode_ok": True, "policies": ["root"],
"minimal_action_path": "sys/auth", "minimal_action_ok": True})
self.assertEqual(rc["revocation"], {"revoke_status": 204, "post_revoke_lookup": 403})
self.assertEqual(rc["cancel_drill"], {"started_then_cancelled": True})
self.assertIsNone(session.credential)
self.assertTrue(rc["teardown"]["readback_credential_revoked"])
# The held token reached lookup-self only: never revoked, never used for anything else.
self.assertEqual(fake.held_paths, ["read auth/token/lookup-self"])
self.assertIn(("delete", "sys/generate-root/attempt"), fake.calls)
text = json.dumps(rc)
for value in (fake.held, fake.generated, *shares):
self.assertNotIn(value, text)
self.assertEqual(rc["claim_limits"]["production_root_token_validity"],
{"as_of_snapshot": LABEL, "snapshot_plaintext_sha256": rc["snapshot"]["plaintext_sha256"]})
self.assertTrue(set(A.DRILL_CLAIMS_NOT_MADE) <= set(rc["claims_not_made"]))
def test_invalid_held_token_is_a_finding_not_an_abort(self):
fake = FakeBao(held_valid=False)
rc = self.run_drill(fake).receipt
self.assertEqual(rc["held_root_token"], {"valid": False, "policies": [], "ttl_zero": None,
"expire_time_null": None, "orphan": None})
self.assertEqual(self.verify(self.drill_receipt(self.run_drill(FakeBao(held_valid=False)))), "drilled")
def failing(self, fake, step, reason, answers=None):
session = self.session(fake, answers if answers is not None else [fake.held, b64share(), b64share()])
steps = [session.drill_held_token, session.drill_generate_root, session.drill_minimal_action,
session.drill_revoke, session.drill_cancel]
with self.assertRaises(A.Refused) as ctx:
for fn in steps[:step + 1]:
fn()
self.assertEqual(ctx.exception.reason, reason)
return session
def test_failures(self):
self.failing(FakeBao(), 0, "held_token_refused", ["not a token!", b64share(), b64share()])
fake = FakeBao()
fake.generated_policies = ["default"]
self.failing(fake, 1, "generate_root_failed")
self.failing(FakeBao(), 1, "generate_root_failed", [FakeBao().held, "short", b64share()])
fake = FakeBao()
fake.revoke_body = b'{"data": {}}'
session = self.failing(fake, 3, "revocation_failed")
self.assertFalse(session.receipt["teardown"]["readback_credential_revoked"])
fake = FakeBao()
fake.cancel_works = False
self.failing(fake, 4, "cancel_drill_failed")
def test_stale_attempt_refused(self):
fake = FakeBao()
fake.attempt = {"otp": "x", "nonce": "y", "progress": 1}
self.failing(fake, 1, "generate_root_failed")
class DrillVerifierTests(DrillBase):
def test_drilled_receipt_passes(self):
self.assertEqual(self.verify(self.drill_receipt()), "drilled")
def test_aborted_drill_receipt_passes(self):
session = self.session(FakeBao(), [])
rc = self.drill_receipt(session)
rc.update(result="aborted", reason="generate_root_failed")
self.assertEqual(self.verify(rc), "aborted")
def test_drill_and_phase_a_shapes_do_not_mix(self):
rc = self.drill_receipt()
rc.update(result="restored", reason="all_required_objects_match")
with self.assertRaises(V.Reject):
self.verify(rc)
phase_a = self.receipt()
phase_a.update(result="drilled", reason="break_glass_drill_complete")
with self.assertRaises(V.Reject):
self.verify(phase_a)
def test_adversarial_fixtures(self):
token = "s." + "Kq3x9ZbT7mW2pL8vR4nY6cJd"
hvs = "hvs.CAESI" + "Kq3x9ZbT7mW2pL8vR4nY6cJd" * 3
accessor = "Kq3x9ZbT7mW2pL8vR4nY6cJd"
otp = "Zq8T1mVb4Xw9Kc2Lp7Rn3Hs6Yd"
nonce = "0b0e6c2d-9f3a-4e1b-8c7d-5a6b7c8d9e0f"
encoded = base64.b64encode(bytes(range(40, 66))).decode()
share = base64.b64encode(bytes(range(1, 34))).decode()
hex_share = "ab01" * 17
gen = lambda **kw: (lambda r: r["generate_root"].update(**kw)) # noqa: E731
held = lambda **kw: (lambda r: r["held_root_token"].update(**kw)) # noqa: E731
cases = {
"token_value_policies": held(policies=[token]),
"token_in_path": lambda r: r.update(attempt_id="RPF-WP-0064-T10-" + token.replace(".", "-")),
"hvs_in_label": lambda r: r["claim_limits"]["production_root_token_validity"].update(as_of_snapshot=hvs),
"token_key": lambda r: r["held_root_token"].update(token=token),
"root_token_key": lambda r: r.update(root_token=token),
"accessor_key": held(accessor=accessor),
"accessor_value": gen(minimal_action_path=accessor),
"accessor_in_network": lambda r: r["network"].update(network_mode=accessor),
"otp_key": gen(otp=otp),
"otp_value": gen(policies=[otp]),
"otp_in_last_error": lambda r: r["restored"]["leader_wait"].update(last_error=otp) if
r["restored"]["leader_wait"] else r["network"].update(mounts=[otp]),
"nonce_key": gen(nonce=nonce),
"nonce_value": gen(minimal_action_path=nonce),
"encoded_token_key": gen(encoded_token=encoded),
"encoded_root_token_key": gen(encoded_root_token=encoded),
"encoded_value": lambda r: r["network"].update(mounts=[encoded]),
"share_key": gen(shares=[share]),
"share_value": gen(policies=[share]),
"share_in_cancel": lambda r: r.update(cancel_drill={"started_then_cancelled": share}),
"hex_share": lambda r: r["network"].update(mounts=[hex_share]),
"unseal_keys": lambda r: r.update(unseal_keys_b64=[share]),
"client_token": lambda r: r["revocation"].update(client_token=token),
}
for name, mutate in cases.items():
with self.subTest(name):
rc = self.drill_receipt()
mutate(rc)
with self.assertRaises(V.Reject):
self.verify(rc)
def test_inconsistent_drill_receipts(self):
cases = {
"revoke_200": (lambda r: r["revocation"].update(revoke_status=200), "drill_inconsistent"),
"lookup_after_revoke_ok": (lambda r: r["revocation"].update(post_revoke_lookup=200),
"drill_inconsistent"),
"one_share": (lambda r: r["generate_root"].update(shares_entered=1), "drill_inconsistent"),
"not_root": (lambda r: r["generate_root"].update(policies=["default"]), "drill_fields"),
"no_cancel": (lambda r: r["cancel_drill"].update(started_then_cancelled=False), "drill_inconsistent"),
"minimal_path": (lambda r: r["generate_root"].update(minimal_action_path="sys/mounts"), "drill_fields"),
"held_invalid_with_facts": (lambda r: r["held_root_token"].update(valid=False), "drill_inconsistent"),
"held_unchecked": (lambda r: r["held_root_token"].update(valid=None, policies=[], ttl_zero=None,
expire_time_null=None, orphan=None),
"drill_inconsistent"),
"claims_missing": (lambda r: r["claims_not_made"].remove("sso_down_access_path"), "claims_not_made"),
"snapshot_unnamed": (lambda r: r["claim_limits"]["production_root_token_validity"].update(
as_of_snapshot="latest"), "claim_limits"),
"snapshot_other_sha": (lambda r: r["claim_limits"]["production_root_token_validity"].update(
snapshot_plaintext_sha256=U.digest("other")), "claim_limits"),
"objects_read": (lambda r: r.update(objects=self.receipt()["objects"]), "objects"),
"phase_a_attempt": (lambda r: r.update(attempt_id="RPF-WP-0064-T07-01x"), "attempt_id"),
"unrevoked_teardown": (lambda r: r["teardown"].update(readback_credential_revoked=None),
"drill_inconsistent"),
"kv_data_read": (lambda r: r.update(kv_data_read=True), "secret_policy"),
"drill_reason_on_phase_a": (lambda r: r.update(result="aborted", reason="nope"), "result_inconsistent"),
}
for name, (mutate, code) in cases.items():
with self.subTest(name):
rc = self.drill_receipt()
mutate(rc)
with self.assertRaises(V.Reject) as ctx:
self.verify(rc)
self.assertEqual(str(ctx.exception), code)
def test_cli_accepts_drilled(self):
path = self.work / "drill.json"
path.write_text(json.dumps(self.drill_receipt()))
done = subprocess.run([sys.executable, str(R.VERIFIER), "--receipt", str(path), "--source-root",
str(self.root), "--expect-result", "drilled"], capture_output=True)
self.assertEqual(done.returncode, 0, done.stdout)
class DrillLauncherTests(DrillBase):
def test_launcher_mode_flag_refuses_before_prompt(self):
launcher = R.ROOT / "scripts/openbao-isolated-restore/run-isolated-restore.sh"
f = self.work / "f"
f.write_text("x")
out = subprocess.run(["bash", str(launcher), "--break-glass-drill", str(f), str(f), U.digest("x"), str(f),
str(self.work)], capture_output=True, stdin=subprocess.DEVNULL).stdout
self.assertIn(b"interactive terminal", out)
self.assertIn(b"--break-glass-drill", subprocess.run(["bash", str(launcher)], capture_output=True).stdout)
if __name__ == "__main__":
unittest.main()