#!/usr/bin/env python3 """Mint the MAS workload key, prove its boundary, and write its one CAS=0 pair. Run only as `--mint-workload` inside the attended OpenBao platform-admin login, started by `mas_evidence_iam_session.mjs --mint-workload`. The founder-held supplier key at SUPPLIER is not read or copied: the workload pair is a second API key on the same IAM application, so it can be revoked on its own. A temporary Scaleway Console session arrives on stdin after the gate signals readiness. If anything fails after the mint, the minted key is deleted. Stdout stays empty. Receipts record statement ids, HTTP statuses, versions, and field names. Values are never written to a receipt, a file, or stderr. """ from __future__ import annotations import json import os import pwd import re import subprocess import sys import time import urllib.error import urllib.request from pathlib import Path ROOT = Path("/home/worsch/railiance-platform") DESIRED = ROOT / "docs/object-storage/railiance-platform-pg-backup-policy.json" POLICY_FILE = ROOT / "openbao/policies/backup-pair-writer.hcl" RECEIPT_DIR = Path("/home/worsch/.local/state/railiance/receipts") BUCKET = "railiance-platform-pg-backup" ENDPOINT = "https://s3.nl-ams.scw.cloud" APPLICATION_ID = "25a4cbca-220e-40c0-9af5-6c7713000174" PREFIX = "platform-pg/application-archives/mas/" SIBLING = "platform-pg/application-archives/email-connect/" MAS_SIDS = ( "MasEvidenceBackupList", "MasEvidenceBackupObjects", ) PROBE_SID = "PublisherBucketProbe" PROBE_ACTIONS = ( "s3:GetBucketLocation", "s3:GetBucketVersioning", "s3:GetLifecycleConfiguration", ) REPLACED_PROBES = ( "CoulombSocialBackupProbe", "EmailConnectBackupProbe", "MasEvidenceBackupProbe", ) STATEMENT_LIMIT = 10 SUPPLIER = "platform/suppliers/scaleway/mas-evidence-backup-primary" WORKLOAD = "platform/workloads/mas/backup-primary" PREFERRED_PROJECT = "reef-storage" PROJECT_ID = "e1a0dd0e-04b8-4ea9-8b30-d53f8c35c688" POLICY_NAME = "mas-evidence-backup-primary" WORKLOAD_KEY_DESCRIPTION = "MAS evidence workload pair; RPF-WP-0070; node delivery" READY_NAME = "mas-evidence-mint-ready.json" MINT_NAME = "mas-evidence-workload-mint.json" PROOF_ATTEMPTS = 7 PROOF_DELAY_SECONDS = 10 BAO_ADDR = "http://127.0.0.1:18200" _PROJECT_ID = re.compile( r"\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\Z" ) _SECRET_AUTH_FIELDS = frozenset({"client_token", "accessor"}) class GateError(RuntimeError): def __init__(self, code, detail=None): super().__init__(code) self.code = code self.detail = detail _ROLE = { "awsaccesskeyid": "access", "accesskey": "access", "accesskeyid": "access", "scwaccesskey": "access", "awssecretaccesskey": "secret", "secretkey": "secret", "secretaccesskey": "secret", "scwsecretkey": "secret", } def _role(name): return _ROLE.get("".join(char.lower() for char in name if char.isalnum())) def _mapping_text(value): """Read one credentials blob. Ignore anything that is not a mapping.""" text = value.strip() if text.startswith("{") and text.endswith("}"): try: parsed = json.loads(text) except json.JSONDecodeError: return None return parsed if isinstance(parsed, dict) else None rows = {} for line in text.splitlines(): line = line.split("#", 1)[0].strip() if not line or line.startswith("[") or "=" not in line: continue key, raw = line.split("=", 1) rows[key.strip()] = raw.strip().strip('"').strip("'") return rows or None def _shape_detail(path, data, reason): detail = {"path": path, "reason": reason} if isinstance(data, dict): detail["keys"] = sorted(str(key) for key in data) else: detail["shape"] = type(data).__name__ return detail def supplier_pair(data, path="", unwrap=True): """Return the two workload fields. Refuse a partial or identical pair.""" if isinstance(data, str): parsed = _mapping_text(data) if isinstance(parsed, dict): return supplier_pair(parsed, path, unwrap=False) raise GateError("supplier_shape_rejected", _shape_detail(path, data, "not_a_mapping")) if not isinstance(data, dict): raise GateError("supplier_shape_rejected", _shape_detail(path, data, "not_a_mapping")) nested = [value for value in data.values() if isinstance(value, dict)] strings = {key: value for key, value in data.items() if isinstance(value, str)} access = [] secret = [] for name, value in strings.items(): role = _role(str(name)) if role == "access": access.append(value.strip()) elif role == "secret": secret.append(value.strip()) if not access and not secret and len(nested) == 1 and not strings: return supplier_pair(nested[0], path, unwrap=False) if unwrap and not access and not secret and len(strings) == 1 and not nested: parsed = _mapping_text(next(iter(strings.values()))) if isinstance(parsed, dict): return supplier_pair(parsed, path, unwrap=False) if len(access) != 1 or len(secret) != 1 or not access or not secret or not access[0] or not secret[0] or access[0] == secret[0]: reason = "empty_or_equal" if access and secret else "pair_not_found" raise GateError("supplier_shape_rejected", _shape_detail(path, data, reason)) return {"AWS_ACCESS_KEY_ID": access[0], "AWS_SECRET_ACCESS_KEY": secret[0]} def mas_statements(document): found = [] for item in document.get("Statement", []): if item.get("Sid") in MAS_SIDS: found.append(item) return found def _actions(statement): value = statement.get("Action") or [] if isinstance(value, str): return [value] return list(value) def _principals(statement): principal = statement.get("Principal") if not isinstance(principal, dict): return [] value = principal.get("SCW") if isinstance(value, str): return [value] if isinstance(value, list): return [item for item in value if isinstance(item, str)] return [] def policy_observation(document): """Non-secret view of the MAS statements and whether object delete is absent.""" rows = mas_statements(document) actions = [] for item in rows: actions.extend(_actions(item)) probe = next( (item for item in document.get("Statement", []) if item.get("Sid") == PROBE_SID), None, ) probe_actions = _actions(probe) if probe else [] return { "sids": [item.get("Sid") for item in rows], "mas_sids_present": [sid for sid in MAS_SIDS if sid in {item.get("Sid") for item in rows}], "object_delete_absent": not any( action in {"s3:DeleteObject", "s3:DeleteObjectVersion"} for action in actions ), "principal": APPLICATION_ID, "statement_count": len(document.get("Statement", [])), "probe_includes_publisher": bool( probe and f"application_id:{APPLICATION_ID}" in _principals(probe) and all(action in probe_actions for action in PROBE_ACTIONS) ), } def merged_policy(live, desired): """Keep live statements and fit the publisher probe plus the MAS prefix into 10.""" desired_items = list(desired.get("Statement", [])) desired_by_sid = {item.get("Sid"): item for item in desired_items} statements = [] seen = set() for item in live.get("Statement", []): sid = item.get("Sid") if sid in REPLACED_PROBES: continue statements.append(desired_by_sid.get(sid, item)) seen.add(sid) for item in desired_items: sid = item.get("Sid") if sid not in seen: statements.append(item) seen.add(sid) sids = [item.get("Sid") for item in statements] if len(statements) > STATEMENT_LIMIT: raise GateError( "policy_statement_limit", {"count": len(statements), "limit": STATEMENT_LIMIT, "sids": sids}, ) return { "Version": live.get("Version") or desired.get("Version"), "Id": live.get("Id") or desired.get("Id"), "Statement": statements, } def denial_observation(results): required = ("sibling_prefix", "retained_delete", "administration") names = (*required, "own_prefix") denied = {name: bool(results.get(name, {}).get("denied")) for name in required} own = results.get("own_prefix") or {} own_allowed = own.get("http_status") == 200 and own.get("denied") is False return { "application_id": APPLICATION_ID, "denied": denied, "http_status": {name: results.get(name, {}).get("http_status") for name in names}, "error_code": {name: results.get(name, {}).get("error_code") for name in names}, "own_prefix_allowed": own_allowed, "passed": all(denied.values()) and own_allowed, } def version_from_put(payload): data = payload.get("data") or {} version = data.get("version") if version != 1: raise GateError("unexpected_version") return {"version": 1, "request_id": payload.get("request_id")} def logical_path(path): for marker in ("/metadata/", "/data/"): if path.startswith("platform" + marker): return "platform/" + path.split(marker, 1)[1] return path def _write_json(path, value): path.parent.mkdir(mode=0o700, exist_ok=True) path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") path.chmod(0o600) def _bao(args, token=None): env = os.environ.copy() env.setdefault("BAO_ADDR", "http://127.0.0.1:18200") env.setdefault("VAULT_ADDR", env["BAO_ADDR"]) if token is None: pass else: env["BAO_TOKEN"] = token env["VAULT_TOKEN"] = token return subprocess.run(["bao", *args], capture_output=True, env=env) def _kv_data(path): proc = _bao(["kv", "get", "-format=json", path]) if proc.returncode: raise GateError("kv_read_failed") body = json.loads(proc.stdout) data = ((body.get("data") or {}).get("data")) or {} if not isinstance(data, dict): raise GateError("kv_read_failed") return data def scoped_supplier_pair(pair, project_id): """Scope only the proof client. The stored workload pair stays the raw key.""" key = pair["AWS_ACCESS_KEY_ID"] project = project_id.strip() if isinstance(project_id, str) else "" applied = bool(project and "@" not in key and _PROJECT_ID.fullmatch(project)) if not applied: return dict(pair), False scoped = dict(pair) scoped["AWS_ACCESS_KEY_ID"] = f"{key}@{project}" return scoped, True def _client(pair): """Avoid the boto3 1.36+ default checksum on PutBucketPolicy. Scaleway accepts the body from older clients. The default checksum makes the same document come back as MalformedPolicy. """ import boto3 from botocore.config import Config return boto3.client( "s3", region_name="nl-ams", endpoint_url=ENDPOINT, aws_access_key_id=pair["AWS_ACCESS_KEY_ID"], aws_secret_access_key=pair["AWS_SECRET_ACCESS_KEY"], config=Config( signature_version="s3v4", request_checksum_calculation="when_required", response_checksum_validation="when_required", ), ) def public_error(message): import re text = str(message) text = re.sub( r"(?i)(secret|token|password|signature)([^\s,;]{0,12}\s*[:=]\s*)\S+", r"\1\2", text, ) text = re.sub(r"SCW[A-Z0-9]{8,}", "SCW", text) text = re.sub(r"\b[A-Za-z0-9+/]{32,}={0,2}\b", "", text) return text[:300] def s3_failure(exc): response = exc.response if isinstance(getattr(exc, "response", None), dict) else {} error = response.get("Error") if isinstance(response.get("Error"), dict) else {} meta = response.get("ResponseMetadata") if isinstance(response.get("ResponseMetadata"), dict) else {} detail = { "error_code": error.get("Code"), "http_status": meta.get("HTTPStatusCode"), } if isinstance(error.get("Message"), str) and error.get("Message"): detail["message"] = public_error(error["Message"]) return detail def _status(call): from botocore.exceptions import ClientError try: result = call() body = result.get("Body") if isinstance(result, dict) else None if body is not None: body.close() code = result.get("ResponseMetadata", {}).get("HTTPStatusCode", 200) return {"http_status": code, "denied": False, "error_code": None} except ClientError as exc: failure = s3_failure(exc) code = failure["http_status"] return {"http_status": code, "denied": code == 403, "error_code": failure["error_code"]} def backup_python(account_home): """Pinned backup runtime on the account home. Attended login replaces the process home with an empty session. A venv under that session, or under /tmp, is not there on the next login. """ return ( Path(account_home) / ".local/share/railiance-platform/backup-venv/bin/python" ) def selected_backup_python(): return backup_python(pwd.getpwuid(os.getuid()).pw_dir) def same_interpreter(current, candidate): """Compare the invoked path. The venv binary is a symlink to system Python.""" return Path(current) == Path(candidate) def _ensure_boto(): try: import boto3 # noqa: F401 return except ImportError: pass candidate = selected_backup_python() if ( candidate.is_file() and os.access(candidate, os.X_OK) and not same_interpreter(sys.executable, candidate) ): os.execv(candidate, [str(candidate), *sys.argv]) raise GateError("boto3_required") def apply_live_policy(reader_pair, writer_pair): from botocore.exceptions import ClientError desired = json.loads(DESIRED.read_text(encoding="utf-8")) reader = _client(reader_pair) try: live_policy = reader.get_bucket_policy(Bucket=BUCKET)["Policy"] except ClientError as exc: raise GateError("policy_read_failed", s3_failure(exc)) from None try: live = json.loads(live_policy) except (TypeError, json.JSONDecodeError): raise GateError("policy_read_failed", {"reason": "response_shape"}) from None policy = merged_policy(live, desired) statements = policy["Statement"] receipt = {"before": [item.get("Sid") for item in live.get("Statement", [])]} if receipt["before"] == [item.get("Sid") for item in statements]: receipt["put"] = "unchanged" else: try: _client(writer_pair).put_bucket_policy(Bucket=BUCKET, Policy=json.dumps(policy)) except ClientError as exc: raise GateError("policy_write_failed", s3_failure(exc)) from None receipt["put"] = "ok" try: after_policy = reader.get_bucket_policy(Bucket=BUCKET)["Policy"] except ClientError as exc: raise GateError("policy_read_failed", s3_failure(exc)) from None try: after = json.loads(after_policy) except (TypeError, json.JSONDecodeError): raise GateError("policy_read_failed", {"reason": "response_shape"}) from None receipt["after"] = [item.get("Sid") for item in after.get("Statement", [])] receipt["mas"] = policy_observation(after) _write_json(RECEIPT_DIR / "backup-bucket-policy-apply.json", receipt) if ( not receipt["mas"]["object_delete_absent"] or receipt["mas"]["mas_sids_present"] != list(MAS_SIDS) or not receipt["mas"]["probe_includes_publisher"] or receipt["mas"]["statement_count"] > STATEMENT_LIMIT ): raise GateError("mas_policy_not_installed") return receipt def prove_denials(pair, project_id=""): scoped, applied = scoped_supplier_pair(pair, project_id) client = _client(scoped) results = { "sibling_prefix": _status( lambda: client.list_objects_v2(Bucket=BUCKET, Prefix=SIBLING, MaxKeys=1) ), "retained_delete": _status( lambda: client.delete_object(Bucket=BUCKET, Key=PREFIX + "retained-delete-probe") ), "administration": _status(lambda: client.get_bucket_policy(Bucket=BUCKET)), "own_prefix": _status( lambda: client.list_objects_v2(Bucket=BUCKET, Prefix=PREFIX, MaxKeys=1) ), } observation = denial_observation(results) observation["preferred_project"] = PREFERRED_PROJECT observation["project_id_present"] = bool(isinstance(project_id, str) and project_id.strip()) observation["project_scope_applied"] = applied _write_json(RECEIPT_DIR / "mas-evidence-publisher-denials.json", observation) if not observation["passed"]: raise GateError("publisher_denials_failed") return observation def _string_list(value): if isinstance(value, list) and all(isinstance(item, str) for item in value): return value return None def _bounded_int(value): if type(value) is int: return value if isinstance(value, str) and value.isdigit(): return int(value) return None def _identity_detail(auth, policies, ttl, uses): identity = _string_list(auth.get("identity_policies")) legacy = _string_list(auth.get("policies")) return { "policies": policies, "legacy_policies": legacy or [], "token_policies_present": _string_list(auth.get("token_policies")) is not None, "policies_present": legacy is not None, "identity_policies": identity or [], "ttl": ttl, "ttl_type": type(auth.get("lease_duration")).__name__, "num_uses": uses, "num_uses_type": type(auth.get("num_uses")).__name__, "auth_fields": sorted(key for key in auth if key not in _SECRET_AUTH_FIELDS), } def _writer_identity(auth): """Use the create response so the two token uses stay available for put and revoke. OpenBao 2.5 returns token_policies. A non-empty legacy policies list must name the same single policy. default and any parent policy stay rejected. """ token_policies = _string_list(auth.get("token_policies")) legacy = _string_list(auth.get("policies")) policies = token_policies if token_policies else (legacy or []) identity = _string_list(auth.get("identity_policies")) if identity is None and "identity_policies" not in auth: identity = [] ttl = _bounded_int(auth.get("lease_duration")) uses = _bounded_int(auth.get("num_uses")) legacy_conflict = bool(legacy) and legacy != ["backup-pair-writer"] accepted = ( not legacy_conflict and policies == ["backup-pair-writer"] and "default" not in policies and identity == [] and type(ttl) is int and 0 < ttl <= 300 and type(uses) is int and 0 < uses <= 2 ) if not accepted: raise GateError("writer_identity_rejected", _identity_detail(auth, policies, ttl, uses)) return { "policies": policies, "identity_policies": [], "ttl": ttl, "num_uses": uses, } class _Transport: def __init__(self, token, absent): self.token = token self.absent = absent def metadata(self, path): if logical_path(path) != WORKLOAD or not self.absent: raise GateError("pair_metadata_rejected") return None def put_cas0(self, path, body): """Write over the HTTP API. The value never touches a file.""" if logical_path(path) != WORKLOAD or body.get("options") != {"cas": 0}: raise GateError("pair_write_failed") request = urllib.request.Request( BAO_ADDR + "/v1/" + path, data=json.dumps(body).encode(), method="POST", headers={"X-Vault-Token": self.token, "Content-Type": "application/json"}, ) opener = urllib.request.build_opener(_NoRedirect) try: with opener.open(request, timeout=20) as response: raw = response.read(65537) except GateError: raise except Exception: raise GateError("pair_write_failed") from None try: return version_from_put(json.loads(raw)) except json.JSONDecodeError: raise GateError("pair_write_failed") from None def revoke(self): _bao(["token", "revoke", "-self"], token=self.token) def _custody(): import importlib.util spec = importlib.util.spec_from_file_location( "application_backup_pair_custody", ROOT / "scripts/application_backup_pair_custody.py" ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module class _NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): raise GateError("writer_token_failed") def writer_auth(payload): """Keep the server's num_uses. The OpenBao 2.5 CLI drops that field.""" auth = payload.get("auth") if isinstance(payload, dict) else None token = auth.get("client_token") if isinstance(auth, dict) else None if not isinstance(token, str) or not token: raise GateError("writer_token_failed") return auth def parent_token(environ=None): """Find the attended admin token the way the bao CLI does. Warden's attended login removes BAO_TOKEN and VAULT_TOKEN from the child and leaves the token only in the private helper at $HOME/.vault-token. Returns (token, source); the token itself is never recorded. """ environ = os.environ if environ is None else environ for name in ("BAO_TOKEN", "VAULT_TOKEN"): value = (environ.get(name) or "").strip() if value: return value, name home = environ.get("HOME") or "" if not home: return "", "absent" helper = Path(home) / ".vault-token" try: info = helper.lstat() except OSError: return "", "absent" import stat as _stat if not _stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077: return "", "helper_rejected" value = helper.read_text(encoding="utf-8").strip() return (value, "token_helper") if value else ("", "absent") def _request_writer_auth(): addr = os.environ.get("BAO_ADDR") or os.environ.get("VAULT_ADDR") or BAO_ADDR if addr.rstrip("/") != BAO_ADDR: raise GateError("writer_token_failed", {"reason": "bao_addr_mismatch"}) parent, source = parent_token() if not parent: raise GateError("writer_token_failed", {"reason": "parent_token_" + source}) body = json.dumps({ "policies": ["backup-pair-writer"], "ttl": "5m", "num_uses": 2, "no_default_policy": True, }).encode() request = urllib.request.Request( BAO_ADDR + "/v1/auth/token/create-orphan", data=body, method="POST", headers={"X-Vault-Token": parent, "Content-Type": "application/json"}, ) opener = urllib.request.build_opener(_NoRedirect) try: with opener.open(request, timeout=20) as response: raw = response.read(65537) except GateError: raise except urllib.error.HTTPError as exc: raise GateError( "writer_token_failed", {"reason": "create_http_error", "http_status": exc.code, "parent_source": source} ) from None except Exception as exc: raise GateError( "writer_token_failed", {"reason": "create_request_failed", "error_type": type(exc).__name__, "parent_source": source} ) from None if len(raw) > 65536: raise GateError("writer_token_failed", {"reason": "create_response_too_large"}) try: return writer_auth(json.loads(raw)) except json.JSONDecodeError: raise GateError("writer_token_failed") from None def write_pair(pair): custody = _custody() existing = _bao(["kv", "metadata", "get", "-format=json", WORKLOAD]) if existing.returncode == 0: raise GateError("existing_custody_requires_rotation_review") auth = _request_writer_auth() token = auth["client_token"] try: identity = _writer_identity(auth) receipt = custody.provision("mas", pair, identity, _Transport(token, absent=True)) except Exception: _bao(["token", "revoke", "-self"], token=token) raise meta = _bao(["kv", "metadata", "get", "-format=json", WORKLOAD]) if meta.returncode: raise GateError("pair_metadata_unreadable") data = json.loads(meta.stdout).get("data") or {} names = sorted(_kv_data(WORKLOAD)) summary = { "path": WORKLOAD, "current_version": data.get("current_version"), "field_names": names, "cas": 0, "kv_version": receipt["kv_version"], "values_recorded": False, } if summary["current_version"] != 1 or names != ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]: raise GateError("pair_metadata_rejected") _write_json(RECEIPT_DIR / "mas-evidence-pair.json", summary) return summary def _app_keys(api, organization): """List the application's keys. A Console session needs organization_id or gets HTTP 400.""" from urllib.parse import urlencode query = urlencode({ "organization_id": organization, "application_id": APPLICATION_ID, "page_size": 100, }) value = api("GET", "/iam/v1alpha1/api-keys?" + query) keys = value.get("api_keys") if isinstance(value, dict) else None if not isinstance(keys, list) or value.get("total_count") != len(keys): raise GateError("key_inventory_rejected") return [item for item in keys if item.get("application_id") == APPLICATION_ID] def _key_view(keys): return { "count": len(keys), "descriptions": sorted(str(item.get("description") or "") for item in keys), "default_project_ids": sorted({str(item.get("default_project_id")) for item in keys}), } def mint_workload_pair(api, prove, write, sleep=time.sleep): """Mint one more key on the MAS application, prove it, then store it CAS=0. `api` is the console transport. `prove(pair, project)` and `write(pair)` are the S3 proof and the OpenBao write. The minted key is deleted when a later step fails, and the receipt says whether that deletion happened. """ receipt = {"application_id": APPLICATION_ID, "values_recorded": False} app = api("GET", "/iam/v1alpha1/applications/" + APPLICATION_ID) organization = app.get("organization_id") if app.get("name") != POLICY_NAME or not isinstance(organization, str) or not organization: raise GateError("application_binding_refused") before = _app_keys(api, organization) receipt["keys_before"] = _key_view(before) if len(before) != 1 or before[0].get("default_project_id") != PROJECT_ID: raise GateError("supplier_key_state_requires_review", receipt["keys_before"]) issued = api("POST", "/iam/v1alpha1/api-keys", { "application_id": APPLICATION_ID, "default_project_id": PROJECT_ID, "description": WORKLOAD_KEY_DESCRIPTION, }) access = issued.get("access_key") if isinstance(issued, dict) else None try: if issued.get("application_id") != APPLICATION_ID: raise GateError("issued_key_binding_refused") pair = { "AWS_ACCESS_KEY_ID": access, "AWS_SECRET_ACCESS_KEY": issued.get("secret_key"), } if any(not isinstance(value, str) or not value for value in pair.values()): raise GateError("issued_key_incomplete") receipt["minted_at"] = issued.get("created_at") attempts = 0 while True: attempts += 1 try: prove(pair, PROJECT_ID) break except GateError as exc: if exc.code != "publisher_denials_failed" or attempts >= PROOF_ATTEMPTS: raise sleep(PROOF_DELAY_SECONDS) receipt["proof_attempts"] = attempts receipt["pair"] = write(pair) except Exception as exc: deleted = False if isinstance(access, str) and access: try: api("DELETE", "/iam/v1alpha1/api-keys/" + access) deleted = True except Exception: deleted = False code = exc.code if isinstance(exc, GateError) else "contained_gate_failure" detail = dict(exc.detail or {}) if isinstance(exc, GateError) and isinstance(exc.detail, dict) else {} detail["minted_key_deleted"] = deleted raise GateError(code, detail) from None receipt["keys_after"] = _key_view(_app_keys(api, organization)) receipt["status"] = "passed" return receipt def _console_api(raw): import importlib.util if len(raw) > 65536: raise GateError("console_auth_rejected") try: auth = json.loads(raw) except json.JSONDecodeError: raise GateError("console_auth_rejected") from None spec = importlib.util.spec_from_file_location( "application_recovery_identities", ROOT / "scripts/application_recovery_identities.py" ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) try: transport = module.Transport(auth) except module.Refused: raise GateError("console_auth_rejected") from None def api(method, path, data=None): try: return transport(method, path, data) except module.ProviderError as exc: raise GateError("provider_http_error", {"method": method, "http_status": exc.status}) from None except module.Refused as exc: raise GateError("provider_refused", {"reason": str(exc)}) from None return api def _preflight(): existing = _bao(["kv", "metadata", "get", "-format=json", WORKLOAD]) if existing.returncode == 0: raise GateError("existing_custody_requires_rotation_review") if _bao(["policy", "write", "backup-pair-writer", str(POLICY_FILE)]).returncode: raise GateError("writer_policy_install_failed") def main(argv=None): args = list(sys.argv[1:] if argv is None else argv) sys.stdout = open(os.devnull, "w") sys.stderr = open(os.devnull, "w") receipt_path = RECEIPT_DIR / "mas-evidence-publisher-gate.json" try: if args != ["--mint-workload"]: raise GateError("usage_refused") _ensure_boto() RECEIPT_DIR.mkdir(mode=0o700, exist_ok=True) (RECEIPT_DIR / MINT_NAME).unlink(missing_ok=True) _preflight() _write_json(RECEIPT_DIR / READY_NAME, {"status": "waiting_for_console", "values_recorded": False}) api = _console_api(sys.stdin.read(65537)) receipt = mint_workload_pair(api, prove_denials, write_pair) _write_json(RECEIPT_DIR / MINT_NAME, receipt) except GateError as exc: receipt = {"status": "failed", "code": exc.code, "values_recorded": False} if exc.detail: receipt["detail"] = exc.detail _write_json(receipt_path, receipt) return 71 except Exception as exc: _write_json( receipt_path, { "status": "failed", "code": "contained_gate_failure", "exception": type(exc).__name__, "values_recorded": False, }, ) return 72 finally: (RECEIPT_DIR / READY_NAME).unlink(missing_ok=True) _write_json(receipt_path, {"status": "passed", "values_recorded": False}) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except SystemExit: raise except Exception: raise SystemExit(72) from None