ZKAC/demo/client_cli.py
2026-04-15 11:32:01 +02:00

90 lines
2.9 KiB
Python

#!/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()