graphrag

Private LightRAG retrieval — three Compass-encrypted HNSW indexes and two XorMM volume-hiding multimaps, all driven from inside an attested SEV-SNP CVM. The entire LightRAG kg_query surface runs as Ring-ORAM accesses over a fully untrusted blob store.

status  shipped · private graph retrieval · benched
trust anchor  SEV-SNP CVM hosts the Compass client
builds on  CAPRISE + two-party HKDF + SnpTrustedExecutor
rev  2026·05·17
private LightRAG · Ring-ORAM + XorMM
measured query latency
10–58ms
in-memory backend · Local 10 ms (N=100) → 34 ms (N=2000) · Hybrid 14 → 58 ms · see §06
layer-0 ORAM reads/q
117–397
Local 117 → 212 · Hybrid 173 → 397 over N=100 → 2000 · directional filter prunes ~50 %
access-pattern leakage
comp.indist.
Ring-ORAM semi-honest baseline shipped · cross-session linkability 100/100 trials diverge · Merkle malicious-mode deferred
crates shipped · tests
6 + 1
ring-oram · compass-index · xormm-emm · compass-rest-backend · light-kg-store · lightrag-private (+graphrag-bench) · 80+ tests green
§01

Role in the project

private-rag has three previously-shipped vertical slices — CAPRISE embedder, CAPRISE storage, GELO reranker — that together cover flat-vector RAG: documents go in as encrypted embeddings, queries come back as encrypted top-k. That vertical assumes the corpus is a flat list of chunks. This page is the graph-augmented slice. LightRAG (Guo et al. EMNLP 2025) replaces the flat list with a knowledge graph of entities + relations + chunk pointers; retrieval becomes three parallel ANN searches plus a 1-hop graph expansion plus a chunk fan-out. Done naïvely, each of those steps is a distinct side channel the storage operator can read.

The job of this slice is to compose LightRAG retrieval semantics over the Compass encrypted-ANN protocol (OSDI 2025) so the full kg_query surface — including the graph expansion and chunk fan-out — runs as oblivious primitives over an untrusted blob store. Three Compass-encrypted HNSW indexes hide which entity / relation / chunk got looked up; two XorMM volume-hiding multi-maps hide adjacency-list lengths and source-chunk fan-out widths; AES-GCM hides chunk text. Every primitive is driven from inside a SEV-SNP CVM, attestation-anchored to the same scheme_identity that already pins the embedder and storage slices.

What this unlocks in the product: a tenant can ship a structured KG (entities, relations, chunks) to the CVM over RATLS, and subsequent queries return graph-grounded context — the kind LightRAG's headline benchmarks show beats flat retrieval on multi-hop and discovery tasks — without exposing the KG topology, the lookup pattern, or the result identities to the storage operator. The retrieval primitives are general; LightRAG is the first consumer.

What this slice is not. It is not the ingest LLM (entities/relations are extracted on the client today; an in-TEE extractor is a roadmap item). It is not the generation-layer defence against AGEA-class extraction over many legitimate API calls (that is PrivGemo / DP-RAG territory, separate from the retrieval crypto here). It is not a portability story for thin browsers: 50–325 MB of in-CVM client state per tenant means we ship as a server-resident multi-tenant CVM, not as on-device code.

§02

Definitions & glossary

TermDefinition
LightRAGgraph-augmented RAG (HKUDS/LightRAG, Guo et al. EMNLP 2025). Corpus → KG of entities + relations + chunk pointers; query → dual keyword extraction + 1-hop expansion + chunk fan-out
TEE-anchored / on-device deploymentstwo parallel deployments of the same Compass protocol with different trust anchors. TEE-anchored: client role inside SEV-SNP CVM (this page). On-device: paper-faithful Compass on the user's own device
kg_queryLightRAG's retrieval entry point (lightrag/operate.py:3164). Returns the assembled context for one of five modes: local · global · hybrid · mix · naive
hl / ll keywordshigh-level (global, thematic) and low-level (local, specific entity) keywords extracted from the query by an LLM. Drive the relations VDB and entities VDB respectively
HNSWHierarchical Navigable Small World graph (Malkov & Yashunin) — the graph-based ANN index Compass wraps. Multi-layer; top layer holds the entry point
efHNSW dynamic candidate-list width — the beam width during greedy search. Larger ef ⇒ higher recall, more work. In Compass, the number of batched ORAM round-trips per query is n = ⌈ef / ef_spec⌉, so ef is publicly observable via RPC count
ef_specCompass speculation-set size — the number of candidates whose neighbours are fetched in one batched ORAM request (Speculative Neighbor Prefetch)
ef_nCompass directional-filter size — after quantised hints prune neighbours by query direction, only the top ef_n per candidate are fetched
MHNSW degree bound — max number of neighbours per node at each layer. Determines per-block payload size in the ORAM (D · f32 + M · u32)
CompassZhu, Patel, Zaharia, Popa (OSDI 2025). HNSW-over-Ring-ORAM with three optimisations: Directional Neighbor Filtering, Speculative Neighbor Prefetch, Graph-Traversal-Tailored ORAM
Ring ORAMRen et al. — tree-based ORAM with Z + S blocks per bucket (Z real + S dummy). Constant online bandwidth via the XOR trick; eviction is amortised every A accesses (off the user's critical path)
Z, S, ARing-ORAM bucket parameters — real-block slots, dummy slots, eviction rate. Tuned per index
ORAMOblivious RAM (Goldreich-Ostrovsky). Protocol that makes a remote storage server's view of access patterns computationally indistinguishable from random
EMMEncrypted Multi-Map. Key → list-of-values primitive. The natural shape for an adjacency list or a source_id list
XorMMPatel-Persiano-Yeo (CCS 2022). Non-lossy volume-hiding EMM: response size independent of the queried key. 1.5–2× storage overhead
source_idLightRAG field on each entity / relation prop bag: a GRAPH_FIELD_SEP-delimited list of chunk IDs the entity appears in. Variable length ⇒ length leaks the entity's identity unless hidden
AP / QP / SPSSE leakage profiles: Access Pattern (which records returned), Query Pattern (which records matched), Search Pattern (whether two queries match the same set). ORAM hides AP; XorMM hides volume / partial QP; HMAC perturbation hides SP across sessions
CVMConfidential VM. The encrypted-memory virtual machine the trusted code runs in. AMD SEV-SNP in this codebase
SEV-SNPAMD Secure Encrypted Virtualization — Secure Nested Paging. CVM technology providing memory encryption + attestation
RATLSRemote-Attested TLS. Client verifies the SEV-SNP attestation report inside the TLS handshake before sending plaintext. Existing transport in gelo-rag::two_party_service
VCEKVersioned Chip Endorsement Key. The SEV-SNP attestation root, signed by AMD
HKDFHMAC-based Key Derivation Function (RFC 5869). Used for the two-party (user_x_sk, tee_user_x_sk) → per-tenant child key chain
CAPRISE / DPEDistance-Preserving Encryption — server can run cosine over ciphertext directly (Ye et al. 2026). In the TEE-anchored deployment, CAPRISE wraps the embedding inside the Compass block as defence in depth
DistanceDPRemoteRAG's (n, ε)-DP construction — planar-Laplace noise on the embedding such that any two queries within radius r are formally indistinguishable. Already lives in crates/remote-rag::planar_laplace
HMAC perturbationThis page's per-session deterministic embedding perturbation — see §05 FIG. 05 inset and the search-perturb card in §04
OpalKaviani et al. 2026, private memory for personal AI. Runs Compass inside a TEE; cited because its security proof imports Compass's batched-access lemma into the G_att-hybrid model — formal precedent for the client-role swap on this page
AGEAAgentic Graph Extraction Attack (arXiv 2601.14662, 2026). Reconstructs the KG over many legitimate API calls — defended at the generation layer (PrivGemo / DP-RAG), not by retrieval crypto
GRAGPOISON / LogicPoisonIntegrity attacks on GraphRAG corpora — defeated by Compass's malicious-server mode (Merkle over the ORAM tree) combined with in-CVM ingest
Vec2TextMorris et al., EMNLP 2023. Inverts embedding vectors back to near-exact source text — the canonical motivation for keeping embeddings off the storage server
PrivGemo / DP-RAGGeneration-layer defences composed on top of the retrieval layer (out of scope here): structural KG anonymisation (PrivGemo, arXiv 2601.08739) and output-side DP (DP-RAG, arXiv 2602.14374)
§03

Threat model

Same trust geometry as the CAPRISE path (storage page): the CVM is trusted, the storage server and network operator are not, the client endpoint is trusted for the duration of an RATLS session. GraphRAG introduces structural attack surfaces the flat vector path does not have — every entity, every relation, every per-node degree, every adjacency list is now a distinct side channel. The TEE-anchored deployment neutralises those by routing every server-visible operation through one of three primitives: Ring-ORAM (over an encrypted HNSW or KV blob), XorMM volume-hiding multi-map (over adjacency & source-id lists), AES-GCM blob fetch (over chunk bodies).

Departure from the Compass paper. Compass's paper places its protocol "client" — the party holding the position map, stash, treetop cache, and ORAM key — on the user's own device (laptop / phone / browser), explicitly avoiding TEEs because enclave side channels weaken the story. This design shifts that client role into the SEV-SNP CVM. The Compass protocol is unchanged; the trust anchor moves from "user owns the hardware" to "SEV-SNP attestation + AMD vendor." Opal (Kaviani et al. 2026) already does this composition and formally imports Compass's batched-access lemma into its G_att-hybrid proof — the security property holds because it's a property of the protocol, not of which silicon hosts the client. The thin-client / multi-tenant SaaS shape this prototype targets cannot put 5–500 MB of ORAM state on every user's browser, and SEV-SNP attestation is the trust anchor this codebase already relies on for CAPRISE-at-rest. Deployments that cannot accept SEV-SNP trust should use the on-device deployment from the design doc instead — paper-faithful Compass on the user's device. The two are parallel deployments of the same protocol with different trust anchors, not primary and fallback.

AspectPaper-faithful CompassTEE-anchored (this design)
Trust anchoruser owns the hardwareSEV-SNP attestation + AMD vendor
Compass client state livesdevice RAMencrypted CVM RAM · hidden from host OS
Side-channel exposuredevice-localSEV-SNP side channels in-scope (CVE-2023-20593, Hertzbleed, PSP firmware)
User-facing clientthick · 5–500 MB ORAM statethin · RATLS + plaintext requests only
State portabilityper-device, user-managed synccentralised · switch devices freely
Multi-tenantone user per devicemany tenants per CVM · HKDF-isolated
Forward securityOS-process isolationtwo-party KDF Option 3 · TEE-seal break alone does not recover past sessions
PartyTrustWhat it seesWhat is hidden
User text + tenant secretconfidential
Clienttrustedplaintext query · user_x_sk · assembled context / top-k returned over RATLS
TEE (SEV-SNP CVM)trustedplaintext text · keyword strings · embeddings · entity / relation / chunk plaintext · ORAM position maps + stash · derived OramKey × 3 + EmmKey × 2 + CAPRISE seed + AES chunk key · tee_user_x_sk
Storage server (operator)untrustedencrypted Ring-ORAM tree buckets · XorMM hash tables · AES-GCM chunk blobs · request rates & timingplaintext text · embeddings · entity / relation / chunk identifiers · access patterns over the tree · adjacency-list sizes · per-node degree
Network operatoruntrustedRATLS-wrapped traffic · attestation evidence · request rate (padded by runner)RATLS contents
Attack surface (graph-specific)What leaks today in plaintext LightRAGDefence (this design)
Per-query embedding inversion (Vec2Text-class)ll_emb, hl_emb, q_emb → readable keyword textembedding stays inside CVM · only reaches storage as a Compass ORAM access
Access pattern over the three HNSW indexeswhich entity / relation IDs got looked upRing-ORAM under Compass — paths random by construction · paper §4.10 proof
Per-node degree fingerprintvariable-length get_nodes_edges_batch reply uniquely identifies the queried entitypartially shipped: XorMM-padded adjacency lists hide list-length leakage at the server boundary · the "degree absorbed into encrypted prop bag" half (Risk D) is deferred until the node-props encrypted KV lands — degree currently lives in cleartext in-CVM only
source_id fan-out length5-chunk entity vs 50-chunk entity distinguishable by reply sizelift source_id out of prop bag · serve via XorMM with bucket-padded volume (Risk E)
AGEA-class extraction over many queries96 % node leak at T=1000 on LightRAG · KG reconstructableorthogonal — defended at the generation layer (PrivGemo / DP-RAG); retrieval crypto is necessary, not sufficient
Cross-session linkability of identical queriessame ll_keywords ⇒ same Compass trace ⇒ linkableper-session HMAC perturbation on the embedding (Risk F) · composes with RemoteRAG DistanceDP
Search-volume leakage (request count)idle vs heavy sessions distinguishablerequest-rate padding at the runner level
GRAGPOISON / LogicPoison corpus integritypoisoned relations propagate to all queriesCompass malicious-server mode (Merkle over ORAM) detects mutation · ingest happens inside CVM
Compromise scenarioWhat the attacker recoversWhat stays safeOutcome
Untrusted storage operator aloneORAM ciphertext + XorMM buckets + AES chunks + access timingany key material · plaintext entities / relations / chunks · plaintext access patternsafe
Malicious storage server tampering with bucketsCompass Merkle integrity catches mutation with overwhelming probabilitysafe
TEE seal broken (memory dump, CVM rollback)tee_user_x_sk · and derived OramKeys / EmmKeys / aes_chunk_key for any tenant whose store is currently resident (the LightRAG service deliberately holds them — re-decrypting the HNSW per request is not viable; the existing CAPRISE service still re-derives + wipes its two-key set per request)user_x_sk (never persisted in CVM, RATLS forward-secret) · CAPRISE keys for the embedder path · all stored ciphertext at rest if the storage server is honestpast embedder sessions safe · resident LightKgStore tenants compromised — re-bootstrap required
Client endpoint compromiseuser_x_sk · plaintext context returned over RATLSeverything earlier in the session’s ORAM historysafe — needs both halves
TEE + client compromised simultaneouslyboth halves · all derivable keys · all stored ciphertextbroken (accepted in design)
Attacker farming AGEA-style queries through the legitimate APIreconstructed KG over many sessionsretrieval crypto · raw stored databroken at the API layer; mitigated by the generation-side defences (out of scope here)
Network / host observer correlating execution fingerprints (timing · round count · batch sizes · SEV-SNP-leaky perf counters) across sessions of the same tenantwhich sessions issued the same queryquery content · access pattern · retrieval resultssafe — per-session HMAC perturbation on each embedding randomises the HNSW traversal across sessions; same tenant + same query in two sessions produces two different execution fingerprints

Inherited weakness. CAPRISE remains distance-preserving inside the Compass block payload — defence in depth, not a substitute. The ORAM hides which block is read; CAPRISE keeps the block contents useless if the AES-GCM key is later broken. A key-holder still recovers plaintext (Vec2Text).

§04

Components

Each card pairs [method ↦ source ↦ what it secures ↦ threat ↦ overhead]. Crates tagged SHIPPED are end-to-end functional behind release-tested integration suites; deferred sub-features are called out in the card body and tracked in §07 Status & gaps.

SHIPPED · ORAM

ring-oramcrates/ring-oram · 34 tests

Ring-ORAM semi-honest baseline + multi-hop lazy eviction (Compass §4.7) + treetop caching. Each bucket holds Z + S blocks (Z real + S dummy). Async BlockBackend trait so the same client runs against the in-memory backend or a REST backend without code change. AES-GCM nonce = u64-LE bucket_id ‖ u32-LE write_counter.

method
read · write · admit · evict_path · flush_evictions · client-held position map + stash · reverse-lexicographic eviction cursor · treetop cache of top t bucket levels (skips backend on the hot path)
source
Ren et al. Ring ORAM (USENIX Security 2015) · port verified against Clive2312/compass
secures
access pattern over the encrypted blob store
threat
semi-honest storage server · network observer · client compromise
cost
O(log N) online · in-memory measured ~10 µs per read at N=2000 (see §06)
deferred
Merkle integrity (malicious-server mode) · XOR trick for constant online bandwidth · batched ORAM reads
SHIPPED · ANN

compass-indexcrates/compass-index · 13 lib + 3 recall

Layered HNSW-over-Ring-ORAM (Malkov-Yashunin upper-layer descent in CVM cleartext · layer-0 beam search ORAM-mediated) with Compass §4.5 Directional Neighbor Filtering (4-bit quantised hints prune neighbours before the ORAM read, ~50 % read reduction measured). Per-block payload (D-vec, M-neighbour list) serialised + AES-GCM-wrapped inside the ORAM bucket. 1K-vector recall @ release ≥ 90 %.

method
upper-layer descent cleartext in CVM (no ORAM hops) · layer-0 beam search ORAM-mediated · Algorithm-4 neighbour selection at build · CompassParams defaults pinned for attestation (M=16, ef_construction=200, ef_search=64, ef_n=4) · paper-parity build seed
source
Zhu, Patel, Zaharia, Popa — Compass, OSDI 2025 · Malkov & Yashunin — HNSW, TPAMI 2018
secures
query content · access pattern over the index
threat
storage operator with full DB access · network observer reading any RPC trace
cost
10 → 34 ms p50 / query over N=100 → 2000 (in-memory, release, see §06) · sub-linear scaling — HNSW theory holds through the ORAM layer
deferred
batched ORAM reads (union-of-paths) · Speculative Neighbor Prefetch (depends on batched reads) · CAPRISE-wrap inside the block payload (defence in depth, not blocking) · paper-scale bench parity
SHIPPED · EMM

xormm-emmcrates/xormm-emm · 13 tests

Non-lossy volume-hiding encrypted multi-map. Built statically from a full key→list map; get returns the value list with response size independent of the queried key (constant volume_bound per EMM). Used for the LightRAG adjacency lists and per-entity src_chunks lists.

method
cuckoo hashing with two PRF-derived candidate buckets per key · (dummy_flag, fingerprint, value_count, padded_values) bucket layout · AES-GCM-encrypted under per-EMM HKDF child (emm_adjacency_key, emm_src_chunks_key) · in-CVM stash for cuckoo failures
source
Patel, Persiano, Yeo — XorMM: practical volume-hiding EMM, CCS 2022 · paper's XOR-filter optimisation deferred
secures
(1) which key was queried, (2) the size of the returned value list — i.e., per-node degree, (3) source-chunk fan-out widths
threat
storage operator inferring entity identity from adjacency-list length · per-session linkability via repeated lookups
cost
~50 µs per get measured (in-memory, N=100–2000 — §06) · sub-linear in corpus, only scales with top_k
deferred
XOR-filter placement (paper's guaranteed-O(1) variant) · async REST backend for the byte store (currently sync in-memory only) · fresh-tier incremental rebuild
SHIPPED · facade

light-kg-storecrates/light-kg-store · 7 lib + 1 acceptance · M6

LightRAG-shaped storage facade: 3× CompassIndex (entities · relations · chunks) + 2× XorMmClient (adjacency · src_chunks) + 1× AesChunkStore. Per-tenant keys derived from V2 HKDF children. build_from_kg(ExtractedKg, params, &derived) → LightKgStore is the single ingest entry point; queries are query_entities_topk, adjacency_for_entity, src_chunks_for_entity, chunk_text.

method
per-tenant key set derived once via HkdfPolicyV2 from (user_x_sk, tee_user_x_sk, tenant_id) → 8 child keys · HMAC-based logical-key derivation maps entity / relation names to LogicalKey per EMM with domain labels (ADJACENCY_ENTITY, SRC_CHUNKS_ENTITY, …) and null-byte separator against shift collisions · generic over both backends so the REST stack drops in via build_from_kg_on
source
in-house
secures
composes the three primitives behind a single LightRAG-shaped API · keeps cross-EMM key derivations safely isolated by domain label
threat
operator stitching together independent leaks across primitives · CVM-internal observer of the in-CVM cleartext name → block_id maps (Risk F — pseudonymisation is post-M9)
cost
ingest measured: 22 ms / 213 ms / 1.55 s for N=100 / 500 / 2000 entities (§06) — HNSW build dominates · ~110 MB CVM state at SIFT1M-scale projected (paper Tab. 4 ×3)
deferred
encrypted KV for node_props / edge_props (closes Risk D fully) · sub-derived ORAM keys per CompassIndex (CompassIndex::with_oram_key) · incremental ingest (currently rebuild on every ingest)
SHIPPED · protocol

lightrag-privatecrates/lightrag-private · 7 lib + 2 integration

Rust port of LightRAG's kg_query orchestration. Local + Hybrid modes shipped: ll_embedding → perturb(Ll) → entities.search → adjacency fan-out → src_chunks fan-out → chunk_text decrypt → context string; Hybrid adds hl_embedding → perturb(Hl) → relations.search → endpoint fan-out merged into the entity set. Faithful to upstream modulo: (a) accepts pre-extracted keywords as input (client-side extraction today; in-TEE LLM deferred) · (b) per-session HMAC perturbation on every embedding (Risk F).

method
LightRagPrivateService::kg_query(ll, hl, params, session_key) → KgContext · KgContext::to_context_string() renders the LLM input · multi-tenant LightRagTwoPartyService wraps it with per-tenant HashMap<TenantId, LightKgStore> + persistent tee_user_x_sk
source
HKUDS/LightRAG lightrag/operate.py · Guo et al. EMNLP 2025
secures
upstream retrieval semantics, transposed onto the encrypted store
threat
regressions silently degrade either accuracy (Risk H) or security (Risk D / E)
cost
Local 10 → 34 ms / Hybrid 14 → 58 ms p50 at N=100 → 2000 (§06) · cross-session linkability test: 100/100 trials produce distinct perturbed embeddings under different session nonces
deferred
Global / Mix / Naive modes · _token_truncation · bit-for-bit upstream _build_context_str + _merge_all_chunks parity (needs Python reference for diff) · (weight, cosine) edge sort (Risk J)
SHIPPED · search privacy

search-perturblightrag-private :: perturb · 5 unit tests

Per-session HMAC perturbation on every embedding before it enters Compass. Breaks cross-session linkability — Compass + Ring-ORAM hides the storage-side access pattern, but two sessions issuing the same query still produce identical execution fingerprints (round count · batch sizes · timing · SEV-SNP-leaky perf counters). This step randomises the start vertex of the HNSW traversal per session, closing that residual. Measured cost: 2–6 µs per embedding on the bench.

method
session_key = HMAC(search_pattern_key, session_nonce) · per-embedding direction = unit_vector(HMAC(session_key, kind ‖ 0x00 ‖ quantize(e))) · e' = normalize(e + ε · direction) · kind ∈ {q, ll, hl} · ε = 2 % default (cosine to original ≥ 0.995 pinned in unit test)
composes with
DistanceDP from remote-rag::planar_laplace — opt-in formal (ε, δ)-bound per call · stacks cleanly: HMAC fixes session origin (deterministic), DistanceDP adds per-call randomness (formal DP)
secures
execution-pattern correlation across sessions · session-origin linkability of repeated queries
threat
network / host observer correlating timing & perf-counter fingerprints across sessions · classical search-pattern leakage in the SSE sense
cost
2–6 µs / embedding measured · invisible against the ~10 ms CompassIndex critical path · linkability test pins 100/100 trials produce distinct perturbations under different session nonces
SHIPPED · KDF

HkdfPolicy · v2crates/core :: keying · M0

Extends v1's two-info-string derivation (caprise_seed · aes_chunk_key) to an eight-child set: + oram_keys × 3 (entities · relations · chunks) + emm_keys × 2 (adjacency · src_chunks) + search_pattern_key. Same Extract, more Expands. Versioned via gelo-rag.v2.…; bumping the version yields entirely fresh keys across all primitives.

method
one Hkdf::<Sha256>::new(salt = tenant_id, ikm = user_x_sk ‖ tee_user_x_sk) · eight hk.expand("gelo-rag.v2.{role}", &mut buf) · each child Zeroizing<[u8; 32]> · SchemeParamsV2 + CompassParams + XorMmParams + LightRagParams canonically encoded into the attestation digest
source
RustCrypto hkdf 0.12 + hmac + sha2 (already in workspace; no new crypto)
secures
cross-primitive key separation · forward security identical to v1 Option 3
threat
downgrade to v1 — pinned in scheme_identity baked into REPORT_DATA[32..64]
cost
µs per call · trivially in the request hot path
SHIPPED · service

LightRagTwoPartyServicelightrag-private :: two_party_service

Mirror of GeloRagTwoPartyService with one deliberate divergence: the LightKgStore holds the derived OramKeys/EmmKeys/aes_chunk_key persistently per-tenant (re-decrypting the HNSW per query is not viable). user_x_sk is still consumed + wiped at ingest; tee_user_x_sk is the only residual at idle. Same loud-failure contract as the embedder service.

method
ingest_kg_for(tenant, user_x_sk, kg, params) derives V2 children + builds LightKgStore + retains it under HashMap<TenantId, Arc<Mutex<Tenant>>> · query_for(tenant, ll, hl, params, session_nonce) mints a SessionKey per request and drives kg_query · UnknownTenant → HTTP 410 Gone mapping pinned in runner test
source
in-house · realises the TEE-anchored deployment composed with two-party KDF Option 3
secures
cross-tenant isolation · loud-failure on CVM restart · all of §03's defences in a single request handler
threat
untrusted cloud operator · TEE seal break recovers resident store keys (§03 compromise table) · breaks under simultaneous TEE + client compromise
cost
HKDF + LightKgStore lookup + Compass + XorMM + AES per request · 10–58 ms p50 in-memory measured · ~ms-class HTTP overhead through axum (in next bench tier)
SHIPPED · tooling

graphrag-benchcrates/graphrag-bench · compass-rag-bench binary

End-to-end stage-timed bench harness. Mirrors kg_query orchestration with Instant::now() boundaries between every stage so we can attribute query latency to perturb / entities_search / relations_search / adjacency / src_chunks / chunk_decrypt. CSV out to stdout; p50/p95/p99/mean/stddev summary to stderr; mean layer0_reads/q headlined.

method
three planned scenarios — in_memory shipped, local_rest + runner_http staged · env-configured (BENCH_SIZE, BENCH_DIM, BENCH_QUERIES, BENCH_MODE) · deterministic seed off corpus dimensions · 5-query warmup discarded from stats by default
source
in-house · pre-shipment bench infrastructure
cost
N=100 in 1 s · N=2000 in ~10 s · ingest dominates beyond ~500
deferred
REST-backed scenario · plain-hnsw_rs baseline for the ORAM-vs-cleartext multiplier · paper-fixture parity (LAION 100K, SIFT1M)
§05

Compute flow & trust boundaries

FIG. 04 — LightRAG-private kg_query path · 3× Compass + 2× XorMM + AES-GCM chunks solid red = trusted secret · solid teal = encrypted RPC over wire · purple = ORAM/EMM access · dashed red = trust boundary
Client (thin) TEE (SEV-SNP CVM) Storage server (untrusted blob store) RATLS · TLS 1.3 ECDHE · request-rate padded HTTP · Ring-ORAM RPC · XorMM lookups · AES-GCM blobs ① Request tenant_id · user_x_sk query · mode · top_k hl_keywords · ll_keywords ⑨ Receive context assembled prompt over RATLS no client crypto ② HKDF v2 → 8 child keys · search_pattern → ④ ③ Embed (GELO) q · ll · hl in one batch ④ Per-session perturb HMAC + optional DistanceDP ⑦ Merge + truncate round-robin · token budget ⑧ Assemble context entities + relations + chunks → prompt ⑤a Compass · entities ⑤b Compass · relations ⑤c Compass · chunks (mix) ⑤d ORAM · node/edge KV deferred · M6.x ⑥a XorMM · adjacency ⑥b XorMM · source_id ⑥c AES-GCM · chunks tee_user_x_sk table enc. HNSW · entities Ring-ORAM tree ~2N buckets · Merkle enc. HNSW · relations Ring-ORAM tree ~2N buckets · Merkle enc. HNSW · chunks Ring-ORAM tree ~2N buckets · Merkle enc. KV · node / edge props deferred · M6.x XorMM · adjacency volume-padded buckets XorMM · source_id volume-padded buckets AES-GCM chunk blobs (chunk_id, nonce, ct) tenant + user_x_sk Ring-ORAM read batched ORAM XorMM lookup AES-GCM fetch plain context over RATLS SEV-SNP report · pinned scheme_identity
#StepWho runs itWhat crosses the boundary
Client opens RATLS, verifies SEV-SNP attestation against pinned scheme_identity (covers HkdfPolicy v2 + LightRAG params + CompassParams + XorMmParams), sends {tenant_id, user_x_sk, query, mode, hl_keywords, ll_keywords, params}Client → TEEplaintext text + 32B secret over RATLS
TEE looks up (or creates) tee_user_x_sk[tenant]; runs HkdfPolicyV2::V2.derive(...) → 8 child keys: caprise_seed, aes_chunk_key, oram_keys × 3 (entities · relations · chunks), emm_keys × 2 (adjacency · src_chunks), search_pattern_key (consumed at step ④)TEE
TEE embeds (query, ll_keywords, hl_keywords) in one GELO-masked batch (see embedding page)TEE (+ GPU under mask)masked activations across PCIe
TEE applies per-session HMAC perturbation (and optionally DistanceDP) to each embedding to break cross-session linkabilityTEE
Three Compass searches in parallel: entities_idx.search(ll_emb), relations_idx.search(hl_emb), chunks_idx.search(q_emb) (mix mode only). Each is a Ring-ORAM-mediated HNSW walk with Directional Neighbor Filtering + Speculative Prefetch — see paper §4 / our plan §3. KV reads on node / edge prop bags use the same ORAM controller (separate tree, same construction)TEE ↔ storageRing-ORAM ReadPath RPCs · XOR-trick responses · per-bucket Merkle proofs
Adjacency expansion (local-mode 1-hop): adjacency.get_batch(entity_names) over XorMM · then source_id fan-out via src_chunks.get_batch(...) over XorMM · then chunk bodies via chunks.get_batch(chunk_ids) AES-GCM blobsTEE ↔ storageXorMM lookups (constant-volume) · AES-GCM blob fetches
Round-robin merge across vector_chunks / entity_chunks / relation_chunks · token-budget truncation per QueryParam (constant time over the candidate list)TEE
Assemble the LightRAG context string (entities + relations + chunks + reference list) and structured QueryContextResultTEE
TEE deferred-flushes any pending Compass evictions (off the user's critical path); zeroizes the incoming user_x_sk and the per-request SessionKey derivative; returns the assembled context over the same RATLS session. Note: the OramKeys / EmmKeys / aes_chunk_key live inside the resident LightKgStore for the tenant — they do not wipe at request end (deliberate design choice; see §03 compromise table). tee_user_x_sk stays in CVM RAM as before.TEE → Client / TEE ↔ storageplain context over RATLS · eviction-path RPCs to storage in the background

What the storage server sees, end-to-end. A stream of Ring-ORAM read_path + write_path RPCs against three trees plus two KV trees, plus XorMM bucket reads against two static EMMs, plus AES-GCM blob fetches. Path IDs are uniform-random by construction. Bucket sizes are uniform per-tree. XorMM response sizes are uniform per-EMM. Request rate is constant per RATLS session. Across two distinct queries by the same tenant, the server's view is computationally indistinguishable.

Today's simplification: step ③ "Embed (GELO)" runs on the client, not in the TEE. The current wire format (/lightrag/ingest, /lightrag/query) accepts pre-computed embeddings for every chunk / entity / relation / query — the client owns the GELO + mask + GPU offload pipeline. The diagram preserves the target shape where the TEE re-runs the embedder under attestation, but the present ship trusts the client (same machine as the keyword-extraction LLM) to do this honestly. Moving embedding back into the TEE is the same plumbing as the embedder slice (embedding page) — no new crypto, just a request-path branch.

Note: ingest path. Same trust geometry, opposite direction — client streams documents + extracted KG over RATLS; TEE embeds, builds a plain hnsw_rs::Hnsw in encrypted CVM RAM, runs CompassIndex::from_plaintext_hnsw, pushes the encrypted ORAM tree to storage, builds and pushes the XorMM EMMs, writes the AES-GCM chunk blobs, then zeroizes the plaintext working set. Heavier than query — expect minutes per 100K docs — but a batch operation, not on any user-perceived path.

Inset · Step ④ per-session search perturbation

Why this step exists: Compass + Ring-ORAM randomises the storage-side path IDs, but two sessions of the same tenant issuing the same query still produce identical execution fingerprints (timing · round count · batch sizes · SEV-SNP-leaky perf counters). The perturbation randomises the HNSW start vertex per session, breaking cross-session linkability.

The Compass paper acknowledges a coarse version of this leak — "different query types use different ef, so an adversary can still distinguish the operation type" — and explicitly puts it out of scope. Concretely: Compass's batched ORAM rounds-per-query are n = ⌈ef / ef_spec⌉, so a search at ef=64 takes 4 round-trips while an insert at ef=200 takes 13. The count of round-trips is itself a fingerprint visible to any passive observer, with no decryption required. Compass openly does not try to hide it. Our HMAC perturbation closes the finer-grained content-level version of the same class of leak: same operation, same mode, but two sessions issuing the same query produce different content-adaptive execution patterns (directional-filter prune rate, speculative-prefetch hit rate, hops-to-convergence) — without the perturbation, those signatures are stable enough to link sessions to each other.

FIG. 05 — Per-session search perturbation · HMAC chain + optional DistanceDP composition red = secret · blue = public per-session value · purple = perturbation operation
Client TEE (SEV-SNP CVM) RATLS · session_nonce echoed on follow-up requests request tenant_id · user_x_sk [session_nonce] HkdfPolicy v2 (user_x_sk · tee_user_x_sk · tenant_id) → 8 child keys s_search child key · per-tenant gelo-rag.v2.search-pattern session_nonce runner-issued · 16 bytes stable for session lifetime session_key HMAC(s_search, session_nonce) zeroized at session end q_emb · ll_emb · hl_emb HMAC(session_key, kind ‖ quantize(e)) 32B → direction ∈ S^(D-1) kind = q | ll | hl e' = normalize (e + ε · direction) ε ≈ 1–5 % of ‖e‖ deterministic per session [opt] DistanceDP e'' = e' + planar_lap (ε, δ)-DP · per call remote-rag::planar_laplace → Compass search input step ⑤ Within session: e → e' is deterministic ⇒ repeat queries are cacheable + consistent. Across sessions: different session_nonce ⇒ different session_key ⇒ different direction ⇒ different HNSW traversal ⇒ uncorrelated execution fingerprints. user_x_sk · tenant embeddings (TEE-only) session_key
LayerWithout itAdds
Compass + Ring-ORAM onlyidentical queries across sessions ⇒ identical execution fingerprints (timing · perf counters · round count)baseline AP-hiding · no search-pattern hiding
+ HMAC perturbation (default on)session-origin still linkablecross-session unlinkability · ~µs cost · deterministic within a session (caching preserved)
+ DistanceDP (opt-in per tenant)no formal privacy boundper-call (ε, δ)-DP via remote-rag::planar_laplace · stacks cleanly on top of HMAC · ~µs added
What it doesn't coverAGEA-class extraction (generation-side defence) · embedder side-channels on input length (closed by fixed-length token padding) · within-session repeat-query linkability (deliberate — caching) · SEV-SNP itself broken (TCB ceiling)
§06

Performance & correctness

Numbers below are from the compass-rag-bench binary (crates/graphrag-bench) — release build, single-threaded, in-memory backends (no network), 50 queries per (size, mode) after 5 warmup queries dropped. The bench replicates the kg_query orchestration with Instant::now() boundaries between each stage so latency can be attributed end-to-end. The next bench tiers will swap InMemoryBlockBackend for compass-rest-backend on loopback (REST overhead) and then POST through /lightrag/query (full user-perceived latency).

Query latency · Local mode

N entitiestotal p50entities_searchadjacencysrc_chunkschunk_decryptlayer0_reads / qingest
10010.0 ms10.0 ms18 µs18 µs3.5 µs117.722 ms
50019.4 ms19.3 ms26 µs21 µs9.6 µs173.7213 ms
200033.9 ms33.7 ms27 µs22 µs11.1 µs212.01.55 s

Query latency · Hybrid mode

N entitiestotal p50entitiesrelationsadjacencysrc_chunkschunk_decryptlayer0_reads / q
10014.3 ms10.2 ms4.1 ms47 µs47 µs8.1 µs172.7
50033.3 ms19.3 ms13.7 ms64 µs57 µs21.4 µs312.4
200058.0 ms34.1 ms23.2 ms68 µs59 µs26.4 µs396.9

Where the time goes. CompassIndex search owns the budget: 99.4 % of Local at every size, ~71 % of Hybrid (split between the entities and relations searches, in proportion to corpus size — relations is N·0.33). EMM lookups + AES chunk decrypt + search_perturb combined stay under 0.5 % of total at every scale. This is the batched-ORAM-reads + Speculative-Neighbor-Prefetch optimization budget: at N=2000 each query does 212 sequential ORAM round-trips that in principle could be unioned into a small number of batched paths.

Sub-linear scaling. Wall time grows 10 → 19 → 34 ms for a 20× corpus (~3.4× growth) — HNSW O(log N) traversal cost is the headline, but layer-0 ORAM read count grows even more gently (117 → 174 → 212, only 1.8×). The wall-time gap is the second-order effect: n_leaves scales with corpus (256 → 1024 → 4096), Ring-ORAM tree levels go 8 → 10 → 12, and per-path-read bytes grow accordingly. Ingest cost is dominated by the HNSW build pass (O(N · ef_construction · M) distance ops); the ORAM admit phase is small.

Correctness — recall + security tests

TestCrateAcceptanceStatus
1K-vector top-K recall ≥ 90 % vs brute-forcecompass-index/tests/recall.rsrelease build · cosine over 1000 unit vectors @ D=64passing
Directional filter reduces layer-0 reads without breaking recall (within 15 %)compass-index/tests/recall.rsfilter on vs off, 256 vectorspassing
Build_from_kg round-trips all six storeslight-kg-store/tests/build_and_query.rs16 entities · 8 relations · 24 chunks · every surface API exercisedpassing
Local-mode kg_query threads all stageslightrag-private/tests/local_kg_query.rstarget entity surfaces in result · chunks decrypt · context-string structure correctpassing
Cross-session linkability — 100/100 trials divergelightrag-private/tests/local_kg_query.rssame query under two distinct session nonces produces distinct perturbed embeddings (100 random vectors @ D=64)100/100
REST backend round-trips ORAM + Compasscompass-rest-backend/tests/integration.rsaxum + sled server on loopback · recall @ N=256 ≥ 85 %passing
HTTP /lightrag/ingest/lightrag/query round-tripgelo-snp-runner (mock mode)4 entities · 2 relations · 4 chunks · query surfaces seeded entity · chunks decryptpassing
HTTP unknown-tenant → 410 Gonegelo-snp-runner (mock mode)loud-failure contract matches the embedder servicepassing
Hybrid-mode threads ll + hl axesgelo-snp-runner (mock mode)ll hit + hl-relation endpoint both surface in single responsepassing

Test totals. ring-oram 34 · xormm-emm 13 · compass-index 13 lib + 3 recall · compass-rest-backend 5 lib + 3 integration · light-kg-store 7 lib + 1 acceptance · lightrag-private 7 lib + 2 integration · gelo-snp-runner (mock mode) 3 lightrag routes. All release-mode where needed (compass-index recall, light-kg-store acceptance, graphrag-bench); see memory/feedback_compass_recall_release.md for the rationale.

§07

Status & gaps

The vertical end-to-end (client → /lightrag/ingest → encrypted store → /lightrag/query → context string) is shipped, including Local and Hybrid kg_query modes and the unknown-tenant loud-failure contract. Below: feature-by-feature status with deferrals called out, then the residual gaps grouped by axis.

FeatureStatusNotes
Skeleton crates · HkdfPolicy v2 · CompassParamsshipped8 child keys derived; scheme_identity_fragment canonically encodes CompassParams + XorMmParams + LightRagParams for the SEV-SNP report
ring-oram semi-honest baselineshipped34 tests · AES-GCM nonce = bucket_id ‖ write_counter · Merkle malicious-mode deferred
xormm-emm cuckoo pathshipped13 tests · XOR-filter optimization deferred
compass-index plain Ring-ORAM HNSWshipped1K-vector recall ≥ 90 % strawman pin
Multi-hop lazy evictionshippeddefer + flush on the search RAII guard (later inlined for async)
Treetop cachingshippedtreetop_levels param · skips backend on the hot path
Batched ORAM readsdeferredWill union-of-paths into one REST call · unlocks Speculative Neighbor Prefetch.
Directional Neighbor Filteringshipped4-bit signed quantization · reads-reduction test in compass-index/tests/recall.rs
Speculative Neighbor PrefetchdeferredDepends on Batched ORAM reads
Layered HNSW (Malkov-Yashunin)shippedupper layers cleartext · layer-0 ORAM-mediated · entry-at-layer-0 small-corpus bug fixed during bring-up
Rust test infra port (LAION / SIFT1M fixtures)deferredPaper-scale parity bench; would unlock a strict latency ceiling. graphrag-bench is the synth-corpus pre-shipment.
async-ify BlockBackendshipped#[async_trait] · RAII evict-guard replaced with explicit defer/flush
compass-rest-backend crateshippedaxum + sled server · RestBlockBackend client · msgpack body · compass-storage-server binary
REST integration testshippedrecall through localhost REST · per-tenant URL isolation pin
gelo-snp-runner per-tenant URL gateshippedcompass::backend_url(root, tenant, IndexKind) → String · validates tenant against [A-Za-z0-9_-]
object_store S3/GCS/R2 adapterdeferredFeature-flagged behind the REST server
light-kg-store composition + build_from_kgshipped3× CompassIndex + 2× XorMm + 1× AesChunkStore · encrypted node/edge-props KV deferred
search_perturb + Local-mode kg_queryshippedperturb construction · linkability test 100/100
Hybrid-mode kg_queryshippedhl + ll axes · relation-endpoint fan-out into the entity set
_token_truncation capdeferredCurrently uncapped; LightRAG-parity diff awaits upstream merge parity
bit-for-bit _build_context_str + _merge_all_chunksdeferredNeeds an upstream Python LightRAG reference to diff against
LightRagTwoPartyService + /lightrag/{ingest,query,attest}shippedJSON wire · per-tenant HashMap<TenantId, LightKgStore> · sub-router merged into build_router
410 Gone on unknown-tenant /lightrag/queryshippedmatches embedder loud-failure contract
RATLS-layer request-rate padding · Global/Mix/Naive payloadsdeferredTied to extra-mode rollout · request-padding closes the §03 search-volume side channel

Residual gaps grouped by axis.

AxisWhat's missingCost of skipping it
Storage-server integrityMerkle tree over the Ring-ORAM buckets (malicious mode)storage operator can corrupt buckets without detection · baseline is semi-honest only
Hot-path bandwidthXOR trick for constant online bandwidtheach read currently sends a full path; XOR trick collapses it to one bucket of online traffic
Search throughputBatched ORAM + Speculative Neighbor Prefetchat N=2000 each query is 212 sequential ORAM round-trips · the bench (§06) confirms this is the optimization budget
Cloud durabilityobject_store adapterstorage server's sled DB is local only; one-host failure loses the encrypted blob
Risk D — degree leakageencrypted node/edge-props KV behind Ring-ORAMdegree lives in cleartext in CVM RAM; not visible to storage but visible to a CVM-internal observer (TEE seal break)
Risk F — entity-ID pseudonymisationHMAC-wrapped entity_name → block_id mapsCVM-internal observer sees the plaintext name → ID maps; mitigated by the TEE boundary but not by the crypto
LightRAG mode coverageGlobal / Mix / Naive · plus the upstream merge + token-truncation parityLocal + Hybrid (the two most common modes per upstream usage) ship; the other three are mode-specific wiring on top of the same primitives
Embedding locationtoday's wire format takes pre-computed embeddings from the client; the in-TEE GELO path stays on the embedder slicethin-client query becomes possible only when this lands; meanwhile the client extractor must run on owner-trusted compute
Search-volume side channelRATLS-layer request-rate paddingidle vs heavy sessions distinguishable on traffic volume; closes a §03 row
Generation-layer extractionAGEA / GRAGPOISON defences (PrivGemo, DP-RAG)orthogonal to retrieval crypto; out of scope here but flagged as the next layer in any production deployment