p2p-grants #1
@ -37,7 +37,7 @@ python -c "import zkac; print(zkac.role_id('admin').hex())"
|
|||||||
|
|
||||||
Run tests: `cargo test` and `pytest tests/test_zkac.py`.
|
Run tests: `cargo test` and `pytest tests/test_zkac.py`.
|
||||||
|
|
||||||
Local web UI over the CLI: `demo/cli_web_server.py` (see `demo/README.md`). Optional in-browser WASM for the legacy Flask demo: `wasm/README.md` and `./demo/build_wasm.sh` (needs **rustup** + `wasm32-unknown-unknown`).
|
Local web UI over the CLI: `demo/cli_web_server.py` (see `demo/README.md`). Optional in-browser WASM for the Flask demo: `wasm/README.md` and `./demo/build_wasm.sh` (needs **rustup** + `wasm32-unknown-unknown`).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ zkac-node auth bob --registry <REGISTRY_ID> --role analyst --server localhost:98
|
|||||||
| `user create <id>` | Generate issuance keypair under `~/.zkac/<id>/` |
|
| `user create <id>` | Generate issuance keypair under `~/.zkac/<id>/` |
|
||||||
| `user list` | List all local user ids |
|
| `user list` | List all local user ids |
|
||||||
| `user show <id>` | Show issuance pk + owned registries + credentials |
|
| `user show <id>` | Show issuance pk + owned registries + credentials |
|
||||||
| `serve <id> [--data-dir D]` | Run server; default data dir is `~/.zkac/<id>/server/` |
|
| `serve <id> [--data-dir D]` | Run server; default data dir is `~/.zkac/<id>/server/` (loopback-only unless `--allow-non-loopback`) |
|
||||||
| `zkac-node-i2p-server <id> [--host H --port P]` | Same as `serve`, for I2P server-tunnel exposure (see below) |
|
| `zkac-node-i2p-server <id> [--host H --port P]` | Same as `serve`, for I2P server-tunnel exposure (see below) |
|
||||||
| `server pin <id> <host:port> --key <hex>` | Pin server public key for that user |
|
| `server pin <id> <host:port> --key <hex>` | Pin server public key for that user |
|
||||||
| `registry create <id> <server> --roles …` | Create registry on server |
|
| `registry create <id> <server> --roles …` | Create registry on server |
|
||||||
@ -53,7 +53,7 @@ zkac-node auth bob --registry <REGISTRY_ID> --role analyst --server localhost:98
|
|||||||
| `registry get <id> <server> --registry R` | Fetch registry state |
|
| `registry get <id> <server> --registry R` | Fetch registry state |
|
||||||
| `registry list <id>` | List registries this user owns locally |
|
| `registry list <id>` | List registries this user owns locally |
|
||||||
| `grant <id> --server S --registry R --role X --to <blob>` | Admin direct p2p grant (recipient contact bundle must include peer endpoint) |
|
| `grant <id> --server S --registry R --role X --to <blob>` | Admin direct p2p grant (recipient contact bundle must include peer endpoint) |
|
||||||
| `p2p-listen <id> [--host H --port P]` | Receive one direct p2p grant and store credential |
|
| `p2p-listen <id> [--host H --port P]` | Receive one direct p2p grant and store credential (loopback-only unless `--allow-non-loopback`) |
|
||||||
| `credentials list <id>` | List local credentials |
|
| `credentials list <id>` | List local credentials |
|
||||||
| `auth <id> --registry R --role X [--server S]` | Authenticated session |
|
| `auth <id> --registry R --role X [--server S]` | Authenticated session |
|
||||||
|
|
||||||
@ -92,6 +92,8 @@ Outbound client connections can be proxied through an I2P SOCKS tunnel.
|
|||||||
export ZKAC_SOCKS5_PROXY=127.0.0.1:4447
|
export ZKAC_SOCKS5_PROXY=127.0.0.1:4447
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the destination ends in `.i2p` and `ZKAC_SOCKS5_PROXY` is not set, `zkac-node` fails fast instead of attempting a direct clearnet connect.
|
||||||
|
|
||||||
3. Use `.b32.i2p:port` endpoints with normal commands:
|
3. Use `.b32.i2p:port` endpoints with normal commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -101,6 +103,8 @@ zkac-node auth bob --registry <REGISTRY_ID> --role analyst --server exampledesti
|
|||||||
|
|
||||||
For inbound direct grants, run `p2p-listen` on a local port and expose it via an I2P inbound tunnel. Peers dial your published I2P destination/port; your tunnel forwards to local `host:port`.
|
For inbound direct grants, run `p2p-listen` on a local port and expose it via an I2P inbound tunnel. Peers dial your published I2P destination/port; your tunnel forwards to local `host:port`.
|
||||||
|
|
||||||
|
`p2p-listen` now refuses non-loopback binds by default (use `--allow-non-loopback` only when intentional).
|
||||||
|
|
||||||
Connectivity sanity check:
|
Connectivity sanity check:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -126,6 +130,6 @@ identity.json issuance keypair
|
|||||||
p2p transport keypair
|
p2p transport keypair
|
||||||
admin/<registry_id>.json BBS+ admin material for owned registries
|
admin/<registry_id>.json BBS+ admin material for owned registries
|
||||||
credentials/<rid>_<role>.json received credentials
|
credentials/<rid>_<role>.json received credentials
|
||||||
servers/<host_port>.json pinned server public keys
|
servers/sha256_<...>.json pinned server public keys (includes original server string)
|
||||||
server/ (only if you run `serve <userid>`) server_key.json, registries/
|
server/ (only if you run `serve <userid>`) server_key.json, registries/
|
||||||
```
|
```
|
||||||
|
|||||||
@ -14,6 +14,8 @@ from zkac.tcp import FramedSession, client_handshake_anon, server_handshake_anon
|
|||||||
|
|
||||||
from . import store
|
from . import store
|
||||||
|
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_S = 8.0
|
||||||
|
|
||||||
|
|
||||||
def _b64(data: bytes) -> str:
|
def _b64(data: bytes) -> str:
|
||||||
return base64.b64encode(data).decode()
|
return base64.b64encode(data).decode()
|
||||||
@ -66,6 +68,28 @@ def _is_i2p_host(host: str) -> bool:
|
|||||||
return host.strip().lower().endswith(".i2p")
|
return host.strip().lower().endswith(".i2p")
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_i2p_pin_hint(userid: str, server: str) -> str:
|
||||||
|
host, _port = _parse_server(server)
|
||||||
|
is_i2p_target = _is_i2p_host(host)
|
||||||
|
known = store.known_servers(userid)
|
||||||
|
candidates: list[str] = []
|
||||||
|
for known_server in known:
|
||||||
|
try:
|
||||||
|
known_host, _ = _parse_server(known_server)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if _is_i2p_host(known_host) == is_i2p_target:
|
||||||
|
continue
|
||||||
|
candidates.append(known_server)
|
||||||
|
if not candidates:
|
||||||
|
return ""
|
||||||
|
choices = ", ".join(sorted(candidates)[:3])
|
||||||
|
return (
|
||||||
|
" You appear to have pins under a different address style "
|
||||||
|
f"({choices}). Pins are exact host:port keys; pin and connect with the same endpoint string."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
while len(buf) < n:
|
while len(buf) < n:
|
||||||
@ -103,13 +127,24 @@ def _socks5_connect(sock: socket.socket, host: str, port: int):
|
|||||||
raise RuntimeError("invalid SOCKS5 address type in reply")
|
raise RuntimeError("invalid SOCKS5 address type in reply")
|
||||||
|
|
||||||
|
|
||||||
def _connect(host: str, port: int) -> socket.socket:
|
def _connect_with_timeout(address: tuple[str, int], timeout_s: float = DEFAULT_CONNECT_TIMEOUT_S) -> socket.socket:
|
||||||
|
sock = socket.create_connection(address, timeout=timeout_s)
|
||||||
|
sock.settimeout(None)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _connect(host: str, port: int, *, connect_timeout_s: float = DEFAULT_CONNECT_TIMEOUT_S) -> socket.socket:
|
||||||
proxy = _proxy_target()
|
proxy = _proxy_target()
|
||||||
|
if _is_i2p_host(host) and proxy is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"destination is .i2p but ZKAC_SOCKS5_PROXY is not set. "
|
||||||
|
"Set ZKAC_SOCKS5_PROXY=127.0.0.1:4447 (or your I2P SOCKS endpoint)."
|
||||||
|
)
|
||||||
# Only route through SOCKS5 for I2P destinations. This keeps local/direct
|
# Only route through SOCKS5 for I2P destinations. This keeps local/direct
|
||||||
# peer delivery (e.g. 127.0.0.1) working even when ZKAC_SOCKS5_PROXY is set.
|
# peer delivery (e.g. 127.0.0.1) working even when ZKAC_SOCKS5_PROXY is set.
|
||||||
if proxy is None or not _is_i2p_host(host):
|
if proxy is None or not _is_i2p_host(host):
|
||||||
return socket.create_connection((host, port))
|
return _connect_with_timeout((host, port), connect_timeout_s)
|
||||||
sock = socket.create_connection(proxy)
|
sock = _connect_with_timeout(proxy, connect_timeout_s)
|
||||||
try:
|
try:
|
||||||
_socks5_connect(sock, host, port)
|
_socks5_connect(sock, host, port)
|
||||||
return sock
|
return sock
|
||||||
@ -129,6 +164,16 @@ def net_check(
|
|||||||
"""Connectivity diagnostic for direct/server endpoints, with optional SOCKS5 and handshake."""
|
"""Connectivity diagnostic for direct/server endpoints, with optional SOCKS5 and handshake."""
|
||||||
host, port = _parse_server(target)
|
host, port = _parse_server(target)
|
||||||
proxy = _proxy_target()
|
proxy = _proxy_target()
|
||||||
|
if _is_i2p_host(host) and proxy is None:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"target": target,
|
||||||
|
"via": "direct",
|
||||||
|
"error": (
|
||||||
|
"destination is .i2p but ZKAC_SOCKS5_PROXY is not set; "
|
||||||
|
"configure your I2P SOCKS proxy (for example 127.0.0.1:4447)"
|
||||||
|
),
|
||||||
|
}
|
||||||
use_proxy = proxy is not None and _is_i2p_host(host)
|
use_proxy = proxy is not None and _is_i2p_host(host)
|
||||||
via = "direct" if not use_proxy else f"socks5:{proxy[0]}:{proxy[1]}"
|
via = "direct" if not use_proxy else f"socks5:{proxy[0]}:{proxy[1]}"
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
@ -182,11 +227,13 @@ def parse_spec(spec: str) -> tuple[str, str, str]:
|
|||||||
def _resolve_server_pk(userid: str, server: str) -> zkac.PublicKey:
|
def _resolve_server_pk(userid: str, server: str) -> zkac.PublicKey:
|
||||||
pin = store.load_server_pin(userid, server)
|
pin = store.load_server_pin(userid, server)
|
||||||
if pin is None:
|
if pin is None:
|
||||||
|
hint = _maybe_i2p_pin_hint(userid, server)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"no pinned transport key for {server!r} under client {userid!r} "
|
f"no pinned transport key for {server!r} under client {userid!r} "
|
||||||
f"(pins are per client identity in ~/.zkac/{userid}/, not the userid "
|
f"(pins are per client identity in ~/.zkac/{userid}/, not the userid "
|
||||||
"passed to `zkac-node serve <…>` on the server). "
|
"passed to `zkac-node serve <…>` on the server). "
|
||||||
f"Run: zkac-node server pin {userid} {server} --key <hex>"
|
f"Run: zkac-node server pin {userid} {server} --key <hex>."
|
||||||
|
f"{hint}"
|
||||||
)
|
)
|
||||||
return zkac.PublicKey.from_bytes(_unb64(pin["server_public_key_b64"]))
|
return zkac.PublicKey.from_bytes(_unb64(pin["server_public_key_b64"]))
|
||||||
|
|
||||||
@ -199,7 +246,7 @@ def _local_node(userid: str) -> zkac.Node:
|
|||||||
|
|
||||||
def _mgmt_connect(userid: str, server: str) -> tuple[socket.socket, FramedSession, bytes]:
|
def _mgmt_connect(userid: str, server: str) -> tuple[socket.socket, FramedSession, bytes]:
|
||||||
host, port = _parse_server(server)
|
host, port = _parse_server(server)
|
||||||
sock = _connect(host, port)
|
sock = _connect(host, port, connect_timeout_s=DEFAULT_CONNECT_TIMEOUT_S)
|
||||||
server_pk = _resolve_server_pk(userid, server)
|
server_pk = _resolve_server_pk(userid, server)
|
||||||
node = _local_node(userid)
|
node = _local_node(userid)
|
||||||
session = client_handshake_anon(sock, node, server_pk)
|
session = client_handshake_anon(sock, node, server_pk)
|
||||||
@ -457,7 +504,7 @@ def grant_p2p(
|
|||||||
)
|
)
|
||||||
host, port = _parse_server(peer)
|
host, port = _parse_server(peer)
|
||||||
peer_transport_pk = zkac.PublicKey.from_bytes(bytes.fromhex(peer_transport_pk_hex))
|
peer_transport_pk = zkac.PublicKey.from_bytes(bytes.fromhex(peer_transport_pk_hex))
|
||||||
sock = _connect(host, port)
|
sock = _connect(host, port, connect_timeout_s=DEFAULT_CONNECT_TIMEOUT_S)
|
||||||
try:
|
try:
|
||||||
session = client_handshake_anon(sock, _local_node(userid), peer_transport_pk)
|
session = client_handshake_anon(sock, _local_node(userid), peer_transport_pk)
|
||||||
framed = FramedSession(sock, session)
|
framed = FramedSession(sock, session)
|
||||||
@ -585,7 +632,7 @@ def authenticate(userid: str, registry_id_hex: str, role_name: str,
|
|||||||
node = zkac.Node(zkac.Keypair())
|
node = zkac.Node(zkac.Keypair())
|
||||||
host, port = _parse_server(server)
|
host, port = _parse_server(server)
|
||||||
|
|
||||||
sock = _connect(host, port)
|
sock = _connect(host, port, connect_timeout_s=DEFAULT_CONNECT_TIMEOUT_S)
|
||||||
try:
|
try:
|
||||||
session = client_handshake_anon(sock, node, server_pk)
|
session = client_handshake_anon(sock, node, server_pk)
|
||||||
framed = FramedSession(sock, session)
|
framed = FramedSession(sock, session)
|
||||||
|
|||||||
@ -55,6 +55,11 @@ use to connect (the .b32.i2p:port form, not localhost).
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="suppress I2P tunnel reminder text",
|
help="suppress I2P tunnel reminder text",
|
||||||
)
|
)
|
||||||
|
p.add_argument(
|
||||||
|
"--allow-non-loopback",
|
||||||
|
action="store_true",
|
||||||
|
help="allow --host values outside loopback (default blocks non-loopback binds)",
|
||||||
|
)
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
data_dir = args.data_dir
|
data_dir = args.data_dir
|
||||||
@ -71,7 +76,7 @@ use to connect (the .b32.i2p:port form, not localhost).
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
serve(data_dir, args.host, args.port)
|
serve(data_dir, args.host, args.port, allow_non_loopback=args.allow_non_loopback)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -11,6 +11,11 @@ from . import client, store
|
|||||||
from .paths import user_dir
|
from .paths import user_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _is_loopback_host(host: str) -> bool:
|
||||||
|
value = host.strip().lower()
|
||||||
|
return value in {"127.0.0.1", "::1", "localhost"}
|
||||||
|
|
||||||
|
|
||||||
# ── user ──────────────────────────────────────────────────────────────
|
# ── user ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _cmd_user_create(args):
|
def _cmd_user_create(args):
|
||||||
@ -73,6 +78,7 @@ def _cmd_serve(args):
|
|||||||
max_connections=args.max_connections,
|
max_connections=args.max_connections,
|
||||||
idle_timeout_s=args.idle_timeout,
|
idle_timeout_s=args.idle_timeout,
|
||||||
listen_backlog=args.listen_backlog,
|
listen_backlog=args.listen_backlog,
|
||||||
|
allow_non_loopback=args.allow_non_loopback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -174,6 +180,13 @@ def _cmd_credentials_list(args):
|
|||||||
|
|
||||||
|
|
||||||
def _cmd_p2p_listen(args):
|
def _cmd_p2p_listen(args):
|
||||||
|
if not _is_loopback_host(args.host):
|
||||||
|
if not args.allow_non_loopback:
|
||||||
|
raise RuntimeError(
|
||||||
|
"refusing to bind p2p-listen outside loopback. "
|
||||||
|
"Use --allow-non-loopback only when exposure is intentional."
|
||||||
|
)
|
||||||
|
print(f"warning: binding p2p-listen outside loopback: {args.host}:{args.port}", file=sys.stderr)
|
||||||
print(f"listening for p2p grant on {args.host}:{args.port}")
|
print(f"listening for p2p grant on {args.host}:{args.port}")
|
||||||
result = client.receive_p2p(args.userid, args.host, args.port, timeout_s=args.timeout)
|
result = client.receive_p2p(args.userid, args.host, args.port, timeout_s=args.timeout)
|
||||||
print("received credential")
|
print("received credential")
|
||||||
@ -241,8 +254,13 @@ def main():
|
|||||||
c.add_argument("--host", default="127.0.0.1")
|
c.add_argument("--host", default="127.0.0.1")
|
||||||
c.add_argument("--port", type=int, default=9800)
|
c.add_argument("--port", type=int, default=9800)
|
||||||
c.add_argument("--max-connections", type=int, default=64, help="max concurrent client connections")
|
c.add_argument("--max-connections", type=int, default=64, help="max concurrent client connections")
|
||||||
c.add_argument("--idle-timeout", type=float, default=30.0, help="per-connection idle timeout seconds")
|
c.add_argument("--idle-timeout", type=float, default=45.0, help="per-connection idle timeout seconds")
|
||||||
c.add_argument("--listen-backlog", type=int, default=64, help="TCP listen backlog")
|
c.add_argument("--listen-backlog", type=int, default=64, help="TCP listen backlog")
|
||||||
|
c.add_argument(
|
||||||
|
"--allow-non-loopback",
|
||||||
|
action="store_true",
|
||||||
|
help="allow --host values outside loopback (default blocks non-loopback binds)",
|
||||||
|
)
|
||||||
c.set_defaults(func=_cmd_serve)
|
c.set_defaults(func=_cmd_serve)
|
||||||
|
|
||||||
# server pin
|
# server pin
|
||||||
@ -312,6 +330,11 @@ def main():
|
|||||||
c.add_argument("--host", default="127.0.0.1")
|
c.add_argument("--host", default="127.0.0.1")
|
||||||
c.add_argument("--port", type=int, default=9810)
|
c.add_argument("--port", type=int, default=9810)
|
||||||
c.add_argument("--timeout", type=float, default=60.0, help="max wait for authenticated sender")
|
c.add_argument("--timeout", type=float, default=60.0, help="max wait for authenticated sender")
|
||||||
|
c.add_argument(
|
||||||
|
"--allow-non-loopback",
|
||||||
|
action="store_true",
|
||||||
|
help="allow --host values outside loopback (default blocks non-loopback binds)",
|
||||||
|
)
|
||||||
c.set_defaults(func=_cmd_p2p_listen)
|
c.set_defaults(func=_cmd_p2p_listen)
|
||||||
|
|
||||||
# auth
|
# auth
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -36,6 +37,11 @@ def _write_private_json(path: Path, payload: dict):
|
|||||||
_chmod_if_possible(path, 0o600)
|
_chmod_if_possible(path, 0o600)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_loopback_host(host: str) -> bool:
|
||||||
|
value = host.strip().lower()
|
||||||
|
return value in {"127.0.0.1", "::1", "localhost"}
|
||||||
|
|
||||||
|
|
||||||
# ── Opaque server storage ─────────────────────────────────────────────
|
# ── Opaque server storage ─────────────────────────────────────────────
|
||||||
|
|
||||||
class _ServerStore:
|
class _ServerStore:
|
||||||
@ -280,10 +286,11 @@ def serve(
|
|||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 9800,
|
port: int = 9800,
|
||||||
max_connections: int = 64,
|
max_connections: int = 64,
|
||||||
idle_timeout_s: float = 30.0,
|
idle_timeout_s: float = 45.0,
|
||||||
listen_backlog: int = 64,
|
listen_backlog: int = 64,
|
||||||
*,
|
*,
|
||||||
debug: ServerDebugState | None = None,
|
debug: ServerDebugState | None = None,
|
||||||
|
allow_non_loopback: bool = False,
|
||||||
):
|
):
|
||||||
dd = Path(data_dir)
|
dd = Path(data_dir)
|
||||||
dd.mkdir(parents=True, exist_ok=True)
|
dd.mkdir(parents=True, exist_ok=True)
|
||||||
@ -305,6 +312,18 @@ def serve(
|
|||||||
print(f"loaded {n} registries")
|
print(f"loaded {n} registries")
|
||||||
print(f"listening on {host}:{port}")
|
print(f"listening on {host}:{port}")
|
||||||
|
|
||||||
|
if not _is_loopback_host(host):
|
||||||
|
if not allow_non_loopback:
|
||||||
|
raise RuntimeError(
|
||||||
|
"refusing to bind outside loopback. "
|
||||||
|
"Use --allow-non-loopback only when you intentionally expose this listener."
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"[warning] binding outside loopback: {host}:{port}. "
|
||||||
|
"Ensure network exposure is intentional.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sock.bind((host, port))
|
sock.bind((host, port))
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
@ -69,17 +70,6 @@ def create_user(userid: str) -> Path:
|
|||||||
def load_identity(userid: str) -> dict:
|
def load_identity(userid: str) -> dict:
|
||||||
p = _ud(userid) / "identity.json"
|
p = _ud(userid) / "identity.json"
|
||||||
data = json.loads(p.read_text())
|
data = json.loads(p.read_text())
|
||||||
changed = False
|
|
||||||
if "transport_secret_b64" not in data or "transport_public_b64" not in data:
|
|
||||||
transport_kp = zkac.Keypair()
|
|
||||||
data["transport_secret_b64"] = _b64(transport_kp.secret_key_bytes())
|
|
||||||
data["transport_public_b64"] = _b64(transport_kp.public_key().to_bytes())
|
|
||||||
changed = True
|
|
||||||
if "grant_token_b64" not in data:
|
|
||||||
data["grant_token_b64"] = _b64(secrets.token_bytes(32))
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
_write_private_json(p, data)
|
|
||||||
return {
|
return {
|
||||||
"issuance_sk": _unb64(data["issuance_secret_b64"]),
|
"issuance_sk": _unb64(data["issuance_secret_b64"]),
|
||||||
"issuance_pk": _unb64(data["issuance_public_b64"]),
|
"issuance_pk": _unb64(data["issuance_public_b64"]),
|
||||||
@ -166,20 +156,25 @@ def list_users() -> list[str]:
|
|||||||
# ── Server pins (per user) ───────────────────────────────────────────
|
# ── Server pins (per user) ───────────────────────────────────────────
|
||||||
|
|
||||||
def _server_key(server: str) -> str:
|
def _server_key(server: str) -> str:
|
||||||
return server.replace(":", "_")
|
digest = hashlib.sha256(server.encode("utf-8")).hexdigest()
|
||||||
|
return f"sha256_{digest}"
|
||||||
|
|
||||||
|
|
||||||
def pin_server(userid: str, server: str, server_pk_b64: str):
|
def pin_server(userid: str, server: str, server_pk_b64: str):
|
||||||
d = _ud(userid) / "servers"
|
d = _ud(userid) / "servers"
|
||||||
_ensure_private_dir(d)
|
_ensure_private_dir(d)
|
||||||
_write_private_json(d / f"{_server_key(server)}.json", {"server_public_key_b64": server_pk_b64})
|
_write_private_json(
|
||||||
|
d / f"{_server_key(server)}.json",
|
||||||
|
{"server": server, "server_public_key_b64": server_pk_b64},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_server_pin(userid: str, server: str) -> dict | None:
|
def load_server_pin(userid: str, server: str) -> dict | None:
|
||||||
p = _ud(userid) / "servers" / f"{_server_key(server)}.json"
|
d = _ud(userid) / "servers"
|
||||||
if not p.exists():
|
new_path = d / f"{_server_key(server)}.json"
|
||||||
return None
|
if new_path.exists():
|
||||||
return json.loads(p.read_text())
|
return json.loads(new_path.read_text())
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def known_servers(userid: str) -> list[str]:
|
def known_servers(userid: str) -> list[str]:
|
||||||
@ -188,9 +183,13 @@ def known_servers(userid: str) -> list[str]:
|
|||||||
d = _ud(userid) / "servers"
|
d = _ud(userid) / "servers"
|
||||||
if d.exists():
|
if d.exists():
|
||||||
for p in d.glob("*.json"):
|
for p in d.glob("*.json"):
|
||||||
host_port = p.stem.replace("_", ":")
|
try:
|
||||||
if host_port not in seen:
|
data = json.loads(p.read_text())
|
||||||
seen.append(host_port)
|
except Exception:
|
||||||
|
continue
|
||||||
|
server = data.get("server")
|
||||||
|
if isinstance(server, str) and server and server not in seen:
|
||||||
|
seen.append(server)
|
||||||
for rid in list_admin_registries(userid):
|
for rid in list_admin_registries(userid):
|
||||||
try:
|
try:
|
||||||
reg = load_admin(userid, rid)
|
reg = load_admin(userid, rid)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ cargo fuzz build -s none handshake_respond
|
|||||||
cargo fuzz run -s none handshake_respond -- -max_total_time=60
|
cargo fuzz run -s none handshake_respond -- -max_total_time=60
|
||||||
```
|
```
|
||||||
|
|
||||||
The helper script sets `SANITIZER=none` by default for broad compatibility. It also prepends `~/.cargo/bin` to `PATH` so `cargo-fuzz` is found after `cargo install cargo-fuzz`.
|
The helper script sets `SANITIZER=none` by default for stable-toolchain usage. It also prepends `~/.cargo/bin` to `PATH` so `cargo-fuzz` is found after `cargo install cargo-fuzz`.
|
||||||
|
|
||||||
### Did anything crash?
|
### Did anything crash?
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user