feat(identity): per-scope network identity plane (slices 1-5)

Fingerprint map + pure resolver (account umbrella / key-level scope
rule), live Warp provider on warp-* structures, broker lifecycle
(up/down/cycle/exec/routes/status/bind), wireguard+socks boilerplate
stubs, agent-manager bind integration. CLI carries emails and
fingerprints only; key bytes never appear. 41 committed tests.
This commit is contained in:
operator
2026-10-08 04:03:45 +00:00
parent 88ddc54a78
commit 41499e069d
7 changed files with 1328 additions and 0 deletions
+342
View File
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""identity-broker.py — Scope lifecycle over proxy providers.
Owns identity-state.json (gitignored runtime state: scope -> label
assignments, no secrets) and drives providers through it:
up <fp> resolve fingerprint, provision its scope
down <fp|unit> teardown scope network, drop assignment
cycle <fp> rotate to a fresh identity (bumps cycles)
exec <fp> -- <cmd> run a command inside the scope network
routes <fp> read-only route/tunnel status
status scopes with emails/labels (no key material)
bind --from <runs.json> --session <s> --fp <f>
attribute live runs (agent-manager --once
--json) to a scope after verifying the
session exists in the scan
v1 boundary: the broker scopes NETWORK identity only. It never sees,
stores, or prints key bytes — fingerprints and emails are the only
identifiers here. Short-lived brokered credential issuance is a
deferred stage; harnesses receive keys through existing means.
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import sys
import time
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
BIN_DIR = REPO_ROOT / "bin"
STATE_FILE = REPO_ROOT / "identity-state.json"
RunFn = Callable[..., Tuple[int, str]]
def _load(name: str, modname: str):
path = BIN_DIR / name
spec = importlib.util.spec_from_file_location(modname, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[modname] = mod
spec.loader.exec_module(mod)
return mod
resolve_mod = _load("identity-resolve.py", "identity_resolve")
provider_mod = _load("identity-provider.py", "identity_provider")
PROVIDERS = {"warp": provider_mod.WarpProvider(),
"wireguard": provider_mod.GenericWireGuardProvider(),
"socks": provider_mod.SocksProxyProvider()}
class BrokerError(RuntimeError):
"""A broker operation failed (message is safe to show)."""
def load_state(path: str | Path = STATE_FILE) -> Dict[str, Any]:
"""Load broker state. Missing/corrupt -> empty (never raises)."""
try:
with open(path, "r") as f:
data = json.load(f)
if isinstance(data, dict):
data.setdefault("scopes", {})
data.setdefault("bindings", [])
return data
except Exception:
pass
return {"scopes": {}, "bindings": []}
def save_state(state: Dict[str, Any],
path: str | Path = STATE_FILE) -> None:
with open(path, "w") as f:
json.dump(state, f, indent=2, sort_keys=True)
def _provider(name: str = "warp"):
try:
prov = PROVIDERS[name]
except KeyError:
raise BrokerError("unknown provider %r (have: %s)"
% (name, ", ".join(sorted(PROVIDERS))))
if not prov.ready:
raise BrokerError("provider %r is boilerplate (not implemented); "
"warp is the live provider" % (name,))
return prov
def _scope_for_fp(fp: str, map_path=None) -> Dict[str, Any]:
scope = resolve_mod.resolve_scope(
resolve_mod.load_map(map_path or resolve_mod.MAP_FILE), fp)
if scope is None:
raise BrokerError("unknown fingerprint (not in identity-map.json)")
scope["slug"] = resolve_mod.scope_slug(scope)
return scope
def _unit_key(fp_or_unit: str, state: Dict[str, Any],
map_path=None) -> str:
"""Resolve CLI input (fp or scope unit) to a state scopes key."""
scopes = state.get("scopes", {})
if fp_or_unit in scopes:
return fp_or_unit
try:
scope = _scope_for_fp(fp_or_unit, map_path)
except BrokerError:
raise BrokerError("no scope for %r (unknown fingerprint, no "
"such unit)" % (fp_or_unit,))
if scope["unit"] not in scopes:
raise BrokerError("scope %s is not up" % (scope["unit"],))
return scope["unit"]
def op_up(fp: str, run: Optional[RunFn] = None, map_path=None,
state_path: str | Path = STATE_FILE,
provider_name: str = "warp") -> Dict[str, Any]:
"""Provision a scope's network. Idempotent (re-up returns existing)."""
scope = _scope_for_fp(fp, map_path)
state = load_state(state_path)
if scope["unit"] in state["scopes"]:
return {"ok": "exists", **state["scopes"][scope["unit"]]}
label = scope["slug"]
try:
res = _provider(provider_name).provision(label, run=run)
except provider_mod.ProviderError as e:
raise BrokerError(str(e))
rec = {"scope": scope["scope"], "email": scope["email"],
"origins": scope["origins"], "label": label,
"provider": provider_name, "netns": res.get("netns", ""),
"created": int(time.time()), "cycles": 0}
state["scopes"][scope["unit"]] = rec
save_state(state, state_path)
return {"ok": "true", **rec}
def op_down(fp_or_unit: str, run: Optional[RunFn] = None, map_path=None,
state_path: str | Path = STATE_FILE) -> Dict[str, Any]:
"""Teardown a scope's network and drop its assignment + bindings."""
state = load_state(state_path)
unit = _unit_key(fp_or_unit, state, map_path)
rec = state["scopes"][unit]
try:
_provider(rec.get("provider", "warp")).teardown(rec["label"],
run=run)
except provider_mod.ProviderError as e:
raise BrokerError(str(e))
del state["scopes"][unit]
state["bindings"] = [b for b in state.get("bindings", [])
if b.get("unit") != unit]
save_state(state, state_path)
return {"ok": "true", "unit": unit, "label": rec["label"]}
def op_cycle(fp: str, run: Optional[RunFn] = None, map_path=None,
state_path: str | Path = STATE_FILE) -> Dict[str, Any]:
"""Rotate a scope to a fresh identity (bumps the cycle count)."""
scope = _scope_for_fp(fp, map_path)
state = load_state(state_path)
if scope["unit"] not in state["scopes"]:
raise BrokerError("scope %s is not up (up it first)"
% (scope["unit"],))
rec = state["scopes"][scope["unit"]]
try:
_provider(rec.get("provider", "warp")).cycle(rec["label"],
run=run)
except provider_mod.ProviderError as e:
raise BrokerError(str(e))
rec["cycles"] = int(rec.get("cycles", 0)) + 1
save_state(state, state_path)
return {"ok": "true", "unit": scope["unit"], "label": rec["label"],
"cycles": rec["cycles"]}
def op_exec(fp: str, cmd: List[str], run: Optional[RunFn] = None,
map_path=None,
state_path: str | Path = STATE_FILE) -> Tuple[int, str]:
"""Run cmd inside the scope's network. Returns (rc, output)."""
scope = _scope_for_fp(fp, map_path)
state = load_state(state_path)
if scope["unit"] not in state["scopes"]:
raise BrokerError("scope %s is not up (up it first)"
% (scope["unit"],))
rec = state["scopes"][scope["unit"]]
try:
return _provider(rec.get("provider", "warp")).exec(
rec["label"], cmd, run=run)
except provider_mod.ProviderError as e:
raise BrokerError(str(e))
def op_routes(fp: str, run: Optional[RunFn] = None, map_path=None,
state_path: str | Path = STATE_FILE) -> Dict[str, Any]:
"""Read-only route/tunnel status for a scope."""
scope = _scope_for_fp(fp, map_path)
state = load_state(state_path)
if scope["unit"] not in state["scopes"]:
raise BrokerError("scope %s is not up (up it first)"
% (scope["unit"],))
rec = state["scopes"][scope["unit"]]
try:
info = _provider(rec.get("provider", "warp")).routes(
rec["label"], run=run)
except provider_mod.ProviderError as e:
raise BrokerError(str(e))
return {"unit": scope["unit"], "email": scope["email"], **info}
def op_status(run: Optional[RunFn] = None,
state_path: str | Path = STATE_FILE) -> Dict[str, Any]:
"""Scopes with live provider status. Emails/labels only."""
state = load_state(state_path)
scopes = []
for unit, rec in sorted(state.get("scopes", {}).items()):
try:
live = _provider(rec.get("provider", "warp")).status(
rec["label"], run=run)
except provider_mod.ProviderError as e:
live = {"conf": "?", "netns": "?", "egress": "n/a",
"error": str(e)}
scopes.append({"unit": unit, "email": rec.get("email", ""),
"scope": rec.get("scope", ""),
"label": rec.get("label", ""),
"provider": rec.get("provider", ""),
"cycles": rec.get("cycles", 0), **live})
return {"scopes": scopes, "bindings": state.get("bindings", [])}
def op_bind(runs_path: str, session: str, fp: str, map_path=None,
state_path: str | Path = STATE_FILE) -> Dict[str, Any]:
"""Attribute live runs to a scope, verifying against a scan.
runs_path is agent-manager.py --once --json output. Every run
whose session group contains `session` is bound to the fp's scope
(fp must resolve; the scope need not be up — binding is
attribution, not network). Sessions absent from the scan are
refused (never bind what we cannot observe).
"""
scope = _scope_for_fp(fp, map_path)
try:
with open(runs_path, "r") as f:
scan = json.load(f)
runs = scan.get("runs", [])
if not isinstance(runs, list):
raise ValueError("no runs list")
except Exception as e:
raise BrokerError("cannot read runs scan %s: %s" % (runs_path, e))
matched = []
for r in runs:
if not isinstance(r, dict):
continue
group = str(r.get("session", "")).split(",")
if session in group:
matched.append(r)
if not matched:
raise BrokerError("session %r not observed in %s (refusing to "
"bind unseen runs)" % (session, runs_path))
state = load_state(state_path)
now = int(time.time())
new = []
for r in matched:
rec = {"unit": scope["unit"], "email": scope["email"],
"device": r.get("device", ""), "type": r.get("type", ""),
"session": session, "pane": r.get("pane", ""),
"pid": r.get("pid", 0), "bound_at": now}
new.append(rec)
# Replace prior bindings for these exact runs (re-bind refreshes).
keys = {(b["device"], b.get("pane"), b.get("pid")) for b in new}
state["bindings"] = [b for b in state.get("bindings", [])
if (b.get("device"), b.get("pane"),
b.get("pid")) not in keys] + new
save_state(state, state_path)
return {"ok": "true", "unit": scope["unit"], "bound": len(new),
"runs": new}
def main(argv: Optional[List[str]] = None) -> int:
ap = argparse.ArgumentParser(prog="identity-broker.py")
ap.add_argument("--map", default=str(resolve_mod.MAP_FILE),
help="identity map (default: identity-map.json)")
ap.add_argument("--state", default=str(STATE_FILE),
help="broker state file (default: identity-state.json)")
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("up", help="provision a scope network")
p.add_argument("fp")
p.add_argument("--provider", default="warp")
p = sub.add_parser("down", help="teardown a scope network")
p.add_argument("fp_or_unit")
p = sub.add_parser("cycle", help="rotate a scope identity")
p.add_argument("fp")
p = sub.add_parser("exec", help="run a command in a scope network")
p.add_argument("fp")
p.add_argument("exec_cmd", nargs=argparse.REMAINDER,
help="command (after --)")
p = sub.add_parser("routes", help="route/tunnel status for a scope")
p.add_argument("fp")
sub.add_parser("status", help="scopes + bindings (emails only)")
p = sub.add_parser("bind", help="attribute live runs to a scope")
p.add_argument("--from", dest="runs", required=True)
p.add_argument("--session", required=True)
p.add_argument("--fp", required=True)
args = ap.parse_args(argv)
mp, sp = args.map, args.state
try:
if args.cmd == "up":
print(json.dumps(op_up(args.fp, map_path=mp,
state_path=sp,
provider_name=args.provider),
indent=2))
elif args.cmd == "down":
print(json.dumps(op_down(args.fp_or_unit, map_path=mp,
state_path=sp), indent=2))
elif args.cmd == "cycle":
print(json.dumps(op_cycle(args.fp, map_path=mp,
state_path=sp), indent=2))
elif args.cmd == "exec":
cmd = [c for c in (args.exec_cmd or []) if c != "--"]
rc, out = op_exec(args.fp, cmd, map_path=mp,
state_path=sp)
sys.stdout.write(out + ("\n" if out else ""))
return rc
elif args.cmd == "routes":
print(json.dumps(op_routes(args.fp, map_path=mp,
state_path=sp), indent=2))
elif args.cmd == "status":
print(json.dumps(op_status(state_path=sp), indent=2))
elif args.cmd == "bind":
print(json.dumps(op_bind(args.runs, args.session, args.fp,
map_path=mp, state_path=sp),
indent=2))
return 0
except BrokerError as e:
print("error: %s" % e)
return 1
if __name__ == "__main__":
sys.exit(main())
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""identity-provider.py — Proxy provider implementations.
A provider owns one network-identity substrate behind a fixed
interface: provision / teardown / cycle / exec / routes / status.
All subprocesses go through an injectable run function (same seam as
box-fleet-tui gather_*), so command shapes are unit-testable and no
test touches netns, sudo, or /etc/netvm.
Security boundaries (from the repo's own scripts):
- Warp identities generate via netvm-new-identity.sh, which the user
explicitly authorized operators to run (see netvm-provision-node.sh
header). Generation installs a root-0600 conf and prints nothing.
- This code NEVER reads /etc/netvm and never prints key material.
Confs are consumed only by root tools (wg setconf inside netns).
- CLI-facing output carries emails, labels, and fingerprints only.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Callable, Dict, List, Optional, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
BIN_DIR = REPO_ROOT / "bin"
RunFn = Callable[..., Tuple[int, str]]
LABEL_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,22}$")
def _run(cmd: List[str], timeout: int = 120) -> Tuple[int, str]:
"""Run cmd, capture output. Returns (returncode, combined_output)."""
try:
r = subprocess.run(cmd, capture_output=True, text=True,
timeout=timeout)
return r.returncode, ((r.stdout or "") + (r.stderr or "")).strip()
except subprocess.TimeoutExpired:
return 124, "timed out after %ds: %s" % (timeout, " ".join(cmd))
except OSError as e:
return 127, str(e)
class ProviderError(RuntimeError):
"""A provider operation failed (message is safe to show)."""
def check_label(label: str) -> str:
"""Validate a netvm label. Returns it or raises ProviderError."""
if not LABEL_RE.match(label or ""):
raise ProviderError(
"invalid label %r: lowercase letters, digits, hyphens "
"(max 23 chars)" % (label,))
return label
class Provider:
"""Interface every proxy provider implements. Boilerplate subclasses
override these with real substrate calls; see WarpProvider."""
name = "base"
ready = False
def provision(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
"""Create the network identity + bring it up. Idempotent."""
raise NotImplementedError
def teardown(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
"""Bring the identity's network down (keeps the identity)."""
raise NotImplementedError
def cycle(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
"""Rotate to a fresh identity (teardown + new identity + up)."""
raise NotImplementedError
def exec(self, label: str, cmd: List[str],
run: Optional[RunFn] = None) -> Tuple[int, str]:
"""Run cmd inside the identity's network. Returns (rc, output)."""
raise NotImplementedError
def routes(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
"""Read-only route/tunnel status for the identity."""
raise NotImplementedError
def status(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
"""Read-only liveness: conf present, netns up, egress IP."""
raise NotImplementedError
class WarpProvider(Provider):
"""Cloudflare Warp provider on the established warp-* structures.
Identity: /etc/netvm/<label>.conf via netvm-new-identity.sh
(operator-authorized). Network: warp-<label> netns via
netvm-node-up.sh / netvm-node-down.sh. Exec: netvm-exec.sh.
Scopes are NOT nodes: no chrome-box profile, no NODES.md entry.
"""
name = "warp"
ready = True
def _conf_exists(self, label: str, run: RunFn) -> bool:
rc, _ = run(["test", "-f", "/etc/netvm/%s.conf" % label],
timeout=10)
return rc == 0
def provision(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
run = run or _run
check_label(label)
steps = []
if not self._conf_exists(label, run):
rc, out = run(["sudo", "-n", str(BIN_DIR / "netvm-new-identity.sh"),
label], timeout=300)
if rc != 0:
raise ProviderError("warp identity failed for %s: %s"
% (label, out[-200:]))
steps.append("identity=new")
else:
steps.append("identity=exists")
rc, out = run(["sudo", "-n", str(BIN_DIR / "netvm-node-up.sh"),
label], timeout=300)
if rc != 0:
raise ProviderError("netns up failed for %s: %s"
% (label, out[-200:]))
steps.append("netns=up")
return {"ok": "true", "label": label, "netns": "warp-" + label,
"steps": ",".join(steps)}
def teardown(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
run = run or _run
check_label(label)
rc, out = run(["sudo", "-n", str(BIN_DIR / "netvm-node-down.sh"),
label], timeout=120)
if rc != 0:
raise ProviderError("netns down failed for %s: %s"
% (label, out[-200:]))
return {"ok": "true", "label": label, "netns": "warp-" + label}
def cycle(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
"""Fresh warp identity: down + remove conf + provision.
Conf removal needs root on /etc/netvm; when denied, the old
identity is left intact (netns down) and the operator gets the
exact human step instead of a half-rotated state.
"""
run = run or _run
check_label(label)
self.teardown(label, run=run)
rc, out = run(["sudo", "-n", "rm", "-f",
"/etc/netvm/%s.conf" % label], timeout=30)
if rc != 0:
raise ProviderError(
"rotation paused for %s: cannot remove old identity "
"(%s). Human: sudo rm /etc/netvm/%s.conf, then cycle "
"again." % (label, out[-120:], label))
return self.provision(label, run=run)
def exec(self, label: str, cmd: List[str],
run: Optional[RunFn] = None) -> Tuple[int, str]:
run = run or _run
check_label(label)
if not cmd:
raise ProviderError("exec needs a command")
return run([str(BIN_DIR / "netvm-exec.sh"), label, "--"] + cmd,
timeout=120)
def routes(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
run = run or _run
check_label(label)
netns = "warp-" + label
_, route_out = run(["sudo", "-n", "ip", "netns", "exec", netns,
"ip", "route"], timeout=30)
_, wg_out = run(["sudo", "-n", "ip", "netns", "exec", netns,
"wg", "show"], timeout=30)
return {"label": label, "netns": netns, "routes": route_out,
"wireguard": wg_out}
def status(self, label: str,
run: Optional[RunFn] = None) -> Dict[str, str]:
run = run or _run
check_label(label)
conf = self._conf_exists(label, run)
rc, out = run(["ip", "netns", "list"], timeout=10)
up = rc == 0 and ("warp-" + label) in out
egress = ""
if conf and up:
rc, eg = self.exec(label, ["curl", "-s", "--max-time", "8",
"https://api.ipify.org"], run=run)
egress = eg.strip().splitlines()[-1] if rc == 0 and eg.strip() \
else ""
return {"label": label, "conf": "yes" if conf else "no",
"netns": "up" if up else "down", "egress": egress or "n/a"}
class GenericWireGuardProvider(Provider):
"""BOILERPLATE: bring-your-own WireGuard confinement.
Intended structure: the operator supplies a wg conf out of band
(same root-0600 handling as Warp confs — never read here);
provision creates warp-<label> netns + veth/NAT exactly like
WarpProvider but consumes the supplied conf instead of a
Cloudflare-registered identity. Cycle swaps to the next supplied
conf. Implement when the first non-Cloudflare tunnel is needed.
"""
name = "wireguard"
class SocksProxyProvider(Provider):
"""BOILERPLATE: per-scope SOCKS5 forward, no netns.
Intended structure: provision opens a dedicated local forward
(ssh -D style) per scope label and records its port; exec runs
commands with ALL_PROXY scoped to that port instead of entering a
netns; cycle re-establishes the forward via a fresh egress.
Implement when a scope needs proxy semantics without tunnels.
"""
name = "socks"
if __name__ == "__main__":
print("identity-provider.py is a library (see identity-broker.py)")
sys.exit(2)
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""identity-resolve.py — Pure identity resolution for the identity plane.
Reads identity-map.json (fingerprints only, never key material) and
resolves an API-key fingerprint to its network-identity scope:
api_key -> account_origin(s); one origin rolls scope UP to the
umbrella account, two or more keep scope DOWN at the key itself.
This module is pure + total (missing/corrupt map -> empty, unknown
fingerprint -> None). CLI output carries emails and fingerprints only;
key bytes never appear here — there is no code path that reads them
except `fp`, which hashes stdin and prints only the digest.
Usage:
identity-resolve.py fp < keyfile # print sha256: fingerprint
identity-resolve.py lookup <fingerprint> # print scope JSON
identity-resolve.py check # validate map schema
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
REPO_ROOT = Path(__file__).resolve().parent.parent
MAP_FILE = REPO_ROOT / "identity-map.json"
LABEL_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,22}$")
def fingerprint_hex(material: bytes) -> str:
"""sha256: fingerprint of raw key bytes."""
return "sha256:" + hashlib.sha256(material).hexdigest()
def load_map(path: str | Path = MAP_FILE) -> Dict[str, Any]:
"""Load the identity map. Missing/corrupt -> {"accounts": {}}."""
try:
with open(path, "r") as f:
data = json.load(f)
if isinstance(data, dict) and isinstance(
data.get("accounts"), dict):
return data
except Exception:
pass
return {"accounts": {}}
def find_key(map_data: Dict[str, Any],
fp: str) -> Optional[Dict[str, Any]]:
"""Locate a key record by fingerprint.
Returns {"email", "key"} or None. Top-level '_' entries ignored.
"""
if not fp:
return None
accounts = map_data.get("accounts")
if not isinstance(accounts, dict):
return None
for email, rec in accounts.items():
if not isinstance(rec, dict):
continue
keys = rec.get("keys")
if not isinstance(keys, list):
continue
for k in keys:
if isinstance(k, dict) and k.get("fp") == fp:
return {"email": email, "key": k}
return None
def resolve_scope(map_data: Dict[str, Any],
fp: str) -> Optional[Dict[str, Any]]:
"""Resolve a fingerprint to its scope unit.
Single origin -> {"scope": "account", "unit": email, ...}.
Multiple origins -> {"scope": "key", "unit": fp, ...}.
Unknown fingerprint -> None. Result carries emails + fingerprints
only (no key material exists anywhere in this module).
"""
found = find_key(map_data, fp)
if found is None:
return None
key = found["key"]
origins = key.get("origins")
if not isinstance(origins, list) or not origins:
return None
origins = [str(o) for o in origins]
if len(origins) == 1:
return {"scope": "account", "unit": origins[0],
"email": found["email"], "origins": origins,
"label": key.get("label", "")}
return {"scope": "key", "unit": fp, "email": found["email"],
"origins": origins, "label": key.get("label", "")}
def scope_slug(scope: Dict[str, Any]) -> str:
"""Deterministic netvm label for a scope (fits label validation).
Account scopes: id-<email-fragment>-<hash7>. Key scopes:
id-k-<fp-hex-prefix>. Always matches ^[a-z0-9][a-z0-9-]{0,22}$.
"""
unit = str(scope.get("unit", ""))
if scope.get("scope") == "key":
hexpart = re.sub(r"[^0-9a-f]", "", unit.lower())[:12] or "0"
return "id-k-%s" % hexpart
frag = re.sub(r"[^a-z0-9]+", "-", unit.lower()).strip("-")[:12]
frag = frag.strip("-") or "x"
tag = hashlib.sha256(unit.encode()).hexdigest()[:7]
return "id-%s-%s" % (frag, tag)
def check_map(map_data: Dict[str, Any]) -> List[str]:
"""Validate map schema. Returns a list of problem strings (empty OK)."""
problems: List[str] = []
accounts = map_data.get("accounts")
if not isinstance(accounts, dict):
return ["top-level 'accounts' must be an object"]
seen_fps: Dict[str, str] = {}
for email, rec in accounts.items():
if not isinstance(email, str) or "@" not in email:
problems.append("account key %r is not an email" % (email,))
if not isinstance(rec, dict) or not isinstance(
rec.get("keys"), list):
problems.append("account %r: 'keys' must be a list" % (email,))
continue
for i, k in enumerate(rec["keys"]):
where = "%s.keys[%d]" % (email, i)
if not isinstance(k, dict):
problems.append("%s: not an object" % where)
continue
fp = k.get("fp", "")
if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(fp)):
problems.append("%s: bad fingerprint %r" % (where, fp))
elif fp in seen_fps:
problems.append("%s: fingerprint already listed under %s"
% (where, seen_fps[fp]))
else:
seen_fps[fp] = email
origins = k.get("origins")
if not isinstance(origins, list) or not origins or not all(
isinstance(o, str) and o for o in origins):
problems.append("%s: 'origins' must be a non-empty "
"string list" % where)
return problems
def main(argv: Optional[List[str]] = None) -> int:
ap = argparse.ArgumentParser(prog="identity-resolve.py")
ap.add_argument("--map", default=str(MAP_FILE),
help="identity map (default: identity-map.json)")
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("fp", help="print sha256: fingerprint of stdin bytes")
p = sub.add_parser("lookup", help="resolve a fingerprint to scope JSON")
p.add_argument("fp")
sub.add_parser("check", help="validate the map schema")
args = ap.parse_args(argv)
if args.cmd == "fp":
sys.stdout.write(fingerprint_hex(sys.stdin.buffer.read()) + "\n")
return 0
if args.cmd == "lookup":
scope = resolve_scope(load_map(args.map), args.fp)
if scope is None:
print("unknown fingerprint (not in map)")
return 1
scope["slug"] = scope_slug(scope)
print(json.dumps(scope, indent=2))
return 0
problems = check_map(load_map(args.map))
if problems:
print("%s INVALID:" % args.map)
for prob in problems:
print(" - %s" % prob)
return 1
print("%s OK" % args.map)
return 0
if __name__ == "__main__":
sys.exit(main())