ZKAC/cli/zkac_cli/main.py
everbarry 6e67836e95 v0.4
2026-04-18 01:06:12 +02:00

315 lines
11 KiB
Python

"""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 <userid> {args.server}:{args.registry}:{args.role} "
f"--pir-peer <SECOND_REPLICA> --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 <second-replica>; 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/<userid>/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()