Probe reports usable without enabling PFC_REQUIRE_NPE. Fail-closed if sender .seed or recipient .npeid is missing.
77 lines
3.3 KiB
Python
77 lines
3.3 KiB
Python
"""NPE sidecar adapter (fail-closed).
|
|
|
|
The live binary is ``npe <keygen|id|send|listen|reply|decode|...>``.
|
|
There is **no** ``npe seal --to`` verb. Production HPKE is ``npe send``
|
|
with a sender ``.seed`` and recipient ``.npeid``.
|
|
|
|
``crypto.mode=npe`` must not fall back to lab-xor.
|
|
``PFC_REQUIRE_NPE=1`` on pfc-py-admin is a bus-wide fail-close; do not
|
|
set it on ns1 until every NATS client uses ``npe send``.
|
|
"""
|
|
|
|
from __future__ import annotations # annotations as strings
|
|
|
|
import json # encode sidecar request
|
|
import os # NPE_BIN / NPE_SENDER_SEED
|
|
import shutil # look up npe on PATH
|
|
import subprocess # run sidecar
|
|
from pathlib import Path # /opt/pfc/bin/npe
|
|
from typing import Any, Dict, Mapping # types
|
|
|
|
|
|
class NpeRequired(RuntimeError):
|
|
"""Raised when NPE is required but the sidecar is not usable."""
|
|
|
|
|
|
def npe_bin() -> str:
|
|
"""Return path to npe or raise. Checks NPE_BIN, PATH, then /opt/pfc/bin/npe."""
|
|
cands = [os.environ.get("NPE_BIN") or "", shutil.which("npe") or "", "/opt/pfc/bin/npe"]
|
|
for path in cands: # first executable wins
|
|
if path and Path(path).is_file() and os.access(path, os.X_OK):
|
|
return path # found
|
|
raise NpeRequired("npe binary not on PATH or /opt/pfc/bin/npe; attach sidecar") # fail closed
|
|
|
|
|
|
def probe() -> Dict[str, Any]:
|
|
"""Does the real CLI exist and speak send/keygen? Never enables fail-close."""
|
|
try: # binary lookup
|
|
path = npe_bin() # may raise
|
|
except NpeRequired as exc: # missing
|
|
return {"ok": False, "usable": False, "error": str(exc)}
|
|
proc = subprocess.run([path], capture_output=True, timeout=5, check=False) # usage on stderr
|
|
text = (proc.stderr or proc.stdout).decode(errors="replace") # usage line
|
|
usable = "send" in text and "keygen" in text # real npe CLI
|
|
return {
|
|
"ok": usable,
|
|
"usable": usable,
|
|
"bin": path,
|
|
"cli": "npe <keygen|id|send|listen|reply|decode>",
|
|
"usage": text.strip()[:240],
|
|
"require_npe_env": os.environ.get("PFC_REQUIRE_NPE", ""),
|
|
}
|
|
|
|
|
|
def seal_npe(to: str, body: Mapping[str, Any]) -> str:
|
|
"""HPKE-seal via ``npe send``. Returns a handle, never lab-xor.
|
|
|
|
Requires ``NPE_SENDER_SEED`` (sender .seed) and ``to`` as a path to a
|
|
recipient ``.npeid``. This publishes on NATS; it is not a hex-only
|
|
sidecar. Missing keys → NpeRequired (fail closed).
|
|
"""
|
|
sender = os.environ.get("NPE_SENDER_SEED", "") # host-only seed
|
|
if not sender or not Path(sender).is_file(): # no identity
|
|
raise NpeRequired("NPE_SENDER_SEED missing; npe send needs sender .seed + recipient .npeid")
|
|
to_path = to # dest is .npeid path for the real CLI
|
|
if not Path(to_path).is_file(): # mailbox id is not a file
|
|
raise NpeRequired("npe send --to expects a .npeid file, not a mailbox string")
|
|
raw = json.dumps(body, sort_keys=True, separators=(",", ":")).encode() # canonical body
|
|
proc = subprocess.run( # real CLI
|
|
[npe_bin(), "send", "--id", sender, "--to", to_path, "--data", "-"],
|
|
input=raw,
|
|
capture_output=True,
|
|
timeout=15,
|
|
check=False,
|
|
)
|
|
if proc.returncode != 0: # sidecar failed
|
|
raise NpeRequired("npe send failed: " + proc.stderr.decode(errors="replace")[:200])
|
|
return proc.stdout.strip().decode() or "npe-send-ok" # token from sidecar
|