"""CLI entry point for zkac-node.""" from __future__ import annotations import argparse import base64 import json import sys from . import client, store from .paths import user_dir # ── user ────────────────────────────────────────────────────────────── def _cmd_user_create(args): path = store.create_user(args.userid) ident = store.load_identity(args.userid) print(f"created user {args.userid!r}") print(f" issuance public key: {ident['issuance_pk'].hex()}") print(f" (share this key out-of-band to receive credentials)") print(f" stored in {path}") def _cmd_user_list(_args): users = store.list_users() if not users: print("no users found") return for u in users: print(u) def _cmd_user_show(args): ident = store.load_identity(args.userid) print(f"user: {args.userid}") print(f" issuance pk: {ident['issuance_pk'].hex()}") owned = [ {"registry_id": r, **store.load_admin(args.userid, r)} for r in store.list_admin_registries(args.userid) ] if owned: print(f" registries owned: {len(owned)}") for r in owned: print(f" {r['registry_id']} @ {r.get('server', '?')} roles={r.get('roles', [])}") creds = store.list_credentials(args.userid) if creds: print(f" credentials: {len(creds)}") for reg_hex, role in creds: print(f" {reg_hex[:16]}… / {role}") # ── serve ───────────────────────────────────────────────────────────── def _cmd_serve(args): from .server import serve data_dir = args.data_dir if data_dir is None: data_dir = str(user_dir(args.userid) / "server") serve(data_dir, args.host, args.port) # ── server pin ─────────────────────────────────────────────────────── def _cmd_server_pin(args): pk_bytes = bytes.fromhex(args.key) store.pin_server(args.userid, args.server, base64.b64encode(pk_bytes).decode()) print(f"pinned {args.server} for {args.userid} -> {args.key[:16]}…") # ── registry ────────────────────────────────────────────────────────── def _cmd_registry_create(args): roles = [r.strip() for r in args.roles.split(",")] rid = client.create_registry(args.userid, args.server, roles) print(f"registry created: {rid}") print(f" roles: {', '.join(roles)}") def _cmd_registry_update(args): add = [r.strip() for r in args.add_roles.split(",")] client.update_registry(args.userid, args.server, args.registry, add) print(f"registry updated: {args.registry[:16]}…") print(f" added roles: {', '.join(add)}") def _cmd_registry_get(args): info = client.get_registry(args.userid, args.server, args.registry) print(f"registry: {args.registry}") print(f" state bytes: {len(info.get('state_bytes_b64', ''))} chars (b64)") def _cmd_registry_list(args): regs = client.list_own_registries(args.userid) if not regs: print("no registries") return for r in regs: print(f" {r['registry_id']} @ {r['server']} roles={r['roles']}") # ── grant ───────────────────────────────────────────────────────────── def _cmd_grant(args): gid, pool_index = client.grant( args.userid, args.server, args.registry, args.role, args.to, ) print(f"granted {args.role!r} to {args.to[:16]}…") print(f" grant id: {gid}") print(f" pool index: {pool_index}") print(f" recipient can collect with:") print( f" zkac-node collect {args.server}:{args.registry}:{args.role} " f"--pir-peer --pool-index {pool_index}" ) # ── credentials / collect ─────────────────────────────────────────── def _cmd_credentials_list(args): local = store.list_credentials(args.userid) print("local credentials:") if not local: print(" (none)") for reg_hex, role in local: print(f" {reg_hex}:{role}") servers = list(args.server or []) for s in store.known_servers(args.userid): if s not in servers: servers.append(s) if not servers: print("\n(no servers to query; pass --server host:port to check for pending)") return if args.pir_peer is None: print( "\n(skipping pending grants: two-server XOR PIR requires " "--pir-peer ; local credentials are listed above)" ) return print("\npending grants (PIR scan, O(n) per server):") any_pending = False for srv in servers: try: grants = client.list_pending(args.userid, srv, args.pir_peer) except Exception as exc: print(f" [{srv}] error: {exc}") continue for g in grants: any_pending = True rid = g.get("registry_id", "?") role = g.get("role_name", "?") pidx = g.get("pool_index") idx_s = f" idx={pidx}" if pidx is not None else "" if rid != "?" and role != "?" and store.has_credential(args.userid, rid, role): note = " (already collected locally)" else: note = "" print(f" {srv}:{rid}:{role}{idx_s}{note}") if not any_pending: print(" (none)") def _cmd_collect(args): result = client.collect( args.userid, args.spec, pir_peer=args.pir_peer, pool_index=args.pool_index, ) print("collected credential") print(f" registry: {result['registry_id']}") print(f" role: {result['role']}") print(f" server: {result['server']}") # ── auth ────────────────────────────────────────────────────────────── def _cmd_auth(args): resp = client.authenticate( args.userid, args.registry, args.role, server=args.server, ) print(json.dumps(resp, indent=2)) # ── argparse ───────────────────────────────────────────────────────── def main(): p = argparse.ArgumentParser(prog="zkac-node") sub = p.add_subparsers(dest="group", required=True) # user user_p = sub.add_parser("user", help="manage local user identities") user_sub = user_p.add_subparsers(dest="action", required=True) c = user_sub.add_parser("create", help="generate a new user identity") c.add_argument("userid") c.set_defaults(func=_cmd_user_create) c = user_sub.add_parser("list", help="list all local users") c.set_defaults(func=_cmd_user_list) c = user_sub.add_parser("show", help="show user keys + registries + credentials") c.add_argument("userid") c.set_defaults(func=_cmd_user_show) # serve c = sub.add_parser("serve", help="run as a ZKAC server node") c.add_argument("userid", help="user whose ~/.zkac//server/ holds server state (unless --data-dir)") c.add_argument("--data-dir", default=None, help="override server data directory") c.add_argument("--host", default="127.0.0.1") c.add_argument("--port", type=int, default=9800) c.set_defaults(func=_cmd_serve) # server pin srv_p = sub.add_parser("server", help="manage server pins") srv_sub = srv_p.add_subparsers(dest="action", required=True) c = srv_sub.add_parser("pin", help="pin a server's public key for a user") c.add_argument("userid") c.add_argument("server", help="host:port") c.add_argument("--key", required=True, help="server public key (hex)") c.set_defaults(func=_cmd_server_pin) # registry reg_p = sub.add_parser("registry", help="manage registries") reg_sub = reg_p.add_subparsers(dest="action", required=True) c = reg_sub.add_parser("create", help="create a new registry on a server") c.add_argument("userid") c.add_argument("server", help="host:port") c.add_argument("--roles", required=True, help="comma-separated role names") c.set_defaults(func=_cmd_registry_create) c = reg_sub.add_parser("update", help="add roles to a registry you own") c.add_argument("userid") c.add_argument("server", help="host:port") c.add_argument("--registry", required=True) c.add_argument("--add-roles", required=True, help="comma-separated new roles") c.set_defaults(func=_cmd_registry_update) c = reg_sub.add_parser("get", help="fetch registry state from server") c.add_argument("userid") c.add_argument("server", help="host:port") c.add_argument("--registry", required=True) c.set_defaults(func=_cmd_registry_get) c = reg_sub.add_parser("list", help="list locally owned registries") c.add_argument("userid") c.set_defaults(func=_cmd_registry_list) # grant c = sub.add_parser("grant", help="issue a credential to a recipient (admin)") c.add_argument("userid") c.add_argument("--server", required=True, help="host:port") c.add_argument("--registry", required=True) c.add_argument("--role", required=True) c.add_argument("--to", required=True, help="recipient issuance public key (hex)") c.set_defaults(func=_cmd_grant) # credentials cred_p = sub.add_parser("credentials", help="credentials (local + pending)") cred_sub = cred_p.add_subparsers(dest="action", required=True) c = cred_sub.add_parser("list", help="show local + pending credentials") c.add_argument("userid") c.add_argument("--server", action="append", help="server to query (host:port); repeatable") c.add_argument( "--pir-peer", default=None, metavar="HOST:PORT", help="second replica with the same grant pool; required to list pending via XOR PIR", ) c.set_defaults(func=_cmd_credentials_list) # collect c = sub.add_parser("collect", help="fetch and finalize a pending credential") c.add_argument("userid") c.add_argument("spec", help="host:port:registry_id:role") c.add_argument( "--pir-peer", required=True, metavar="HOST:PORT", help="second replica with an identical grant pool (two-server XOR PIR)", ) c.add_argument( "--pool-index", type=int, required=True, help="grant row index from admin (printed on grant)", ) c.set_defaults(func=_cmd_collect) # auth c = sub.add_parser("auth", help="authenticate with a credential") c.add_argument("userid") c.add_argument("--registry", required=True) c.add_argument("--role", required=True) c.add_argument("--server", default=None, help="host:port (optional if known from registry)") c.set_defaults(func=_cmd_auth) args = p.parse_args() if not hasattr(args, "func"): p.print_help() sys.exit(1) args.func(args) if __name__ == "__main__": main()