S27: HPKE-Base content wrap + public-key directory; reject xor on send
Some checks are pending
ci / python (push) Waiting to run
ci / go (push) Waiting to run

Routing/error fields stay clear. Content is X25519-HKDF-SHA256-ChaCha20.
This commit is contained in:
George Lambert 2026-09-15 23:43:01 -04:00
parent 9cdc64b185
commit 94919185a9
8 changed files with 364 additions and 13 deletions

View file

@ -0,0 +1,93 @@
"""Public-key directory for NPE/HPKE endpoint encryption.
Only *public* material is listed. Private keys stay in private/ (0600).
Every service that needs E2E looks up handles here (HTTP or files).
"""
from __future__ import annotations # annotations
import json # records
import os # chmod
import time # updated_ts
from pathlib import Path # dir root
from typing import Any, Dict, List, Optional # types
from .hpke import generate_keypair # X25519
class PubKeyDir:
"""File-backed directory: public/<handle>.json + private/<handle>.sk."""
def __init__(self, root: Path):
self.root = Path(root) # /opt/pfc/etc/npe-dir
self.public = self.root / "public"
self.private = self.root / "private"
self.public.mkdir(parents=True, exist_ok=True)
self.private.mkdir(parents=True, exist_ok=True)
def create_endpoint(self, handle: str) -> Dict[str, Any]:
"""Generate X25519 keypair; write public record + private sk."""
handle = _safe(handle)
sk, pk = generate_keypair()
rec = {
"handle": handle,
"kem": "x25519",
"kdf": "hkdf-sha256",
"aead": "chacha20poly1305",
"suite": "HPKE-Base X25519-HKDF-SHA256-ChaCha20-Poly1305",
"enc_pk": pk.hex(),
"inbox": "npe.inbox." + pk[:16].hex(),
"updated_ts": time.time(),
}
(self.public / (handle + ".json")).write_text(json.dumps(rec, indent=2) + "\n")
skp = self.private / (handle + ".sk")
skp.write_bytes(sk)
os.chmod(skp, 0o600)
return rec
def get(self, handle: str) -> Optional[Dict[str, Any]]:
p = self.public / (_safe(handle) + ".json")
if not p.exists():
return None
return json.loads(p.read_text())
def list(self) -> List[Dict[str, Any]]:
rows = []
for p in sorted(self.public.glob("*.json")):
rows.append(json.loads(p.read_text()))
return rows
def put_public(self, rec: Dict[str, Any]) -> Dict[str, Any]:
"""Publish someone else's public key (no private)."""
handle = _safe(str(rec.get("handle") or ""))
if not handle or not rec.get("enc_pk"):
raise ValueError("handle and enc_pk required")
rec = dict(rec)
rec["handle"] = handle
rec["updated_ts"] = time.time()
(self.public / (handle + ".json")).write_text(json.dumps(rec, indent=2) + "\n")
return rec
def load_sk(self, handle: str) -> bytes:
p = self.private / (_safe(handle) + ".sk")
if not p.exists():
raise FileNotFoundError("no private key for " + handle)
return p.read_bytes()
def available(self) -> Dict[str, Any]:
"""What every E2E service can see: handles + public keys only."""
recs = self.list()
return {
"ok": True,
"directory": str(self.root),
"suite": "HPKE-Base X25519-HKDF-SHA256-ChaCha20-Poly1305",
"handles": [r["handle"] for r in recs],
"keys": recs,
}
def _safe(handle: str) -> str:
h = "".join(c for c in handle if c.isalnum() or c in "-_")
if not h:
raise ValueError("empty handle")
return h