add cli
This commit is contained in:
parent
5fd0565596
commit
ccc0200472
121
cli/README.md
Normal file
121
cli/README.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# zkac-node CLI
|
||||||
|
|
||||||
|
Command-line interface for [ZKAC](../README.md) using the **Python bindings only** (`zkac` package). It runs a **registry-capable server** (management + client-managed registries + optional issuance relay) and **per-user** material under `~/.zkac/` (or `$ZKAC_HOME`).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python ≥ 3.9
|
||||||
|
- The **`zkac`** extension built and installed from the repository root, for example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/ZKAC
|
||||||
|
maturin develop
|
||||||
|
# or: pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/ZKAC/cli
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the **`zkac-node`** console script.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
| Variable | Meaning |
|
||||||
|
|------------|---------|
|
||||||
|
| `ZKAC_HOME` | Base directory for users (default: `~/.zkac`). Each user lives at `$ZKAC_HOME/<userid>/`. |
|
||||||
|
|
||||||
|
## Server vs client
|
||||||
|
|
||||||
|
- **Server** (`zkac-node serve`): a node that can **accept registry create/update** from an operator with the **`zkac.mgmt`** credential. It also serves **managed** sessions (BBS+ auth against stored client-managed registries) and optionally a **relay** port for blind issuance queues.
|
||||||
|
- **Client**: a **userid** with files under `$ZKAC_HOME/<userid>/` (transport key, registries, credentials).
|
||||||
|
|
||||||
|
## Ports (defaults)
|
||||||
|
|
||||||
|
| Port role | Default | Purpose |
|
||||||
|
|------------|---------|---------|
|
||||||
|
| Management | 7400 | ZKAC + static role `zkac.mgmt`; JSON commands (create/update registry, issuance peek/grant). |
|
||||||
|
| Managed | 7401 | ZKAC + `RegistryManager`; member proves a role in a client-managed registry. |
|
||||||
|
| Relay | 7402 | Optional **plaintext** JSON line protocol for enqueue/poll of issuance requests. Use `--relay-port 0` on `serve` to disable. Binds with `--relay-bind` (default `127.0.0.1`). |
|
||||||
|
|
||||||
|
## Layout on disk
|
||||||
|
|
||||||
|
**Per user** (`$ZKAC_HOME/<userid>/`):
|
||||||
|
|
||||||
|
- `transport.json` — Ristretto **client** transport keypair (`zkac.Keypair`).
|
||||||
|
- `profile.json` — `userid` and metadata.
|
||||||
|
- `registries/<slug>/` — one directory per logical registry:
|
||||||
|
- `admin.json` / `registry.json` — produced by `registry-init` (admin issuer material + public state + state cert).
|
||||||
|
- `roles/<name>.json` — member credential payloads for `connect`.
|
||||||
|
- `issued/` — files from `issue-member` (handoff).
|
||||||
|
- `pending/<request_id>.json` — saved by `issuance-request` until `issuance-poll` finalizes.
|
||||||
|
- `servers/<label>/server.json` — from `pin-server` (pinned server pubkey + host/ports).
|
||||||
|
|
||||||
|
**Server data** (`serve --data <dir>`):
|
||||||
|
|
||||||
|
- `transport.json` — server transport keypair.
|
||||||
|
- `mgmt_issuer.json` — BBS issuer secret for the static `zkac.mgmt` role only.
|
||||||
|
- `mgmt_member.json` — member credential for `zkac.mgmt` (give to operators who push registries).
|
||||||
|
- `registry_events.json` — append-only log to rebuild `RegistryManager` after restart (registry state; **not** the issuance queue).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Run `zkac-node --help` and `zkac-node <command> --help` for full flags.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `serve` | Start server; `--init` creates keys and mgmt credential in `--data`. |
|
||||||
|
| `user-create` | Create a new userid directory with a client transport key. |
|
||||||
|
| `pin-server` | Save server pubkey + host/ports for a user (from server’s `transport.json`). |
|
||||||
|
| `registry-init` | Offline: build a client-managed registry (`RegistryState` + state cert) under the user’s `registries/<slug>/`. |
|
||||||
|
| `registry-push` | Push create (or `--update`) to the server over the **management** port using `mgmt_member.json` from the server data dir. |
|
||||||
|
| `registry-import-public` | Copy `registry.json` from another user (public metadata for peers). |
|
||||||
|
| `connect` | Open a **managed** ZKAC session and send a JSON command (`--command`, default `whoami`). |
|
||||||
|
| `issue-member` | Admin: blind-issue a role credential to a file under `issued/` (out-of-band handoff). |
|
||||||
|
| `import-credential` | Install a member JSON into `roles/<name>.json`. |
|
||||||
|
| `issuance-request` | Enqueue a blind commitment on the **relay** (see caveats below). |
|
||||||
|
| `issuance-grant` | Admin: list pending via mgmt peek, `issue_blind`, `grant_credential`. |
|
||||||
|
| `issuance-poll` | Poll relay for blind signature; finalize credential into `roles/<role>.json` when ready. |
|
||||||
|
|
||||||
|
## Typical flows
|
||||||
|
|
||||||
|
**Operator: first-time server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ZKAC_HOME=~/.zkac # optional
|
||||||
|
zkac-node serve --data ./myserver --init --mgmt-port 7400 --managed-port 7401 --relay-port 7402
|
||||||
|
```
|
||||||
|
|
||||||
|
Distribute **`myserver/transport.json`** (public key) and **`myserver/mgmt_member.json`** (sensitive) to admins who push registries.
|
||||||
|
|
||||||
|
**Admin: create registry offline and push**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zkac-node user-create alice
|
||||||
|
zkac-node registry-init --user alice --slug demo --roles analyst reader
|
||||||
|
zkac-node registry-push --user alice --slug demo --server-data ./myserver --host 127.0.0.1 --mgmt-port 7400
|
||||||
|
```
|
||||||
|
|
||||||
|
**Member: pin server, import public registry + credential, connect**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zkac-node pin-server --user bob --name prod --transport-file ./myserver/transport.json \
|
||||||
|
--host 10.0.0.1 --mgmt-port 7400 --managed-port 7401 --relay-port 7402
|
||||||
|
zkac-node registry-import-public --from-user alice --to-user bob --slug demo
|
||||||
|
# copy or issue credential file into bob/registries/demo/roles/analyst.json
|
||||||
|
zkac-node connect --user bob --slug demo --credential roles/analyst.json \
|
||||||
|
--server-file ~/.zkac/bob/servers/prod/server.json --host 10.0.0.1 --managed-port 7401
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
1. **Issuance relay queue is in-memory.** Pending grants are lost if `serve` restarts before `issuance-grant` / `issuance-poll` complete. Registry snapshots are persisted via `registry_events.json`; the relay queue is not.
|
||||||
|
2. **Relay is a localhost-oriented, plaintext JSON line protocol** (not the full E2E design in the Rust layer). Use `--relay-bind 127.0.0.1` and do not expose the relay port untrusted networks without additional protection.
|
||||||
|
3. **`zkac.mgmt`** is a **separate** BBS role from the client-managed registry admin; it only authorizes **management RPCs** to the server (push registry, peek/grant issuance). Registry state integrity still comes from BBS+ state certificates inside `RegistryManager`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The CLI package name is **`zkac-node-cli`** (see `pyproject.toml`). It declares no PyPI dependency on `zkac`; install the `zkac` wheel from the parent project first.
|
||||||
17
cli/pyproject.toml
Normal file
17
cli/pyproject.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "zkac-node-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "ZKAC node CLI (server + client) using the zkac Python bindings"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
zkac-node = "zkac_cli.main:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["zkac_cli"]
|
||||||
3
cli/zkac_cli/__init__.py
Normal file
3
cli/zkac_cli/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""ZKAC node CLI — server (registry-capable) and client commands."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
BIN
cli/zkac_cli/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/client_ops.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/client_ops.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/creds.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/creds.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/issuance_util.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/issuance_util.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/main.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/paths.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/paths.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/registry_local.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/registry_local.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/registry_log.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/registry_log.cpython-314.pyc
Normal file
Binary file not shown.
BIN
cli/zkac_cli/__pycache__/server_app.cpython-314.pyc
Normal file
BIN
cli/zkac_cli/__pycache__/server_app.cpython-314.pyc
Normal file
Binary file not shown.
64
cli/zkac_cli/client_ops.py
Normal file
64
cli/zkac_cli/client_ops.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""TCP clients for management, managed, and relay channels."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import zkac
|
||||||
|
from zkac.tcp import FramedSession, client_handshake, client_handshake_managed
|
||||||
|
|
||||||
|
|
||||||
|
def mgmt_call(
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
server_pk: zkac.PublicKey,
|
||||||
|
mgmt_cred: zkac.Credential,
|
||||||
|
cmd: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
node = zkac.Node(zkac.Keypair())
|
||||||
|
sock = socket.create_connection((host, port))
|
||||||
|
try:
|
||||||
|
session = client_handshake(sock, node, server_pk, mgmt_cred)
|
||||||
|
framed = FramedSession(sock, session)
|
||||||
|
framed.send(json.dumps(cmd).encode("utf-8"))
|
||||||
|
return json.loads(framed.recv().decode("utf-8"))
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def managed_call(
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
server_pk: zkac.PublicKey,
|
||||||
|
cred: zkac.Credential,
|
||||||
|
registry_id: bytes,
|
||||||
|
cmd: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
node = zkac.Node(zkac.Keypair())
|
||||||
|
sock = socket.create_connection((host, port))
|
||||||
|
try:
|
||||||
|
session = client_handshake_managed(sock, node, server_pk, cred, registry_id)
|
||||||
|
framed = FramedSession(sock, session)
|
||||||
|
framed.send(json.dumps(cmd).encode("utf-8"))
|
||||||
|
return json.loads(framed.recv().decode("utf-8"))
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def relay_line(host: str, port: int, obj: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
payload = (json.dumps(obj) + "\n").encode("utf-8")
|
||||||
|
sock = socket.create_connection((host, port))
|
||||||
|
try:
|
||||||
|
sock.sendall(payload)
|
||||||
|
buf = b""
|
||||||
|
while b"\n" not in buf:
|
||||||
|
chunk = sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buf += chunk
|
||||||
|
line = buf.split(b"\n", 1)[0]
|
||||||
|
return json.loads(line.decode("utf-8"))
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
84
cli/zkac_cli/creds.py
Normal file
84
cli/zkac_cli/creds.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""Serialize / restore BBS+ member credentials and transport keys."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import zkac
|
||||||
|
|
||||||
|
|
||||||
|
def save_json(path: Path, obj: Any) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(obj, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: Path) -> Any:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def member_payload(
|
||||||
|
blind_sig: bytes,
|
||||||
|
req: zkac.BlindRequest,
|
||||||
|
role_id: bytes,
|
||||||
|
epoch: int,
|
||||||
|
issuer_pk: zkac.BbsPublicKey,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"role_id_hex": role_id.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(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_member_credential(data: dict[str, Any]) -> zkac.Credential:
|
||||||
|
pk = zkac.BbsPublicKey.from_bytes(base64.b64decode(data["issuer_public_key_b64"]))
|
||||||
|
rid = bytes.fromhex(data["role_id_hex"])
|
||||||
|
return zkac.Credential.finalize(
|
||||||
|
base64.b64decode(data["blind_sig_b64"]),
|
||||||
|
base64.b64decode(data["member_secret_b64"]),
|
||||||
|
base64.b64decode(data["prover_blind_b64"]),
|
||||||
|
rid,
|
||||||
|
int(data["epoch"]),
|
||||||
|
pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_transport_keypair(path: Path, kp: zkac.Keypair) -> None:
|
||||||
|
save_json(
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
"secret_key_b64": base64.b64encode(kp.secret_key_bytes()).decode(),
|
||||||
|
"public_key_b64": base64.b64encode(kp.public_key().to_bytes()).decode(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_transport_keypair(path: Path) -> zkac.Keypair:
|
||||||
|
t = load_json(path)
|
||||||
|
return zkac.Keypair.from_secret_key(base64.b64decode(t["secret_key_b64"]))
|
||||||
|
|
||||||
|
|
||||||
|
def save_server_transport(path: Path, kp: zkac.Keypair) -> None:
|
||||||
|
save_json(
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
"server_secret_key_b64": base64.b64encode(kp.secret_key_bytes()).decode(),
|
||||||
|
"server_public_key_b64": base64.b64encode(kp.public_key().to_bytes()).decode(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_server_public_key(path: Path) -> zkac.PublicKey:
|
||||||
|
t = load_json(path)
|
||||||
|
return zkac.PublicKey.from_bytes(base64.b64decode(t["server_public_key_b64"]))
|
||||||
|
|
||||||
|
|
||||||
|
def load_server_keypair(path: Path) -> zkac.Keypair:
|
||||||
|
t = load_json(path)
|
||||||
|
return zkac.Keypair.from_secret_key(base64.b64decode(t["server_secret_key_b64"]))
|
||||||
26
cli/zkac_cli/issuance_util.py
Normal file
26
cli/zkac_cli/issuance_util.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""Helpers around RegistryManager issuance queues (Python API drains on take)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import zkac
|
||||||
|
|
||||||
|
|
||||||
|
def peek_pending_requests(
|
||||||
|
mgr: zkac.RegistryManager, registry_id: bytes
|
||||||
|
) -> list[tuple[bytes, bytes, bytes, bytes]]:
|
||||||
|
"""Return pending (request_id, role_id, eph_pk, ciphertext) and re-queue them."""
|
||||||
|
items = mgr.take_pending_requests(registry_id)
|
||||||
|
out: list[tuple[bytes, bytes, bytes, bytes]] = []
|
||||||
|
for tup in items:
|
||||||
|
req_id, role_id, eph_pk, enc = tup
|
||||||
|
out.append((req_id, role_id, eph_pk, enc))
|
||||||
|
mgr.queue_issuance_request(registry_id, req_id, role_id, eph_pk, enc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def take_pending_requests(
|
||||||
|
mgr: zkac.RegistryManager, registry_id: bytes
|
||||||
|
) -> list[tuple[bytes, bytes, bytes, bytes]]:
|
||||||
|
"""Drain pending queue (admin processes and must grant or lose requests)."""
|
||||||
|
items = mgr.take_pending_requests(registry_id)
|
||||||
|
return [(a, b, c, d) for a, b, c, d in items]
|
||||||
416
cli/zkac_cli/main.py
Normal file
416
cli/zkac_cli/main.py
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
"""zkac-node CLI entrypoint."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import zkac
|
||||||
|
|
||||||
|
from zkac_cli import creds
|
||||||
|
from zkac_cli import client_ops
|
||||||
|
from zkac_cli import paths
|
||||||
|
from zkac_cli import registry_local
|
||||||
|
from zkac_cli import server_app
|
||||||
|
|
||||||
|
|
||||||
|
MGMT_ROLE = server_app.MGMT_ROLE
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_serve(args: argparse.Namespace) -> None:
|
||||||
|
data = Path(args.data).resolve()
|
||||||
|
if args.init or not (data / "transport.json").is_file():
|
||||||
|
data.mkdir(parents=True, exist_ok=True)
|
||||||
|
transport = zkac.Keypair()
|
||||||
|
issuer = zkac.BbsIssuer()
|
||||||
|
pk = issuer.public_key()
|
||||||
|
mg_rid = zkac.role_id(MGMT_ROLE)
|
||||||
|
req = zkac.prepare_blind_request()
|
||||||
|
sig = issuer.issue_blind(req.commitment_with_proof(), mg_rid, 1)
|
||||||
|
_ = zkac.Credential.finalize(
|
||||||
|
sig, req.member_secret(), req.prover_blind(), mg_rid, 1, pk
|
||||||
|
)
|
||||||
|
creds.save_server_transport(data / "transport.json", transport)
|
||||||
|
creds.save_json(
|
||||||
|
data / "mgmt_issuer.json",
|
||||||
|
{"issuer_secret_b64": base64.b64encode(issuer.secret_key_bytes()).decode()},
|
||||||
|
)
|
||||||
|
m = creds.member_payload(sig, req, mg_rid, 1, pk)
|
||||||
|
creds.save_json(data / "mgmt_member.json", m)
|
||||||
|
print(f"Initialized server data in {data}", flush=True)
|
||||||
|
print(
|
||||||
|
"Copy mgmt_member.json for operators; distribute server_public_key from transport.json",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
relay = None if args.relay_port == 0 else args.relay_port
|
||||||
|
relay_bind = args.relay_bind
|
||||||
|
print(
|
||||||
|
f"Starting: mgmt={args.mgmt_port} managed={args.managed_port} relay={relay!r} bind={relay_bind}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
server_app.serve(data, args.mgmt_port, args.managed_port, relay, relay_bind)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_create(args: argparse.Namespace) -> None:
|
||||||
|
uid = args.userid
|
||||||
|
d = paths.ensure_user(uid)
|
||||||
|
kp = zkac.Keypair()
|
||||||
|
creds.save_transport_keypair(d / "transport.json", kp)
|
||||||
|
creds.save_json(d / "profile.json", {"userid": uid})
|
||||||
|
print(f"User {uid!r}: wrote {d / 'transport.json'}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_registry_init(args: argparse.Namespace) -> None:
|
||||||
|
base = paths.ensure_user(args.user)
|
||||||
|
out = (base / "registries" / args.slug).resolve()
|
||||||
|
r = registry_local.create_registry_bundle(args.slug, list(args.roles), out)
|
||||||
|
print(json.dumps(r, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_registry_push(args: argparse.Namespace) -> None:
|
||||||
|
server_data = Path(args.server_data).resolve()
|
||||||
|
reg_dir = (paths.user_dir(args.user) / "registries" / args.slug).resolve()
|
||||||
|
reg = creds.load_json(reg_dir / "registry.json")
|
||||||
|
mg = creds.load_json(server_data / "mgmt_member.json")
|
||||||
|
t = creds.load_json(server_data / "transport.json")
|
||||||
|
server_pk = zkac.PublicKey.from_bytes(base64.b64decode(t["server_public_key_b64"]))
|
||||||
|
mgmt_cred = creds.load_member_credential(mg)
|
||||||
|
cmd = {"cmd": "create_registry", "state_b64": reg["state_bytes_b64"], "state_cert_b64": reg["state_cert_b64"]}
|
||||||
|
if args.update:
|
||||||
|
cmd = {"cmd": "update_registry", "state_b64": reg["state_bytes_b64"], "state_cert_b64": reg["state_cert_b64"]}
|
||||||
|
out = client_ops.mgmt_call(args.host, args.mgmt_port, server_pk, mgmt_cred, cmd)
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_connect(args: argparse.Namespace) -> None:
|
||||||
|
reg_dir = (paths.user_dir(args.user) / "registries" / args.slug).resolve()
|
||||||
|
reg_path = reg_dir / "registry.json"
|
||||||
|
if reg_path.is_file():
|
||||||
|
reg = creds.load_json(reg_path)
|
||||||
|
elif args.registry_id:
|
||||||
|
reg = {"registry_id_hex": args.registry_id}
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"Missing registry.json. Copy from the admin (public) or pass --registry-id.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
cred_path = (reg_dir / args.credential).resolve()
|
||||||
|
m = creds.load_json(cred_path)
|
||||||
|
credential = creds.load_member_credential(m)
|
||||||
|
server = creds.load_json(Path(args.server_file).expanduser().resolve())
|
||||||
|
server_pk = zkac.PublicKey.from_bytes(base64.b64decode(server["server_public_key_b64"]))
|
||||||
|
registry_id = bytes.fromhex(reg["registry_id_hex"])
|
||||||
|
body = client_ops.managed_call(
|
||||||
|
args.host,
|
||||||
|
args.managed_port,
|
||||||
|
server_pk,
|
||||||
|
credential,
|
||||||
|
registry_id,
|
||||||
|
{"cmd": args.command},
|
||||||
|
)
|
||||||
|
print(json.dumps(body, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issuance_request(args: argparse.Namespace) -> None:
|
||||||
|
"""Queue a blind issuance request (payload is commitment bytes; localhost relay only)."""
|
||||||
|
reg = creds.load_json(paths.user_dir(args.user) / "registries" / args.slug / "registry.json")
|
||||||
|
issuance_pk = base64.b64decode(reg["issuance_public_key_b64"])
|
||||||
|
req = zkac.prepare_blind_request()
|
||||||
|
payload = req.commitment_with_proof()
|
||||||
|
req_id = secrets.token_bytes(32)
|
||||||
|
eph_pk = secrets.token_bytes(32)
|
||||||
|
# Store locally for finalize
|
||||||
|
pending = {
|
||||||
|
"request_id_hex": req_id.hex(),
|
||||||
|
"role_name": args.role,
|
||||||
|
"member_secret_b64": base64.b64encode(req.member_secret()).decode(),
|
||||||
|
"prover_blind_b64": base64.b64encode(req.prover_blind()).decode(),
|
||||||
|
"issuer_public_key_b64": reg["admin_issuer_public_key_b64"],
|
||||||
|
"epoch": 1,
|
||||||
|
}
|
||||||
|
pdir = paths.user_dir(args.user) / "pending"
|
||||||
|
pdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
creds.save_json(pdir / f"{req_id.hex()}.json", pending)
|
||||||
|
|
||||||
|
obj = {
|
||||||
|
"cmd": "enqueue",
|
||||||
|
"registry_id_hex": reg["registry_id_hex"],
|
||||||
|
"role_name": args.role,
|
||||||
|
"request_id_hex": req_id.hex(),
|
||||||
|
"eph_pk_hex": eph_pk.hex(),
|
||||||
|
"payload_b64": base64.b64encode(payload).decode(),
|
||||||
|
}
|
||||||
|
out = client_ops.relay_line(args.host, args.relay_port, obj)
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
print(f"Saved pending material to {pdir / (req_id.hex() + '.json')}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issuance_poll(args: argparse.Namespace) -> None:
|
||||||
|
reg = creds.load_json(paths.user_dir(args.user) / "registries" / args.slug / "registry.json")
|
||||||
|
obj = {
|
||||||
|
"cmd": "poll",
|
||||||
|
"registry_id_hex": reg["registry_id_hex"],
|
||||||
|
"request_id_hex": args.request_id,
|
||||||
|
}
|
||||||
|
out = client_ops.relay_line(args.host, args.relay_port, obj)
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
if out.get("status") == "ready":
|
||||||
|
pend = creds.load_json(paths.user_dir(args.user) / "pending" / f"{args.request_id}.json")
|
||||||
|
pk = zkac.BbsPublicKey.from_bytes(base64.b64decode(pend["issuer_public_key_b64"]))
|
||||||
|
rid = zkac.role_id(pend["role_name"])
|
||||||
|
blind_sig = base64.b64decode(out["blind_sig_b64"])
|
||||||
|
zkac.Credential.finalize(
|
||||||
|
blind_sig,
|
||||||
|
base64.b64decode(pend["member_secret_b64"]),
|
||||||
|
base64.b64decode(pend["prover_blind_b64"]),
|
||||||
|
rid,
|
||||||
|
int(pend["epoch"]),
|
||||||
|
pk,
|
||||||
|
)
|
||||||
|
out_dir = paths.user_dir(args.user) / "registries" / args.slug / "roles"
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
member = {
|
||||||
|
"role_id_hex": rid.hex(),
|
||||||
|
"epoch": int(pend["epoch"]),
|
||||||
|
"blind_sig_b64": base64.b64encode(blind_sig).decode(),
|
||||||
|
"member_secret_b64": pend["member_secret_b64"],
|
||||||
|
"prover_blind_b64": pend["prover_blind_b64"],
|
||||||
|
"issuer_public_key_b64": pend["issuer_public_key_b64"],
|
||||||
|
}
|
||||||
|
dest = out_dir / f"{pend['role_name']}.json"
|
||||||
|
creds.save_json(dest, member)
|
||||||
|
print(f"You have role {pend['role_name']!r}; credential saved to {dest}")
|
||||||
|
print("Use: zkac-node connect ... --credential roles/{0}.json".format(pend["role_name"]))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issuance_grant(args: argparse.Namespace) -> None:
|
||||||
|
server_data = Path(args.server_data).resolve()
|
||||||
|
reg_dir = (paths.user_dir(args.admin_user) / "registries" / args.slug).resolve()
|
||||||
|
admin = creds.load_json(reg_dir / "admin.json")
|
||||||
|
issuer = zkac.BbsIssuer.from_secret_key(base64.b64decode(admin["admin_issuer_secret_b64"]))
|
||||||
|
t = creds.load_json(server_data / "transport.json")
|
||||||
|
mg = creds.load_json(server_data / "mgmt_member.json")
|
||||||
|
server_pk = zkac.PublicKey.from_bytes(base64.b64decode(t["server_public_key_b64"]))
|
||||||
|
mgmt_cred = creds.load_member_credential(mg)
|
||||||
|
|
||||||
|
peek = client_ops.mgmt_call(
|
||||||
|
args.host,
|
||||||
|
args.mgmt_port,
|
||||||
|
server_pk,
|
||||||
|
mgmt_cred,
|
||||||
|
{"cmd": "issuance_peek", "registry_id_hex": args.registry_id},
|
||||||
|
)
|
||||||
|
pending = peek.get("pending") or []
|
||||||
|
target = None
|
||||||
|
for p in pending:
|
||||||
|
if p["request_id_hex"] == args.request_id:
|
||||||
|
target = p
|
||||||
|
break
|
||||||
|
if target is None:
|
||||||
|
print("request_id not in pending queue", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
commit = base64.b64decode(target["payload_b64"])
|
||||||
|
role_id = bytes.fromhex(target["role_id_hex"])
|
||||||
|
blind = issuer.issue_blind(commit, role_id, 1)
|
||||||
|
out = client_ops.mgmt_call(
|
||||||
|
args.host,
|
||||||
|
args.mgmt_port,
|
||||||
|
server_pk,
|
||||||
|
mgmt_cred,
|
||||||
|
{
|
||||||
|
"cmd": "issuance_grant",
|
||||||
|
"registry_id_hex": args.registry_id,
|
||||||
|
"request_id_hex": args.request_id,
|
||||||
|
"blind_sig_b64": base64.b64encode(blind).decode(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issue_member_file(args: argparse.Namespace) -> None:
|
||||||
|
"""Admin issues a role credential locally (out-of-band handoff)."""
|
||||||
|
reg_dir = (paths.user_dir(args.admin_user) / "registries" / args.slug).resolve()
|
||||||
|
admin = creds.load_json(reg_dir / "admin.json")
|
||||||
|
issuer = zkac.BbsIssuer.from_secret_key(base64.b64decode(admin["admin_issuer_secret_b64"]))
|
||||||
|
pk = issuer.public_key()
|
||||||
|
rid = zkac.role_id(args.role)
|
||||||
|
req = zkac.prepare_blind_request()
|
||||||
|
sig = issuer.issue_blind(req.commitment_with_proof(), rid, 1)
|
||||||
|
payload = creds.member_payload(sig, req, rid, 1, pk)
|
||||||
|
out_path = reg_dir / "issued" / f"{args.role}_{args.target_user}.json"
|
||||||
|
creds.save_json(out_path, payload)
|
||||||
|
print(f"Wrote {out_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_registry_import_public(args: argparse.Namespace) -> None:
|
||||||
|
"""Copy public registry.json (state + cert summary) from another local user."""
|
||||||
|
src = (paths.user_dir(args.from_user) / "registries" / args.slug / "registry.json").resolve()
|
||||||
|
dst_dir = paths.ensure_user(args.to_user) / "registries" / args.slug
|
||||||
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy(src, dst_dir / "registry.json")
|
||||||
|
print(f"Copied {src} -> {dst_dir / 'registry.json'}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_import_credential(args: argparse.Namespace) -> None:
|
||||||
|
src = Path(args.file).resolve()
|
||||||
|
data = creds.load_json(src)
|
||||||
|
dest = paths.user_dir(args.user) / "registries" / args.slug / "roles" / f"{args.name}.json"
|
||||||
|
creds.save_json(dest, data)
|
||||||
|
print(f"Imported credential to {dest}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_pin_server(args: argparse.Namespace) -> None:
|
||||||
|
"""Save server public key + ports for a user (from server's transport.json)."""
|
||||||
|
tpath = Path(args.transport_file).resolve()
|
||||||
|
t = creds.load_json(tpath)
|
||||||
|
d = paths.ensure_user(args.user) / "servers" / args.name
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
creds.save_json(
|
||||||
|
d / "server.json",
|
||||||
|
{
|
||||||
|
"server_public_key_b64": t["server_public_key_b64"],
|
||||||
|
"host": args.host,
|
||||||
|
"mgmt_port": args.mgmt_port,
|
||||||
|
"managed_port": args.managed_port,
|
||||||
|
"relay_port": args.relay_port,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f"Pinned server {args.name!r} for user {args.user!r} -> {d / 'server.json'}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(
|
||||||
|
prog="zkac-node",
|
||||||
|
description="ZKAC node CLI (server + client). "
|
||||||
|
"Requires the 'zkac' package from this repo (maturin develop). "
|
||||||
|
"Set ZKAC_HOME to override ~/.zkac/. "
|
||||||
|
"Issuance relay queues are in-memory — use the same server process between request and grant.",
|
||||||
|
)
|
||||||
|
sub = p.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
ps = sub.add_parser("serve", help="Run server (registry-capable node)")
|
||||||
|
ps.add_argument("--data", type=Path, required=True, help="Server data directory")
|
||||||
|
ps.add_argument("--init", action="store_true", help="Initialize data dir (transport + mgmt issuer)")
|
||||||
|
ps.add_argument("--mgmt-port", type=int, default=7400)
|
||||||
|
ps.add_argument("--managed-port", type=int, default=7401)
|
||||||
|
ps.add_argument(
|
||||||
|
"--relay-port",
|
||||||
|
type=int,
|
||||||
|
default=7402,
|
||||||
|
help="Plaintext relay for issuance queue (localhost only). Use 0 to disable.",
|
||||||
|
)
|
||||||
|
ps.add_argument("--relay-bind", default="127.0.0.1")
|
||||||
|
ps.set_defaults(func=cmd_serve)
|
||||||
|
|
||||||
|
su = sub.add_parser("user-create", help="Create a user directory under ~/.zkac/")
|
||||||
|
su.add_argument("userid")
|
||||||
|
su.set_defaults(func=cmd_user_create)
|
||||||
|
|
||||||
|
pi = sub.add_parser("pin-server", help="Save server connection info for a user")
|
||||||
|
pi.add_argument("--user", required=True)
|
||||||
|
pi.add_argument("--name", required=True, help="Label for this server")
|
||||||
|
pi.add_argument("--transport-file", required=True, help="Path to server's transport.json")
|
||||||
|
pi.add_argument("--host", required=True)
|
||||||
|
pi.add_argument("--mgmt-port", type=int, default=7400)
|
||||||
|
pi.add_argument("--managed-port", type=int, default=7401)
|
||||||
|
pi.add_argument("--relay-port", type=int, default=7402)
|
||||||
|
pi.set_defaults(func=cmd_pin_server)
|
||||||
|
|
||||||
|
ri = sub.add_parser("registry-init", help="Create a client-managed registry offline")
|
||||||
|
ri.add_argument("--user", required=True)
|
||||||
|
ri.add_argument("--slug", required=True)
|
||||||
|
ri.add_argument("--roles", nargs="+", required=True)
|
||||||
|
ri.set_defaults(func=cmd_registry_init)
|
||||||
|
|
||||||
|
rp = sub.add_parser("registry-push", help="Push registry snapshot to server (mgmt channel)")
|
||||||
|
rp.add_argument("--user", required=True)
|
||||||
|
rp.add_argument("--slug", required=True)
|
||||||
|
rp.add_argument("--server-data", type=Path, required=True, help="Server data dir (transport + mgmt_member)")
|
||||||
|
rp.add_argument("--host", default="127.0.0.1")
|
||||||
|
rp.add_argument("--mgmt-port", type=int, default=7400)
|
||||||
|
rp.add_argument("--update", action="store_true", help="Send update_registry instead of create")
|
||||||
|
rp.set_defaults(func=cmd_registry_push)
|
||||||
|
|
||||||
|
cc = sub.add_parser("connect", help="Managed ZKAC session test (whoami/get_registry)")
|
||||||
|
cc.add_argument("--user", required=True)
|
||||||
|
cc.add_argument("--slug", required=True)
|
||||||
|
cc.add_argument("--credential", default="roles/member.json", help="Relative path under registries/<slug>/")
|
||||||
|
cc.add_argument(
|
||||||
|
"--registry-id",
|
||||||
|
default="",
|
||||||
|
help="If registry.json is absent, hex registry id (pin public registry first)",
|
||||||
|
)
|
||||||
|
cc.add_argument("--server-file", required=True, help="Pinned server.json from pin-server")
|
||||||
|
cc.add_argument("--host", required=True)
|
||||||
|
cc.add_argument("--managed-port", type=int, default=7401)
|
||||||
|
cc.add_argument("--command", default="whoami")
|
||||||
|
cc.set_defaults(func=cmd_connect)
|
||||||
|
|
||||||
|
ir = sub.add_parser(
|
||||||
|
"issuance-request",
|
||||||
|
help="Enqueue blind commitment on relay (localhost; see help text)",
|
||||||
|
)
|
||||||
|
ir.add_argument("--user", required=True)
|
||||||
|
ir.add_argument("--slug", required=True)
|
||||||
|
ir.add_argument("--role", required=True)
|
||||||
|
ir.add_argument("--host", default="127.0.0.1")
|
||||||
|
ir.add_argument("--relay-port", type=int, default=7402)
|
||||||
|
ir.set_defaults(func=cmd_issuance_request)
|
||||||
|
|
||||||
|
ip = sub.add_parser("issuance-poll", help="Poll relay for blind signature")
|
||||||
|
ip.add_argument("--user", required=True)
|
||||||
|
ip.add_argument("--slug", required=True)
|
||||||
|
ip.add_argument("--request-id", required=True, dest="request_id")
|
||||||
|
ip.add_argument("--host", default="127.0.0.1")
|
||||||
|
ip.add_argument("--relay-port", type=int, default=7402)
|
||||||
|
ip.set_defaults(func=cmd_issuance_poll)
|
||||||
|
|
||||||
|
ig = sub.add_parser("issuance-grant", help="Admin: issue blind sig and push grant to server")
|
||||||
|
ig.add_argument("--admin-user", required=True)
|
||||||
|
ig.add_argument("--slug", required=True)
|
||||||
|
ig.add_argument("--server-data", type=Path, required=True)
|
||||||
|
ig.add_argument("--registry-id", required=True, dest="registry_id")
|
||||||
|
ig.add_argument("--request-id", required=True, dest="request_id")
|
||||||
|
ig.add_argument("--host", default="127.0.0.1")
|
||||||
|
ig.add_argument("--mgmt-port", type=int, default=7400)
|
||||||
|
ig.set_defaults(func=cmd_issuance_grant)
|
||||||
|
|
||||||
|
im = sub.add_parser("issue-member", help="Admin: issue credential to file for handoff")
|
||||||
|
im.add_argument("--admin-user", required=True)
|
||||||
|
im.add_argument("--slug", required=True)
|
||||||
|
im.add_argument("--role", required=True)
|
||||||
|
im.add_argument("--target-user", required=True)
|
||||||
|
im.set_defaults(func=cmd_issue_member_file)
|
||||||
|
|
||||||
|
irp = sub.add_parser(
|
||||||
|
"registry-import-public",
|
||||||
|
help="Copy registry.json from another user (public state + cert metadata)",
|
||||||
|
)
|
||||||
|
irp.add_argument("--from-user", required=True, dest="from_user")
|
||||||
|
irp.add_argument("--to-user", required=True, dest="to_user")
|
||||||
|
irp.add_argument("--slug", required=True)
|
||||||
|
irp.set_defaults(func=cmd_registry_import_public)
|
||||||
|
|
||||||
|
ii = sub.add_parser("import-credential", help="Copy a member json into a user's registry folder")
|
||||||
|
ii.add_argument("--user", required=True)
|
||||||
|
ii.add_argument("--slug", required=True)
|
||||||
|
ii.add_argument("--name", required=True, help="Filename base (e.g. analyst)")
|
||||||
|
ii.add_argument("file", type=Path)
|
||||||
|
ii.set_defaults(func=cmd_import_credential)
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = build_parser().parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
19
cli/zkac_cli/paths.py
Normal file
19
cli/zkac_cli/paths.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def zkac_home() -> Path:
|
||||||
|
return Path(os.environ.get("ZKAC_HOME", Path.home() / ".zkac"))
|
||||||
|
|
||||||
|
|
||||||
|
def user_dir(userid: str) -> Path:
|
||||||
|
return zkac_home() / userid
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_user(userid: str) -> Path:
|
||||||
|
zkac_home().mkdir(parents=True, exist_ok=True)
|
||||||
|
d = user_dir(userid)
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
return d
|
||||||
68
cli/zkac_cli/registry_local.py
Normal file
68
cli/zkac_cli/registry_local.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""Offline client-managed registry creation (same structure as demo/setup_managed_demo)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import zkac
|
||||||
|
|
||||||
|
from zkac_cli import creds
|
||||||
|
|
||||||
|
|
||||||
|
def create_registry_bundle(
|
||||||
|
slug: str,
|
||||||
|
role_names: list[str],
|
||||||
|
out_dir: Path,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Build a new registry with admin issuer = role issuer for all listed roles.
|
||||||
|
Writes admin.json, registry.json under out_dir.
|
||||||
|
"""
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
admin_issuer = zkac.BbsIssuer()
|
||||||
|
admin_pk = admin_issuer.public_key()
|
||||||
|
admin_rid = zkac.admin_role_id()
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
issuance_kp = zkac.IssuanceKeypair()
|
||||||
|
role_entries = [(zkac.role_id(name), admin_pk, 1) for name in role_names]
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
admin_payload = {
|
||||||
|
"slug": slug,
|
||||||
|
"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(),
|
||||||
|
}
|
||||||
|
creds.save_json(out_dir / "admin.json", admin_payload)
|
||||||
|
|
||||||
|
reg_payload = {
|
||||||
|
"slug": slug,
|
||||||
|
"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": role_names,
|
||||||
|
}
|
||||||
|
creds.save_json(out_dir / "registry.json", reg_payload)
|
||||||
|
|
||||||
|
return {"registry_id_hex": registry_id.hex(), "path": str(out_dir)}
|
||||||
59
cli/zkac_cli/registry_log.py
Normal file
59
cli/zkac_cli/registry_log.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""Persist RegistryManager by replaying create/update events."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import zkac
|
||||||
|
|
||||||
|
|
||||||
|
def save_events(path: Path, events: list[dict[str, Any]]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps({"events": events}, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def load_events(path: Path) -> list[dict[str, Any]]:
|
||||||
|
if not path.is_file():
|
||||||
|
return []
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
return list(data.get("events", []))
|
||||||
|
|
||||||
|
|
||||||
|
def replay_manager(events: list[dict[str, Any]]) -> zkac.RegistryManager:
|
||||||
|
"""Rebuild manager from persisted events (multiple registries supported)."""
|
||||||
|
mgr = zkac.RegistryManager()
|
||||||
|
by_rid: dict[bytes, list[dict[str, Any]]] = {}
|
||||||
|
for e in events:
|
||||||
|
st = base64.b64decode(e["state_b64"])
|
||||||
|
st_obj = zkac.RegistryState.deserialize(st)
|
||||||
|
rid = st_obj.registry_id()
|
||||||
|
by_rid.setdefault(rid, []).append(e)
|
||||||
|
for rid, evs in by_rid.items():
|
||||||
|
evs.sort(
|
||||||
|
key=lambda e: zkac.RegistryState.deserialize(
|
||||||
|
base64.b64decode(e["state_b64"])
|
||||||
|
).version()
|
||||||
|
)
|
||||||
|
for i, e in enumerate(evs):
|
||||||
|
st = base64.b64decode(e["state_b64"])
|
||||||
|
cert = base64.b64decode(e["state_cert_b64"])
|
||||||
|
if i == 0:
|
||||||
|
mgr.create(st, cert)
|
||||||
|
else:
|
||||||
|
mgr.update(rid, st, cert)
|
||||||
|
return mgr
|
||||||
|
|
||||||
|
|
||||||
|
def append_event(path: Path, op: str, state_bytes: bytes, state_cert: bytes) -> None:
|
||||||
|
events = load_events(path)
|
||||||
|
events.append(
|
||||||
|
{
|
||||||
|
"op": op,
|
||||||
|
"state_b64": base64.b64encode(state_bytes).decode(),
|
||||||
|
"state_cert_b64": base64.b64encode(state_cert).decode(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
save_events(path, events)
|
||||||
272
cli/zkac_cli/server_app.py
Normal file
272
cli/zkac_cli/server_app.py
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
"""TCP servers: ZKAC management, ZKAC managed app, localhost relay (credential queue)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import zkac
|
||||||
|
from zkac.tcp import FramedSession, server_handshake, server_handshake_managed
|
||||||
|
|
||||||
|
from zkac_cli import issuance_util
|
||||||
|
from zkac_cli import registry_log
|
||||||
|
|
||||||
|
|
||||||
|
MGMT_ROLE = "zkac.mgmt"
|
||||||
|
|
||||||
|
|
||||||
|
def build_static_registry(issuer_pk: zkac.BbsPublicKey) -> zkac.RoleRegistry:
|
||||||
|
reg = zkac.RoleRegistry()
|
||||||
|
reg.register_role(zkac.role_id(MGMT_ROLE), issuer_pk, 1)
|
||||||
|
return reg
|
||||||
|
|
||||||
|
|
||||||
|
def handle_mgmt_session(
|
||||||
|
conn: socket.socket,
|
||||||
|
data_dir: Path,
|
||||||
|
mgr: zkac.RegistryManager,
|
||||||
|
registry_ids: set[bytes],
|
||||||
|
events_path: Path,
|
||||||
|
save_registry: Callable[[], None],
|
||||||
|
) -> None:
|
||||||
|
from zkac_cli import creds
|
||||||
|
|
||||||
|
framed = None
|
||||||
|
try:
|
||||||
|
sk = creds.load_server_keypair(data_dir / "transport.json")
|
||||||
|
node = zkac.Node(sk)
|
||||||
|
issuer = zkac.BbsIssuer.from_secret_key(
|
||||||
|
base64.b64decode(creds.load_json(data_dir / "mgmt_issuer.json")["issuer_secret_b64"])
|
||||||
|
)
|
||||||
|
static_reg = build_static_registry(issuer.public_key())
|
||||||
|
session, role_id = server_handshake(conn, node, static_reg)
|
||||||
|
if role_id != zkac.role_id(MGMT_ROLE):
|
||||||
|
return
|
||||||
|
framed = FramedSession(conn, session)
|
||||||
|
raw = framed.recv()
|
||||||
|
msg = json.loads(raw.decode("utf-8"))
|
||||||
|
cmd = msg["cmd"]
|
||||||
|
if cmd == "create_registry":
|
||||||
|
st = base64.b64decode(msg["state_b64"])
|
||||||
|
cert = base64.b64decode(msg["state_cert_b64"])
|
||||||
|
rid = mgr.create(st, cert)
|
||||||
|
registry_ids.add(rid)
|
||||||
|
registry_log.append_event(events_path, "create", st, cert)
|
||||||
|
save_registry()
|
||||||
|
out = {"ok": True, "registry_id_hex": rid.hex()}
|
||||||
|
elif cmd == "update_registry":
|
||||||
|
st = base64.b64decode(msg["state_b64"])
|
||||||
|
cert = base64.b64decode(msg["state_cert_b64"])
|
||||||
|
st_o = zkac.RegistryState.deserialize(st)
|
||||||
|
rid = st_o.registry_id()
|
||||||
|
mgr.update(rid, st, cert)
|
||||||
|
registry_log.append_event(events_path, "update", st, cert)
|
||||||
|
save_registry()
|
||||||
|
out = {"ok": True, "registry_id_hex": rid.hex()}
|
||||||
|
elif cmd == "get_registry":
|
||||||
|
rid = bytes.fromhex(msg["registry_id_hex"])
|
||||||
|
st, c = mgr.get(rid)
|
||||||
|
out = {
|
||||||
|
"ok": True,
|
||||||
|
"state_b64": base64.b64encode(st).decode(),
|
||||||
|
"state_cert_b64": base64.b64encode(c).decode(),
|
||||||
|
}
|
||||||
|
elif cmd == "list_registries":
|
||||||
|
out = {"ok": True, "registry_ids_hex": [h.hex() for h in sorted(registry_ids)]}
|
||||||
|
elif cmd == "issuance_peek":
|
||||||
|
rid = bytes.fromhex(msg["registry_id_hex"])
|
||||||
|
pending = issuance_util.peek_pending_requests(mgr, rid)
|
||||||
|
out = {
|
||||||
|
"ok": True,
|
||||||
|
"pending": [
|
||||||
|
{
|
||||||
|
"request_id_hex": a.hex(),
|
||||||
|
"role_id_hex": b.hex(),
|
||||||
|
"eph_pk_hex": c.hex(),
|
||||||
|
"payload_b64": base64.b64encode(d).decode(),
|
||||||
|
}
|
||||||
|
for a, b, c, d in pending
|
||||||
|
],
|
||||||
|
}
|
||||||
|
elif cmd == "issuance_grant":
|
||||||
|
rid = bytes.fromhex(msg["registry_id_hex"])
|
||||||
|
req_id = bytes.fromhex(msg["request_id_hex"])
|
||||||
|
blind = base64.b64decode(msg["blind_sig_b64"])
|
||||||
|
mgr.grant_credential(rid, req_id, blind)
|
||||||
|
save_registry()
|
||||||
|
out = {"ok": True}
|
||||||
|
else:
|
||||||
|
out = {"ok": False, "error": f"unknown cmd {cmd}"}
|
||||||
|
framed.send(json.dumps(out).encode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_managed_handler(
|
||||||
|
conn: socket.socket,
|
||||||
|
data_dir: Path,
|
||||||
|
mgr: zkac.RegistryManager,
|
||||||
|
) -> None:
|
||||||
|
from zkac_cli import creds
|
||||||
|
|
||||||
|
try:
|
||||||
|
sk = creds.load_server_keypair(data_dir / "transport.json")
|
||||||
|
node = zkac.Node(sk)
|
||||||
|
session, registry_id, role_id = server_handshake_managed(conn, node, mgr)
|
||||||
|
framed = FramedSession(conn, session)
|
||||||
|
raw = framed.recv()
|
||||||
|
msg = json.loads(raw.decode("utf-8"))
|
||||||
|
cmd = msg.get("cmd", "ping")
|
||||||
|
if cmd == "get_registry":
|
||||||
|
rid = bytes.fromhex(msg["registry_id_hex"])
|
||||||
|
st, c = mgr.get(rid)
|
||||||
|
body = {
|
||||||
|
"ok": True,
|
||||||
|
"state_b64": base64.b64encode(st).decode(),
|
||||||
|
"state_cert_b64": base64.b64encode(c).decode(),
|
||||||
|
}
|
||||||
|
elif cmd == "whoami":
|
||||||
|
body = {
|
||||||
|
"ok": True,
|
||||||
|
"registry_id_hex": registry_id.hex(),
|
||||||
|
"role_id_hex": role_id.hex(),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
body = {
|
||||||
|
"ok": True,
|
||||||
|
"registry_id_hex": registry_id.hex(),
|
||||||
|
"role_id_hex": role_id.hex(),
|
||||||
|
"note": "authenticated managed session",
|
||||||
|
}
|
||||||
|
framed.send(json.dumps(body).encode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_relay_session(
|
||||||
|
conn: socket.socket,
|
||||||
|
mgr: zkac.RegistryManager,
|
||||||
|
save_registry: Callable[[], None],
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
buf = b""
|
||||||
|
while b"\n" not in buf:
|
||||||
|
chunk = conn.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
return
|
||||||
|
buf += chunk
|
||||||
|
line, _, _ = buf.partition(b"\n")
|
||||||
|
msg = json.loads(line.decode("utf-8"))
|
||||||
|
cmd = msg["cmd"]
|
||||||
|
if cmd == "enqueue":
|
||||||
|
rid = bytes.fromhex(msg["registry_id_hex"])
|
||||||
|
role = zkac.role_id(msg["role_name"])
|
||||||
|
req_id = bytes.fromhex(msg["request_id_hex"])
|
||||||
|
eph = bytes.fromhex(msg["eph_pk_hex"])
|
||||||
|
blob = base64.b64decode(msg["payload_b64"])
|
||||||
|
mgr.queue_issuance_request(rid, req_id, role, eph, blob)
|
||||||
|
save_registry()
|
||||||
|
out = {"ok": True, "status": "queued"}
|
||||||
|
elif cmd == "poll":
|
||||||
|
rid = bytes.fromhex(msg["registry_id_hex"])
|
||||||
|
req_id = bytes.fromhex(msg["request_id_hex"])
|
||||||
|
g = mgr.take_granted_credential(rid, req_id)
|
||||||
|
save_registry()
|
||||||
|
if g is None:
|
||||||
|
out = {"ok": True, "status": "pending"}
|
||||||
|
else:
|
||||||
|
out = {
|
||||||
|
"ok": True,
|
||||||
|
"status": "ready",
|
||||||
|
"blind_sig_b64": base64.b64encode(g).decode(),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
out = {"ok": False, "error": "unknown relay cmd"}
|
||||||
|
conn.sendall((json.dumps(out) + "\n").encode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def serve(
|
||||||
|
data_dir: Path,
|
||||||
|
mgmt_port: int,
|
||||||
|
managed_port: int,
|
||||||
|
relay_port: int | None,
|
||||||
|
relay_bind: str,
|
||||||
|
) -> None:
|
||||||
|
events_path = data_dir / "registry_events.json"
|
||||||
|
registry_ids: set[bytes] = set()
|
||||||
|
if events_path.is_file():
|
||||||
|
mgr = registry_log.replay_manager(registry_log.load_events(events_path))
|
||||||
|
for e in registry_log.load_events(events_path):
|
||||||
|
st = base64.b64decode(e["state_b64"])
|
||||||
|
registry_ids.add(zkac.RegistryState.deserialize(st).registry_id())
|
||||||
|
else:
|
||||||
|
mgr = zkac.RegistryManager()
|
||||||
|
|
||||||
|
def save_registry() -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def mgmt_loop() -> None:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(("0.0.0.0", mgmt_port))
|
||||||
|
s.listen(8)
|
||||||
|
print(f"[mgmt] ZKAC listening on 0.0.0.0:{mgmt_port}")
|
||||||
|
while True:
|
||||||
|
c, a = s.accept()
|
||||||
|
print(f"[mgmt] connect {a}")
|
||||||
|
threading.Thread(
|
||||||
|
target=handle_mgmt_session,
|
||||||
|
args=(c, data_dir, mgr, registry_ids, events_path, save_registry),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def managed_loop() -> None:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(("0.0.0.0", managed_port))
|
||||||
|
s.listen(8)
|
||||||
|
print(f"[managed] ZKAC listening on 0.0.0.0:{managed_port}")
|
||||||
|
while True:
|
||||||
|
c, a = s.accept()
|
||||||
|
print(f"[managed] connect {a}")
|
||||||
|
threading.Thread(
|
||||||
|
target=run_managed_handler,
|
||||||
|
args=(c, data_dir, mgr),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def relay_loop() -> None:
|
||||||
|
if relay_port is None:
|
||||||
|
return
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind((relay_bind, relay_port))
|
||||||
|
s.listen(8)
|
||||||
|
print(f"[relay] plaintext JSON lines on {relay_bind}:{relay_port}")
|
||||||
|
while True:
|
||||||
|
c, a = s.accept()
|
||||||
|
print(f"[relay] connect {a}")
|
||||||
|
threading.Thread(
|
||||||
|
target=handle_relay_session,
|
||||||
|
args=(c, mgr, save_registry),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
threading.Thread(target=mgmt_loop, daemon=True).start()
|
||||||
|
threading.Thread(target=managed_loop, daemon=True).start()
|
||||||
|
if relay_port is not None:
|
||||||
|
threading.Thread(target=relay_loop, daemon=True).start()
|
||||||
|
threading.Event().wait()
|
||||||
Loading…
x
Reference in New Issue
Block a user