S27: HPKE-Base content wrap + public-key directory; reject xor on send
Routing/error fields stay clear. Content is X25519-HKDF-SHA256-ChaCha20.
This commit is contained in:
parent
9cdc64b185
commit
94919185a9
8 changed files with 364 additions and 13 deletions
63
python/secure_messaging/content.py
Normal file
63
python/secure_messaging/content.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""NATS content wrapping: HPKE body, routing/error in the clear.
|
||||
|
||||
Clear (broker may see): subject, ``to``, ``from_lookup_id``, ``alg``,
|
||||
error_code, dest_class, lookup_id.
|
||||
Never clear: inner content JSON / PHI / object bytes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations # annotations
|
||||
|
||||
import json # inner body
|
||||
import os # urandom
|
||||
from typing import Any, Dict, Mapping, Optional # types
|
||||
|
||||
from .envelope import lookup_id # opaque sender handle
|
||||
from .hpke import open_ct, seal # HPKE-Base
|
||||
from .pubkey_dir import PubKeyDir # directory
|
||||
|
||||
|
||||
def wrap_content(
|
||||
*,
|
||||
to_handle: str,
|
||||
sender: str,
|
||||
body: Mapping[str, Any],
|
||||
directory: PubKeyDir,
|
||||
) -> Dict[str, Any]:
|
||||
"""Encrypt body to the recipient's directory public key."""
|
||||
rec = directory.get(to_handle)
|
||||
if not rec: # unknown endpoint
|
||||
raise KeyError("directory has no public key for " + to_handle)
|
||||
pk = bytes.fromhex(rec["enc_pk"])
|
||||
inner = json.dumps(body, sort_keys=True, separators=(",", ":")).encode()
|
||||
aad = ("to=" + to_handle).encode() # bind ciphertext to routing dest
|
||||
enc, ct = seal(pk, inner, aad=aad)
|
||||
nonce = os.urandom(16)
|
||||
return {
|
||||
"v": 1,
|
||||
"alg": "npe",
|
||||
"mode": "passthrough",
|
||||
"to": rec.get("inbox") or to_handle, # routing in the clear
|
||||
"to_handle": to_handle,
|
||||
"from_lookup_id": lookup_id(sender, nonce),
|
||||
"nonce": nonce.hex(),
|
||||
"enc": enc.hex(), # HPKE encapsulated key (not content)
|
||||
"ct": ct.hex(), # content ciphertext
|
||||
}
|
||||
|
||||
|
||||
def unwrap_content(env: Mapping[str, Any], *, handle: str, directory: PubKeyDir) -> Dict[str, Any]:
|
||||
"""Open content as the named endpoint. Broker cannot do this."""
|
||||
sk = directory.load_sk(handle)
|
||||
to_handle = str(env.get("to_handle") or handle)
|
||||
aad = ("to=" + to_handle).encode()
|
||||
pt = open_ct(sk, bytes.fromhex(env["enc"]), bytes.fromhex(env["ct"]), aad=aad)
|
||||
return json.loads(pt.decode())
|
||||
|
||||
|
||||
def routing_view(env: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
"""What an untrusted router is allowed to look at."""
|
||||
return {
|
||||
k: env[k]
|
||||
for k in ("v", "alg", "mode", "to", "to_handle", "from_lookup_id", "nonce")
|
||||
if k in env
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue