#!/usr/bin/env python3 """Disposable KeyCape image -> actual Secrets Engine client -> Approval Engine. Synthetic keys/credentials only. No OpenBao, cluster mutation, or model call. The PDP is a sequencing double; this is not production authorization evidence. """ from __future__ import annotations import argparse import base64 import copy import hashlib import http.server import ipaddress import json import os from pathlib import Path import secrets import socket import ssl import subprocess import sys import tempfile import threading import time from datetime import datetime, timedelta, timezone from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import Request, urlopen from unittest.mock import patch from wsgiref.simple_server import WSGIRequestHandler, make_server from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID import yaml KEYCAPE_IMAGE = "forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611" ROOT = Path(__file__).resolve().parents[1] sys.path[:0] = [str(ROOT / "src"), str(ROOT)] def run(args): result = subprocess.run(args, capture_output=True, timeout=45) if result.returncode: raise RuntimeError("contained command failed") return result.stdout.decode().strip() def commit(path): return run(["git", "-C", str(path), "rev-parse", "HEAD"]) def private_write(path, value): with path.open("x") as out: os.chmod(path, 0o600) out.write(value) def exercise(args): sys.path.insert(0, str(args.approval_engine_source)) from approval_engine.api import App from approval_engine.auth import JWTAuthenticator from approval_engine.store import Engine from secrets_engine import cli from secrets_engine.approval_auth import approval_token, credential_urlopen from secrets_engine.approval_consume import ConsumeBinding, _expected_request, consume_approval from secrets_engine.catalog import validate_entry from secrets_engine.config import Config from secrets_engine.errors import BackendError, DecisionError from tests.authorization_stub import AuthorizationStub from tests.test_catalog import VALID receipt = { "schema_version": 1, "target": "disposable local processes; synthetic credentials", "started_at": datetime.now(timezone.utc).isoformat(), "keycape_image": KEYCAPE_IMAGE, "approval_engine_commit": commit(args.approval_engine_source), "keycape_contract_commit": commit(args.keycape_source), "consumer_source_sha256": { name: hashlib.sha256((ROOT / "src/secrets_engine" / name).read_bytes()).hexdigest() for name in ("approval_auth.py", "approval_consume.py", "config.py", "service_auth.py") }, "limitations": ["PDP sequencing double", "local Approval Engine source, not deployed image", "no live custody or client-side read grant", "no OpenBao effect or model execution"], "checks": {}, } with tempfile.TemporaryDirectory(prefix="approval-identity-private-") as temporary: root = Path(temporary) key = rsa.generate_private_key(public_exponent=65537, key_size=2048) private_write(root / "key.pem", key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()).decode()) now = datetime.now(timezone.utc) name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "local approval identity exercise")]) cert = (x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()) .serial_number(x509.random_serial_number()).not_valid_before(now - timedelta(minutes=1)) .not_valid_after(now + timedelta(hours=1)) .add_extension(x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]), False) .add_extension(x509.BasicConstraints(ca=True, path_length=None), True) .sign(key, hashes.SHA256())) (root / "cert.pem").write_bytes(cert.public_bytes(serialization.Encoding.PEM)) with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) keycape_port = sock.getsockname()[1] class Proxy(http.server.BaseHTTPRequestHandler): def log_message(self, *_args): pass def proxy(self): body = self.rfile.read(int(self.headers.get("Content-Length", 0))) if self.command == "POST" else None req = Request(f"http://127.0.0.1:{keycape_port}" + self.path, data=body, headers={k: self.headers[k] for k in ("Content-Type", "Authorization") if k in self.headers}) try: response = urlopen(req, timeout=10) except HTTPError as error: response = error with response: self.send_response(response.code) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(response.read()) do_GET = proxy do_POST = proxy server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Proxy) context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(root / "cert.pem", root / "key.pem") server.socket = context.wrap_socket(server.socket, server_side=True) issuer = f"https://127.0.0.1:{server.server_port}" proxy_thread = threading.Thread(target=server.serve_forever, daemon=True) proxy_thread.start() config = yaml.safe_load((args.keycape_source / "config/dev-config.yaml").read_text()) clients = yaml.safe_load((args.keycape_source / "config/service-clients.example.yaml").read_text())["clients"] clients = [c for c in clients if c["clientId"] in ("secrets-engine-approval", "approval-engine-operator")] assert len(clients) == 2 config.update(issuer=issuer, clients=clients) config["authelia"]["issuer"] = "https://synthetic-upstream.invalid" private_write(root / "config.yaml", yaml.safe_dump(config, sort_keys=False)) values = {c["clientId"]: secrets.token_urlsafe(48) for c in clients} private_write(root / "env", "KEYCAPE_CONFIG=/etc/keycape/config.yaml\n" + "".join( c["secretRef"].removeprefix("env:") + "=" + values[c["clientId"]] + "\n" for c in clients)) private_write(root / "approval.secret", values["secrets-engine-approval"]) private_write(root / "pdp.token", "synthetic-pdp-token") container = "approval-identity-exercise-" + secrets.token_hex(5) created = False engine = None api_server = None pdp = None try: run(["docker", "run", "-d", "--rm", "--name", container, "--user", str(os.getuid()), "--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--publish", f"127.0.0.1:{keycape_port}:8080", "--env-file", str(root / "env"), "--mount", "type=bind,source=" + temporary + ",target=/etc/keycape,readonly", KEYCAPE_IMAGE]) created = True with patch.dict(os.environ, {"SSL_CERT_FILE": str(root / "cert.pem"), "SECRETS_ENGINE_UNSAFE_DEMO": ""}): for _ in range(40): try: with credential_urlopen(issuer + "/jwks", timeout=1) as response: if response.status == 200: break except Exception: time.sleep(0.25) else: raise RuntimeError("disposable KeyCape did not start") engine = Engine(root / "approval.sqlite") app = App(engine, JWTAuthenticator(issuer=issuer, audience="approval-engine", jwks_url=issuer + "/jwks")) class Quiet(WSGIRequestHandler): def log_message(self, *_args): pass api_server = make_server("127.0.0.1", 0, app, handler_class=Quiet) threading.Thread(target=api_server.serve_forever, daemon=True).start() api = f"http://127.0.0.1:{api_server.server_port}" def operator_token(scope): credential = base64.b64encode(("approval-engine-operator:" + values["approval-engine-operator"]).encode()).decode() req = Request(issuer + "/token", data=urlencode({"grant_type": "client_credentials", "scope": scope}).encode(), headers={"Authorization": "Basic " + credential}) with credential_urlopen(req, timeout=3) as response: return json.load(response)["access_token"] def operator_request(path, payload, scope): req = Request(api + path, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json", "Authorization": "Bearer " + operator_token(scope)}) with credential_urlopen(req, timeout=3) as response: return json.load(response) raw = copy.deepcopy(VALID) raw["stage"] = "prod" raw["approval"] = {"model": "bootstrap-only", "authorization_id": "synthetic-approval", "purpose": "disposable identity proof"} entry = validate_entry(raw) pdp = AuthorizationStub(approval_id="unused", package="secrets-engine.catalog-lane.lifecycle", version="v2").start() cfg = Config(catalog_dir=root, policy_dir=root, evidence_dir=root / "evidence", hub_url="", bao_addr="", topic_id="", approval_url=api, approval_client_secret_file=root / "approval.secret", keycape_issuer=issuer, keycape_token_url=issuer + "/token", authorization_subject_id="secrets-engine", authorization_subject_type="service", authorization_policy_package="secrets-engine.catalog-lane.lifecycle", authorization_policy_version="v2", pdp_url=pdp.url, pdp_token_file=root / "pdp.token") request = _expected_request(cfg, entry, "apply") digest = pdp.bind_request(request) obj = operator_request("/v1/approvals", { "id": "synthetic-approval", "binding": {"action": "secrets.apply", "target": {"id": entry.id, "stage": "prod"}, "actor": "service:approval-engine-operator", "principal": "synthetic-operator", "purpose": "disposable identity proof"}, "validity": {"not_before": (now - timedelta(minutes=1)).isoformat(), "expires_at": (now + timedelta(minutes=10)).isoformat()}, "pdp_digest": digest, "pdp_path": True}, "approval:create") operator_request(f"/v1/approvals/{obj['id']}/entries", {}, "approval:approve") receipt["checks"]["operator_issued_and_approved_via_verified_jwt"] = True try: cli._require_lane_approval(cfg, entry, "destroy") except DecisionError: assert not engine.claim(obj["id"])["consumed"] else: raise AssertionError("wrong action accepted") receipt["checks"]["wrong_action_refused_before_consume"] = True cli._require_lane_approval(cfg, entry, "apply") assert engine.claim(obj["id"])["consumed"] assert pdp.calls == ["check"] receipt["checks"]["actual_consumer_claim_check_consume"] = True binding = ConsumeBinding(obj["id"], digest) retry = consume_approval(base_url=api, binding=binding, token_provider=lambda: approval_token(cfg, scope="approval:consume")) assert retry.idempotent receipt["checks"]["same_digest_retry_idempotent"] = True try: consume_approval(base_url=api, binding=ConsumeBinding(obj["id"], "sha256:" + "f" * 64), token_provider=lambda: approval_token(cfg, scope="approval:consume")) except DecisionError: pass else: raise AssertionError("different digest accepted") receipt["checks"]["different_digest_refused"] = True try: cli._require_lane_approval(cfg, entry, "apply") except DecisionError: pass else: raise AssertionError("spent claim accepted") receipt["checks"]["spent_claim_refused"] = True try: operator_token("approval:consume") except HTTPError as error: assert error.code in (400, 401) assert json.load(error)["error"] == "invalid_profile_usage" else: raise AssertionError("operator consume scope accepted") receipt["checks"]["operator_consume_scope_denied_by_issuer"] = True (root / "approval.secret").write_text("synthetic-wrong-secret") try: approval_token(cfg, scope="approval:read") except BackendError: pass else: raise AssertionError("wrong secret accepted") receipt["checks"]["wrong_secret_refused"] = True assert not (root / "approval.token").exists() receipt["checks"]["no_access_token_file_created"] = True finally: if api_server: api_server.shutdown() api_server.server_close() if pdp: pdp.stop() if engine: engine.close() if created: run(["docker", "stop", "--time=5", container]) server.shutdown() server.server_close() proxy_thread.join() receipt.update(status="passed", cleanup_complete=True, finished_at=datetime.now(timezone.utc).isoformat()) private_write(args.receipt, json.dumps(receipt, indent=2) + "\n") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--keycape-source", required=True, type=Path) parser.add_argument("--approval-engine-source", required=True, type=Path) parser.add_argument("--receipt", required=True, type=Path) args = parser.parse_args() if args.receipt.exists(): raise SystemExit("receipt path must be new") try: exercise(args) except Exception as error: # Never dump a request, client environment or credential-bearing frame. print("Synthetic approval identity exercise failed: " + type(error).__name__, file=sys.stderr) return 1 print("Synthetic approval identity exercise passed; temporary resources removed.") return 0 if __name__ == "__main__": raise SystemExit(main())