diff --git a/demo/client_cli.py b/demo/client_cli.py
deleted file mode 100644
index 5c46b85..0000000
--- a/demo/client_cli.py
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/usr/bin/env python3
-"""
-ZKAC TCP client: load a member credential, complete handshake, request /api (encrypted JSON).
-
-The "API" is not HTTP on port 8765 — it is one JSON request inside the ZKAC session on --port (default 9876).
-"""
-
-from __future__ import annotations
-
-import argparse
-import base64
-import json
-import socket
-from pathlib import Path
-
-import zkac
-from zkac.tcp import FramedSession, client_handshake
-
-
-def load_credential(member_json: Path) -> zkac.Credential:
- """Rebuild Credential from setup_demo.py output (same fields as zkac.Credential.finalize)."""
- m = json.loads(member_json.read_text(encoding="utf-8"))
- pk = zkac.BbsPublicKey.from_bytes(base64.b64decode(m["issuer_public_key_b64"]))
- rid = bytes.fromhex(m["role_id_hex"])
- return zkac.Credential.finalize(
- base64.b64decode(m["blind_sig_b64"]),
- base64.b64decode(m["member_secret_b64"]),
- base64.b64decode(m["prover_blind_b64"]),
- rid,
- int(m["epoch"]),
- pk,
- )
-
-
-def load_server_pk(creds_dir: Path) -> zkac.PublicKey:
- """Pinned server identity: must match the Keypair used by server.py (from transport.json)."""
- t = json.loads((creds_dir / "transport.json").read_text(encoding="utf-8"))
- raw = base64.b64decode(t["server_public_key_b64"])
- return zkac.PublicKey.from_bytes(raw)
-
-
-def main() -> None:
- ap = argparse.ArgumentParser(description="ZKAC demo client (TCP + credential)")
- ap.add_argument(
- "--creds-dir",
- type=Path,
- default=Path(__file__).resolve().parent / "creds",
- help="Directory with transport.json and member_*.json",
- )
- ap.add_argument(
- "--member",
- type=Path,
- help="Path to member_*.json (default: creds-dir/member_analyst.json)",
- )
- ap.add_argument("--host", default="127.0.0.1")
- ap.add_argument("--port", type=int, default=9876)
- args = ap.parse_args()
-
- creds_dir: Path = args.creds_dir
- member_path = args.member or (creds_dir / "member_analyst.json")
- if not member_path.is_file():
- raise SystemExit(f"Missing member file: {member_path}")
-
- credential = load_credential(member_path)
- server_pk = load_server_pk(creds_dir)
-
- # Ephemeral client transport identity (not the BBS+ member secret — that is inside credential).
- client_kp = zkac.Keypair()
- node = zkac.Node(client_kp)
-
- sock = socket.create_connection((args.host, args.port))
- try:
- # X25519 + server Schnorr + BBS+ auth; returns symmetric Session.
- session = client_handshake(sock, node, server_pk, credential)
- framed = FramedSession(sock, session)
-
- # Logical GET /api: path is checked by server after decrypt.
- request_obj = {"path": "/api"}
- payload = json.dumps(request_obj).encode("utf-8")
- framed.send(payload)
-
- reply = framed.recv().decode("utf-8")
- print(json.dumps(json.loads(reply), indent=2))
- finally:
- sock.close()
-
-
-if __name__ == "__main__":
- main()
diff --git a/demo/client_managed.py b/demo/client_managed.py
new file mode 100644
index 0000000..a114dda
--- /dev/null
+++ b/demo/client_managed.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+"""
+ZKAC client for client-managed registries.
+
+1. Issues a credential locally (admin issues to self — in production this
+ would go through the server's E2E-encrypted issuance relay).
+2. Verifies the registry state certificate.
+3. Authenticates via managed-registry handshake.
+4. Sends a JSON request over the encrypted session.
+
+Usage:
+ python client_managed.py --role analyst
+ python client_managed.py --role operator
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import json
+import socket
+from pathlib import Path
+
+import zkac
+from zkac.tcp import FramedSession, client_handshake_managed
+
+
+def main() -> None:
+ ap = argparse.ArgumentParser(description="ZKAC managed-registry client")
+ ap.add_argument("--creds-dir", type=Path,
+ default=Path(__file__).resolve().parent / "creds")
+ ap.add_argument("--role", default="analyst", choices=["analyst", "operator"])
+ ap.add_argument("--host", default="127.0.0.1")
+ ap.add_argument("--port", type=int, default=9877)
+ args = ap.parse_args()
+
+ creds_dir: Path = args.creds_dir
+ admin_data = json.loads((creds_dir / "managed_admin.json").read_text(encoding="utf-8"))
+ reg_data = json.loads((creds_dir / "managed_registry.json").read_text(encoding="utf-8"))
+ transport_data = json.loads((creds_dir / "transport.json").read_text(encoding="utf-8"))
+
+ # Reconstruct admin issuer to issue a credential for the chosen role.
+ # In production, this would be done via the E2E issuance relay.
+ admin_issuer = zkac.BbsIssuer.from_secret_key(
+ base64.b64decode(admin_data["admin_issuer_secret_b64"])
+ )
+ admin_pk = admin_issuer.public_key()
+
+ role_rid = zkac.role_id(args.role)
+ req = zkac.prepare_blind_request()
+ blind_sig = admin_issuer.issue_blind(req.commitment_with_proof(), role_rid, 1)
+ cred = zkac.Credential.finalize(
+ blind_sig, req.member_secret(), req.prover_blind(), role_rid, 1, admin_pk
+ )
+
+ # Verify registry state certificate before trusting it
+ state_bytes = base64.b64decode(reg_data["state_bytes_b64"])
+ state_cert = base64.b64decode(reg_data["state_cert_b64"])
+ registry_id = bytes.fromhex(reg_data["registry_id_hex"])
+
+ issuer_pk_for_verify = zkac.BbsPublicKey.from_bytes(
+ base64.b64decode(reg_data["admin_issuer_public_key_b64"])
+ )
+ expected_rid = zkac.registry_id(issuer_pk_for_verify)
+ assert expected_rid == registry_id, "registry_id mismatch"
+ assert zkac.RegistryState.verify_cert(issuer_pk_for_verify, state_cert, state_bytes), \
+ "state certificate verification failed"
+ print(f"Registry state verified (registry_id={registry_id.hex()[:16]}…)")
+
+ # Connect and authenticate
+ server_pk = zkac.PublicKey.from_bytes(
+ base64.b64decode(transport_data["server_public_key_b64"])
+ )
+ node = zkac.Node(zkac.Keypair())
+
+ sock = socket.create_connection((args.host, args.port))
+ try:
+ session = client_handshake_managed(
+ sock, node, server_pk, cred, registry_id
+ )
+ framed = FramedSession(sock, session)
+
+ framed.send(json.dumps({"path": "/api"}).encode())
+ reply = json.loads(framed.recv().decode())
+ print(json.dumps(reply, indent=2))
+ finally:
+ sock.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/demo/server.py b/demo/server.py
deleted file mode 100644
index 08a355c..0000000
--- a/demo/server.py
+++ /dev/null
@@ -1,196 +0,0 @@
-#!/usr/bin/env python3
-"""
-HTTP static site + ZKAC TCP service. Authenticated /api is accessed over TCP with zkac.tcp
-after setup_demo.py has created creds/.
-"""
-
-from __future__ import annotations
-
-import argparse
-import base64
-import json
-import os
-import threading
-import http.server
-import socket
-import traceback
-from pathlib import Path
-
-import zkac
-from zkac.tcp import FramedSession, server_handshake
-
-
-def load_registry(creds_dir: Path, epoch: int) -> zkac.RoleRegistry:
- """Load issuer public key and register every demo role at the same epoch."""
- iss = json.loads((creds_dir / "issuer.json").read_text(encoding="utf-8"))
- issuer_pk = zkac.BbsPublicKey.from_bytes(
- base64.b64decode(iss["issuer_public_key_b64"])
- )
- reg = zkac.RoleRegistry()
- for name in ("analyst", "operator"):
- reg.register_role(zkac.role_id(name), issuer_pk, epoch)
- return reg
-
-
-def _role_debug_label(role_id: bytes) -> str:
- """Map verified role_id bytes to a short label for logs (demo only)."""
- for name in ("analyst", "operator"):
- if role_id == zkac.role_id(name):
- return name
- return "unknown"
-
-
-def api_body_for_role(role_id: bytes) -> dict:
- """JSON returned for the logical /api resource after ZKAC auth; varies by credential role."""
- analyst = zkac.role_id("analyst")
- operator = zkac.role_id("operator")
- if role_id == analyst:
- return {
- "path": "/api",
- "role": "analyst",
- "datasets": ["summary", "aggregated_metrics"],
- "note": "Analyst tier: aggregated data only.",
- }
- if role_id == operator:
- return {
- "path": "/api",
- "role": "operator",
- "datasets": ["summary", "aggregated_metrics", "raw_logs", "pii"],
- "note": "Operator tier: full API slice including raw logs.",
- }
- return {"error": "unknown role", "path": "/api"}
-
-
-def handle_zkac_client(
- conn: socket.socket,
- client_addr: tuple,
- creds_dir: Path,
- registry: zkac.RoleRegistry,
-) -> None:
- """
- One TCP connection: ZKAC handshake + BBS+ auth, then one framed JSON request and response.
- Each handler rebuilds the server Node from persisted secret (Keypair is consumed by Node).
- """
- peer = f"{client_addr[0]}:{client_addr[1]}"
- print(f"[zkac] connect peer={peer}")
-
- try:
- # Same long-term server identity every time; from_secret_key because Node consumes Keypair.
- t = json.loads((creds_dir / "transport.json").read_text(encoding="utf-8"))
- sk = base64.b64decode(t["server_secret_key_b64"])
- server_kp = zkac.Keypair.from_secret_key(sk)
- node = zkac.Node(server_kp)
-
- session, role_id = server_handshake(conn, node, registry)
- label = _role_debug_label(role_id)
- print(
- f"[zkac] handshake_ok peer={peer} role_id={role_id.hex()} role={label!r}"
- )
-
- framed = FramedSession(conn, session)
- raw = framed.recv()
- print(
- f"[zkac] request peer={peer} plaintext_bytes={len(raw)} raw={raw!r}"
- )
-
- req = json.loads(raw.decode("utf-8"))
- print(f"[zkac] request_json peer={peer} parsed={req!r}")
-
- path = req.get("path")
- if path != "/api":
- err_body = {"error": "unsupported path", "allowed": ["/api"], "got": path}
- out = json.dumps(err_body).encode()
- framed.send(out)
- print(
- f"[zkac] response peer={peer} status=reject path={path!r} response_bytes={len(out)}"
- )
- return
-
- body = api_body_for_role(role_id)
- out_bytes = json.dumps(body).encode()
- framed.send(out_bytes)
- print(
- f"[zkac] response peer={peer} status=ok path=/api role={label!r} "
- f"response_bytes={len(out_bytes)} body_keys={list(body.keys())}"
- )
- except (ConnectionError, BrokenPipeError, OSError) as e:
- print(f"[zkac] peer={peer} connection_error: {e!r}")
- except (json.JSONDecodeError, ValueError) as e:
- print(f"[zkac] peer={peer} protocol_error: {e!r}")
- except Exception as e:
- print(f"[zkac] peer={peer} unexpected_error: {e!r}")
- traceback.print_exc()
- finally:
- conn.close()
- print(f"[zkac] closed peer={peer}")
-
-
-def run_http(host: str, port: int, static_root: Path) -> None:
- # Process-wide CWD: only this thread should rely on relative paths after chdir.
- os.chdir(static_root)
-
- class Handler(http.server.SimpleHTTPRequestHandler):
- def log_message(self, fmt: str, *args) -> None:
- # Default fmt is like '%s - - [%s] %s' — include client address for debugging.
- try:
- line = fmt % args if args else fmt
- except (TypeError, ValueError):
- line = f"{fmt} {args}"
- peer_ip = self.client_address[0] if self.client_address else "?"
- peer_port = self.client_address[1] if len(self.client_address) > 1 else "?"
- print(f"[http] peer={peer_ip}:{peer_port} | {line.strip()}")
-
- http.server.HTTPServer((host, port), Handler).serve_forever()
-
-
-def main() -> None:
- ap = argparse.ArgumentParser(description="ZKAC demo HTTP + TCP server")
- ap.add_argument(
- "--creds-dir",
- type=Path,
- default=Path(__file__).resolve().parent / "creds",
- )
- ap.add_argument("--http-host", default="127.0.0.1")
- ap.add_argument("--http-port", type=int, default=8765)
- ap.add_argument("--zkac-host", default="127.0.0.1")
- ap.add_argument("--zkac-port", type=int, default=9876)
- args = ap.parse_args()
- creds_dir: Path = args.creds_dir
- if not (creds_dir / "transport.json").is_file():
- raise SystemExit(f"Missing {creds_dir}/transport.json — run setup_demo.py first.")
-
- # Epoch must match the member files issued at setup (any member file is enough).
- member = json.loads((creds_dir / "member_analyst.json").read_text(encoding="utf-8"))
- epoch = int(member["epoch"])
- registry = load_registry(creds_dir, epoch)
-
- static_root = Path(__file__).resolve().parent / "static"
- if not static_root.is_dir():
- raise SystemExit(f"Missing static directory: {static_root}")
-
- http_thread = threading.Thread(
- target=run_http,
- args=(args.http_host, args.http_port, static_root),
- daemon=True,
- )
- http_thread.start()
- print(
- f"HTTP http://{args.http_host}:{args.http_port}/ (static demo page)\n"
- f"ZKAC {args.zkac_host}:{args.zkac_port} (authenticated /api over TCP)"
- )
-
- zkac_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- zkac_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- zkac_sock.bind((args.zkac_host, args.zkac_port))
- zkac_sock.listen(8)
- while True:
- conn, addr = zkac_sock.accept()
- threading.Thread(
- target=handle_zkac_client,
- args=(conn, addr, creds_dir, registry),
- daemon=True,
- ).start()
-
-
-if __name__ == "__main__":
- main()
diff --git a/demo/server_managed.py b/demo/server_managed.py
new file mode 100644
index 0000000..d33f232
--- /dev/null
+++ b/demo/server_managed.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+"""
+ZKAC TCP server using client-managed registries.
+
+Loads a registry from managed_registry.json (created by setup_managed_demo.py),
+verifies BBS+ state certificates, and authenticates clients against the registry.
+Also handles credential issuance requests via the E2E-encrypted relay.
+
+Run setup_managed_demo.py first, then this server, then client_managed.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import json
+import socket
+import struct
+import threading
+import traceback
+from pathlib import Path
+
+import zkac
+from zkac.tcp import (
+ FramedSession,
+ read_frame,
+ write_frame,
+ server_handshake_managed,
+)
+
+
+def load_managed_registry(creds_dir: Path, mgr: zkac.RegistryManager) -> bytes:
+ """Load the managed registry state + cert into the manager. Returns registry_id."""
+ r = json.loads((creds_dir / "managed_registry.json").read_text(encoding="utf-8"))
+ state_bytes = base64.b64decode(r["state_bytes_b64"])
+ state_cert = base64.b64decode(r["state_cert_b64"])
+ rid = mgr.create(state_bytes, state_cert)
+ return rid
+
+
+def _role_label(role_id: bytes) -> str:
+ for name in ("analyst", "operator"):
+ if role_id == zkac.role_id(name):
+ return name
+ return role_id.hex()[:16]
+
+
+def api_body_for_role(role_id: bytes) -> dict:
+ if role_id == zkac.role_id("analyst"):
+ return {
+ "path": "/api",
+ "role": "analyst",
+ "datasets": ["summary", "aggregated_metrics"],
+ "note": "Analyst tier: aggregated data only.",
+ "registry": "client-managed",
+ }
+ if role_id == zkac.role_id("operator"):
+ return {
+ "path": "/api",
+ "role": "operator",
+ "datasets": ["summary", "aggregated_metrics", "raw_logs", "pii"],
+ "note": "Operator tier: full access.",
+ "registry": "client-managed",
+ }
+ return {"error": "unknown role", "path": "/api"}
+
+
+def handle_client(
+ conn: socket.socket,
+ addr: tuple,
+ creds_dir: Path,
+ mgr: zkac.RegistryManager,
+) -> None:
+ peer = f"{addr[0]}:{addr[1]}"
+ print(f"[zkac-managed] connect peer={peer}")
+
+ try:
+ t = json.loads((creds_dir / "transport.json").read_text(encoding="utf-8"))
+ sk = base64.b64decode(t["server_secret_key_b64"])
+ server_kp = zkac.Keypair.from_secret_key(sk)
+ node = zkac.Node(server_kp)
+
+ session, registry_id, role_id = server_handshake_managed(conn, node, mgr)
+ label = _role_label(role_id)
+ print(
+ f"[zkac-managed] auth_ok peer={peer} registry={registry_id.hex()[:16]}… "
+ f"role={label!r}"
+ )
+
+ framed = FramedSession(conn, session)
+ raw = framed.recv()
+ req = json.loads(raw.decode("utf-8"))
+ print(f"[zkac-managed] request peer={peer} {req!r}")
+
+ path = req.get("path")
+ if path != "/api":
+ body = {"error": "unsupported path", "got": path}
+ else:
+ body = api_body_for_role(role_id)
+
+ out = json.dumps(body).encode()
+ framed.send(out)
+ print(f"[zkac-managed] response peer={peer} {list(body.keys())}")
+
+ except (ConnectionError, BrokenPipeError, OSError) as e:
+ print(f"[zkac-managed] peer={peer} connection_error: {e!r}")
+ except (json.JSONDecodeError, ValueError) as e:
+ print(f"[zkac-managed] peer={peer} protocol_error: {e!r}")
+ except Exception as e:
+ print(f"[zkac-managed] peer={peer} unexpected_error: {e!r}")
+ traceback.print_exc()
+ finally:
+ conn.close()
+
+
+def main() -> None:
+ ap = argparse.ArgumentParser(description="ZKAC managed-registry TCP server")
+ ap.add_argument("--creds-dir", type=Path,
+ default=Path(__file__).resolve().parent / "creds")
+ ap.add_argument("--host", default="127.0.0.1")
+ ap.add_argument("--port", type=int, default=9877)
+ args = ap.parse_args()
+
+ creds_dir: Path = args.creds_dir
+ if not (creds_dir / "managed_registry.json").is_file():
+ raise SystemExit(
+ f"Missing {creds_dir}/managed_registry.json — run setup_managed_demo.py first."
+ )
+
+ mgr = zkac.RegistryManager()
+ rid = load_managed_registry(creds_dir, mgr)
+ print(f"Loaded managed registry {rid.hex()[:16]}…")
+ print(f"ZKAC managed {args.host}:{args.port}")
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind((args.host, args.port))
+ sock.listen(8)
+ while True:
+ conn, addr = sock.accept()
+ threading.Thread(
+ target=handle_client,
+ args=(conn, addr, creds_dir, mgr),
+ daemon=True,
+ ).start()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/demo/setup_demo.py b/demo/setup_demo.py
deleted file mode 100644
index 95cae44..0000000
--- a/demo/setup_demo.py
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env python3
-"""
-Generate demo credentials under creds/: issuer, server transport key, two member credentials.
-Run once before starting the server.
-"""
-
-from __future__ import annotations
-
-import argparse
-import base64
-import json
-from pathlib import Path
-
-import zkac
-
-# Human-readable role names; each becomes a 32-byte opaque role_id via zkac.role_id().
-# Must stay in sync with server.py (registry + api_body_for_role).
-ROLES = ("analyst", "operator")
-
-
-def main() -> None:
- ap = argparse.ArgumentParser(description="Generate ZKAC demo credential files.")
- ap.add_argument(
- "--output-dir",
- type=Path,
- default=Path(__file__).resolve().parent / "creds",
- help="Directory to write files (default: demo/creds)",
- )
- args = ap.parse_args()
- out: Path = args.output_dir
- out.mkdir(parents=True, exist_ok=True)
-
- # BBS+ issuer: signs blind credentials; server only needs the public key in RoleRegistry.
- issuer = zkac.BbsIssuer()
- issuer_pk = issuer.public_key()
- epoch = 1
-
- # Long-term Ristretto identity for the TCP server (X25519 handshake + Schnorr identity proof).
- server_kp = zkac.Keypair()
- server_pk = server_kp.public_key()
-
- issuer_payload = {
- "issuer_secret_key_b64": base64.b64encode(issuer.secret_key_bytes()).decode(),
- "issuer_public_key_b64": base64.b64encode(issuer_pk.to_bytes()).decode(),
- }
- (out / "issuer.json").write_text(json.dumps(issuer_payload, indent=2), encoding="utf-8")
-
- transport_payload = {
- "server_secret_key_b64": base64.b64encode(server_kp.secret_key_bytes()).decode(),
- "server_public_key_b64": base64.b64encode(server_pk.to_bytes()).decode(),
- }
- (out / "transport.json").write_text(json.dumps(transport_payload, indent=2), encoding="utf-8")
-
- # One blind issuance per role: issuer never learns member_secret.
- for role_name in ROLES:
- rid = zkac.role_id(role_name)
- req = zkac.prepare_blind_request()
- blind_sig = issuer.issue_blind(req.commitment_with_proof(), rid, epoch)
- member = {
- "role_name": role_name,
- "role_id_hex": rid.hex(),
- "epoch": epoch,
- "blind_sig_b64": base64.b64encode(blind_sig).decode(),
- "member_secret_b64": base64.b64encode(req.member_secret()).decode(),
- "prover_blind_b64": base64.b64encode(req.prover_blind()).decode(),
- "issuer_public_key_b64": base64.b64encode(issuer_pk.to_bytes()).decode(),
- }
- (out / f"member_{role_name}.json").write_text(
- json.dumps(member, indent=2), encoding="utf-8"
- )
-
- print(f"Wrote issuer, transport, and member files to {out}")
- print(
- f"Roles: {', '.join(ROLES)} — use member_{ROLES[0]}.json / member_{ROLES[1]}.json with client_cli.py"
- )
-
-
-if __name__ == "__main__":
- main()
diff --git a/demo/setup_managed_demo.py b/demo/setup_managed_demo.py
new file mode 100644
index 0000000..ca65f52
--- /dev/null
+++ b/demo/setup_managed_demo.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+"""
+Generate credentials for the managed-registry demo.
+
+Creates:
+ creds/managed_admin.json – admin BBS+ issuer key + admin credential + issuance keypair
+ creds/managed_registry.json – serialized registry state + cert (analyst + operator roles)
+ creds/transport.json – server transport key (created if not already present)
+
+Users request credentials through the server at runtime (E2E-encrypted issuance).
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import json
+from pathlib import Path
+
+import zkac
+
+ROLES = ("analyst", "operator")
+
+
+def main() -> None:
+ ap = argparse.ArgumentParser(description="Generate managed-registry demo files.")
+ ap.add_argument(
+ "--output-dir",
+ type=Path,
+ default=Path(__file__).resolve().parent / "creds",
+ )
+ args = ap.parse_args()
+ out: Path = args.output_dir
+ out.mkdir(parents=True, exist_ok=True)
+
+ # Admin BBS+ issuer (signs credentials AND certifies registry state)
+ admin_issuer = zkac.BbsIssuer()
+ admin_pk = admin_issuer.public_key()
+ admin_rid = zkac.admin_role_id()
+
+ # Self-issue admin credential
+ req = zkac.prepare_blind_request()
+ sig = admin_issuer.issue_blind(req.commitment_with_proof(), admin_rid, 0)
+ admin_cred = zkac.Credential.finalize(
+ sig, req.member_secret(), req.prover_blind(), admin_rid, 0, admin_pk
+ )
+
+ # X25519 issuance keypair for E2E-encrypted credential requests
+ issuance_kp = zkac.IssuanceKeypair()
+
+ # Build registry state with roles (admin is also the issuer for all roles)
+ role_entries = []
+ for name in ROLES:
+ role_entries.append((zkac.role_id(name), admin_pk, 1))
+
+ state = zkac.RegistryState.build(
+ admin_pk, issuance_kp.public_key_bytes(), 1, b"\x00" * 32, role_entries
+ )
+ state_bytes = state.serialize()
+ state_cert = state.certify(admin_cred)
+ registry_id = state.registry_id()
+
+ # Save admin material
+ admin_payload = {
+ "admin_issuer_secret_b64": base64.b64encode(admin_issuer.secret_key_bytes()).decode(),
+ "admin_issuer_public_key_b64": base64.b64encode(admin_pk.to_bytes()).decode(),
+ "admin_member_secret_b64": base64.b64encode(req.member_secret()).decode(),
+ "admin_prover_blind_b64": base64.b64encode(req.prover_blind()).decode(),
+ "admin_blind_sig_b64": base64.b64encode(sig).decode(),
+ "issuance_secret_b64": base64.b64encode(issuance_kp.secret_bytes()).decode(),
+ "issuance_public_key_b64": base64.b64encode(issuance_kp.public_key_bytes()).decode(),
+ "registry_id_hex": registry_id.hex(),
+ }
+ (out / "managed_admin.json").write_text(json.dumps(admin_payload, indent=2), encoding="utf-8")
+
+ # Save registry state + cert
+ reg_payload = {
+ "registry_id_hex": registry_id.hex(),
+ "state_bytes_b64": base64.b64encode(state_bytes).decode(),
+ "state_cert_b64": base64.b64encode(bytes(state_cert)).decode(),
+ "admin_issuer_public_key_b64": base64.b64encode(admin_pk.to_bytes()).decode(),
+ "issuance_public_key_b64": base64.b64encode(issuance_kp.public_key_bytes()).decode(),
+ "roles": list(ROLES),
+ }
+ (out / "managed_registry.json").write_text(json.dumps(reg_payload, indent=2), encoding="utf-8")
+
+ # Transport key (create if not present)
+ transport_path = out / "transport.json"
+ if not transport_path.is_file():
+ server_kp = zkac.Keypair()
+ transport_payload = {
+ "server_secret_key_b64": base64.b64encode(server_kp.secret_key_bytes()).decode(),
+ "server_public_key_b64": base64.b64encode(server_kp.public_key().to_bytes()).decode(),
+ }
+ transport_path.write_text(json.dumps(transport_payload, indent=2), encoding="utf-8")
+
+ print(f"Wrote managed-registry demo files to {out}")
+ print(f"Registry ID: {registry_id.hex()}")
+ print(f"Roles: {', '.join(ROLES)}")
+ print(f"\nAdmin can issue credentials for these roles through the server.")
+ print(f"Users request credentials via E2E-encrypted issuance relay.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/demo/static/index.html b/demo/static/index.html
deleted file mode 100644
index 694f349..0000000
--- a/demo/static/index.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
- ZKAC demo
-
-
-
-
ZKAC demo
-
- This page is served over normal HTTP. Role-based /api data is not on this port:
- it is exposed only after a ZKAC session on the separate TCP port (BBS+ credential + encrypted transport).
-
-
-
1. Generate credentials
-
python setup_demo.py
-
Creates creds/ with issuer keys, server transport keys, and two members: analyst and operator.
-
-
2. Start the server
-
python server.py
-
HTTP (this page) defaults to 127.0.0.1:8765. ZKAC TCP defaults to 127.0.0.1:9876.