An example answer to "can we build a real messaging system where the server
runs in FHE and the clients hold their private keys?" — rebased on XMPP
(RFC 6120/6122 data model) rather than an abstract mailbox. The client side
uses real XMPP concepts (JIDs, a roster, <message> stanzas with
<subject>/<body>); the server side is a single recorded ct×ct triage
circuit (fhetch_sim locally, hollow ring 2^16 for the Fog) that matches a
stanza's subject against the recipient's encrypted rule box and returns
only an encrypted match vector. The FHE circuit is byte-identical to the
verified fhe-messenger triage circuit; the kernel surface was extended by
one call (encrypt-to-maildrop: sender encrypts under the RECIPIENT's
published keys, per the XMPP "encrypt to the contact's key" model).
Honest boundary line: roster/presence/SASL stay plain, envelope metadata (from/to/when) is visible to the server, and the message body is out-of-scope for FHE (it rides in the client-side stanza log only). What an FHE Fog server can and cannot host today is in design/DESIGN.md.
Roles (demo: two users)
alice@fhe.example— recipient:alice_home/holds her keypair (sk.bin 0600, never leaves) and her rule box[invoice, maple, standup].bob@fhe.example— sender:bob_home/holds his roster (jid → alice's maildrop) and his<message>stanza log.alice_drop/— the server-side maildrop for alice. This directory is what a Fog worker receives whole; it contains only ciphertexts and public key material (cc/pk/mk, ct_rules, ct_one, ct_msgrep, ct_match).
Layout
src/xmpp.rs— pure Rust: bare-JID parsing/validation,<message>stanza format + subject extraction. No FFI.src/demo.rs— pure Rust: token hash (FNV-1a → 48 bits → [-0.45,+0.45]), SIMD packing, soft-eq^8 twin, MIN_SEP separation guards. No FFI.src/fhe.rs+src/kern/msg_kernel.cpp(+.h) — thin C ABI over OpenFHE (build.rs links OpenFHE + nbfhetch from the niobium-client checkout). The triage circuit: d = msgrep − rules, sc = (1 − d²)⁸ by repeated squaring, entirely ciphertext × ciphertext (ct_one carries the "1").src/main.rs— thefhexmppCLI: keygen / roster-add / rules / send / triage / check / watch.design/DESIGN.md— privacy model, circuit budget, measured Fog numbers, what adaptation to XMPP/DeltaChat would take vs what this demo covers.
Diagrams (design/*.dot — hand-written Graphviz sources, SVG the deliverable)
| File | Shows |
|---|---|
| topology.svg | trust-boundary map: the two clients (all secrets, roster, stanzas), alice's maildrop (public keys + ciphertexts only, sk-guarded), and the Fog worker; which flows cross the boundary as ciphertext vs which stay classical |
| pipeline.svg | one <message> end to end: keygen → rules → send (subject hashed + encrypted under the recipient's pk) → triage → Fog job → watch; the two trust-boundary crossings carry ciphertexts only |
| circuit.svg | the 4,255-instruction ct×ct triage circuit: slot geometry of this demo (invoice/maple/standup + PAD), the four multiplies, and the measured Fog-output scores against the 0.5 threshold / 0.288 ambiguity edge |
Build (✅ verified)
Prerequisite: a Release-built niobium-client
(cd ~/code/niobium-client && make release).
make build # cargo build --release
make unit # 7 Rust unit tests (hash grid, soft-eq extremes, PAD,
# gray-zone guard, JID parsing, stanza round-trip)
make twin # plaintext f64 twin: hash grid + soft-eq^8 + expectations
Local sim round-trip (✅ verified)
XMPP-shaped flow at ring 4096: alice keygen → bob keygen → bob rosters
alice's maildrop → alice parks her rule box → bob sends subject "maple"
(stanza to bob_home/stanza.log, ct_msgrep under alice's pk) → server
triage (record + fhetch_sim replay) → plain-FHE differential → alice
decrypts the match vector (subject read back from bob's stanza log).
make local
Expected tail:
sent: bob@fhe.example -> alice@fhe.example subject "maple" -> ct_msgrep.bin in alice_drop/
server departure inventory: cc/pk/mk + ct_rules + ct_one + ct_msgrep (no sk, no plaintext, no stanzas)
MATCH
[PASS] plain-OpenFHE circuit matches the f64 twin
=== watch: decrypted MATCH vector (stanza subject was: "maple") ===
[PASS] lit slots [1] == expected [1]
Guards (all verified):
make nomatch # subject "quartz": no slot lights
make canary # plant sk.bin in the maildrop: server REFUSES with exit 1
Fog round-trip (2^16, hollow record — ✅ verified)
make fog-record # N=2^16 keygen (both users) + rules + send + hollow record
make fog-submit # ships the trace; needs fogrs + ~/.fog/credentials
Verified run (the XMPP-shaped flow, job 1145db61, batch, dal1worker1):
132 MB project upload (138.5 MB streamed, 4.6 s), ~11 s worker wall for the
4,255-instruction triage circuit, 4.0 MB encrypted match vector back.
Decrypted on bob's client against the stanza from bob's own log — slot 1
(maple) lit at score 1.0, watch PASSed. (fog-submit asserts
--expect-hot 1.)
What this does / does NOT do
DOES: XMPP data model (JIDs, roster, stanzas with subject watchword), encrypted ct×ct subject triage, sender-→-recipient encryption against the recipient's published keys, secret-key discipline (client-only, guarded on both sides), local sim + 2^16 hollow Fog-shaped gates.
Does NOT (yet — escalation steps in DESIGN.md): roster fan-out routing on encrypted ids, presence/push, a stateful mailbox, encrypted bodies, MUC, XMPP SASL/TLS transport (would stay classical anyway), or any DeltaChat adaptation.
License
BSD-3-Clause (see LICENSE).