storage
— CAPRISE-at-rest

Where vectors land. The index server holds distance-preserving ciphertext. Cosine ANN runs directly on the ciphertext; the CAPRISE seed is HKDF-derived inside the SEV-SNP CVM per request from a two-party secret (spec) and zeroized at return. The chosen production storage path.

scheme  CAPRISE (Ye et al. 2026)
key model  Option 3 · two-party HKDF
runner  gelo-snp-runner ships this
tests  9 / 9 e2e passing
production storage path · two-party HKDF
§01

Role in the project

Once the embedding layer has produced a pooled unit vector, the storage layer is responsible for two things: what gets persisted on the untrusted cloud, and what does the operator see when a query arrives? The chosen answer is CAPRISE-at-rest with two-party HKDF: the index is encrypted such that the operator never sees plaintext doc embeddings, the CAPRISE key never persists in the TEE outside an attested session, and the runner derives it deterministically from the client-held + TEE-held halves on every request.

The cloud operator with full DB access learns ciphertext geometry and access patterns — but not the documents, not the queries, and not which plaintext vector any given ciphertext encodes. The strongest property we can extract from a fully untrusted storage shape without going to FHE (which is orders of magnitude too slow at this scale).

An alternative path — RemoteRAG (planar-Laplace query DP + Paillier homomorphic rerank against a plaintext doc index) — was prototyped during this work and is documented at storage-remoterag.html. The two are mutually exclusive at the type level: Paillier needs plaintext doc embeddings on the server, which is exactly the at-rest confidentiality this path prevents. The project chose CAPRISE-at-rest because the threat model targets a confidential corpus on an untrusted cloud; RemoteRAG would be the right call for the inverse shape (untrusted query against a public corpus).

§02

Definitions & glossary

TermDefinition
CAPRISECiphertext-Aware Privacy-pReserving Inner-product Similarity Encryption (Ye et al. 2026). Distance-preserving encryption: cosine similarity over ciphertext equals cosine over plaintext up to a known coefficient, so the server can run ANN directly on ciphertext.
Two-party HKDFThe CAPRISE seed and AES-GCM chunk key are derived per request via HKDF from (user_x_sk ‖ tee_user_x_sk). user_x_sk lives client-side; tee_user_x_sk lives inside the SEV-SNP CVM. Neither half alone is sufficient — forward-secure against TEE-only break.
RATLSRemote-Attested TLS. Client verifies the SEV-SNP attestation report inside the TLS handshake before sending plaintext; ECDHE is forward-secret per session.
SEV-SNP / CVMAMD Secure Encrypted Virtualization · Secure Nested Paging. Confidential VM technology providing memory encryption + attestation.
MEASUREMENTSHA-384 of the CVM launch image + initial register state. Pinned in the SEV-SNP attestation report — any change in TEE code rotates this value and invalidates impersonation.
REPORT_DATA64-byte field in the SEV-SNP attestation report controlled by the guest. This codebase binds (model_id, scheme_id, nonce) into it so attestation covers the deployed scheme parameters.
Vec2TextMorris et al., EMNLP 2023. Inverts embedding vectors back to near-exact source text — the canonical reason for keeping embeddings off the storage server even if "only" the geometry leaks.
DPEDistance-Preserving Encryption — the class of schemes (CAPRISE is one) that preserve a chosen distance function across the boundary.
§03

Threat model

Client + TEE trusted; GPU + storage + network untrusted. The two-party-KDF construction adds forward security against TEE-only compromise: a future seal break recovers tee_user_x_sk but not user_x_sk (never persisted; the RATLS that carried it is ECDHE-forward-secret), so past sessions remain undecryptable.

Component Trust What it sees What it does NOT see
User-side text + tenant secretconfidential
Clienttrustedplaintext query · user_x_sk · plaintext top-k returned over RATLS
TEE (SEV-SNP CVM)trustedplaintext text · activations · derived caprise_seed_key + aes_chunk_key · tee_user_x_sk · model weights
Storage server (operator)untrustedCAPRISE ciphertext vectors · AES-GCM chunk blobs · access patternsplaintext docs · plaintext queries · which plaintext each ciphertext encodes · CAPRISE key
Network operatoruntrustedRATLS-wrapped traffic · attestation evidenceRATLS contents
Compromise scenarioWhat the attacker recoversWhat stays safeOutcome
Untrusted storage operator aloneciphertext + access patternsany key materialsafe
TEE seal broken (memory dump, CVM rollback)tee_user_x_skuser_x_sk (never persisted; TLS forward-secret)past sessions safe
Client endpoint compromiseuser_x_sktee_user_x_sksafe — needs both halves
TEE + client compromised simultaneouslyboth halvesbroken (accepted in design)
Active malicious CVM exfiltrating user_x_sk mid-sessionboth halves via runtime exfilbroken; mitigated by pinning MEASUREMENT

Known structural weakness. CAPRISE is distance-preserving by construction — the server can run cosine over ciphertext directly. That property leaks inter-embedding distance geometry even though it hides plaintext vectors. NOT secure against a key-holder running Vec2Text.

§04

Components

Each card pairs [method ↦ source ↦ what it secures ↦ threat ↦ overhead].

at-rest cipher

CAPRISErag_core :: caprise

Distance-preserving embedding encryption — server runs plain cosine over ciphertext, recovers cleartext only with the key. Asymmetric noise coefficients keep query-doc rank order intact.

method
scale + radial-Gaussian noise on the unit sphere · PRF-derived per-nonce direction · 3/8 coefficient for docs, 1/8 for queries · CapriseKey::from_seed takes HKDF output
source
Ye et al. 2026 / prRAG — arXiv
secures
stored embedding vectors against a curious operator without the key
threat
curious cloud operator with full DB access. NOT secure against a key-holder running Vec2Text.
cost
~0 retrieval overhead · 2,339 vec/s encryption · Vec2Text BLEU drops 83 → 12.4 (paper)
legacy cipher

SAPrag_core :: sap

Earlier port of the CAPRISE construction; symmetric coefficients on query and doc sides. Kept as a parity baseline; CAPRISE supersedes it in the bench.

method
Same scale + radial-Gaussian on the unit sphere · β/4 radius · no asymmetry
source
in-house port of the SAP / DCPE family
secures
same as CAPRISE
threat
same as CAPRISE
cost
~0 retrieval overhead
KDF

HkdfPolicyrag_core :: keying

The two-party KDF. One HKDF-Extract + two HKDF-Expands over RustCrypto's hkdf 0.12 + hmac + sha2no hand-rolled crypto at any layer.

method
derive(user_x_sk, tee_user_x_sk, tenant_id) → (caprise_seed, aes_chunk_key) · both outputs Zeroizing<[u8; 32]> · versioned info strings (gelo-rag.v1.…) · tenant_id is the HKDF salt → cross-tenant key separation
source
in-house · realises design-doc Option 3 · spec at caprise-two-party-kdf.md
secures
cross-session determinism (same inputs ⇒ same keys) so ingest & query share a key without persisting it
threat
downgrade between versions — pin the policy in attested scheme_identity
cost
µs per call · scheme_identity_digest() = sha256 of canonical policy bytes, goes into REPORT_DATA[32..64]
service

GeloRagTwoPartyServicegelo_rag :: two_party_service

Multi-tenant CAPRISE-in-TEE orchestrator. Holds no per-tenant scheme at idle. Every ingest_chunks_for / query_for derives the keys, zeroizes them at return.

method
Per-tenant tee_user_x_sk + index map. Client sends (tenant_id, user_x_sk) per request; missing tenant → TwoPartyError::UnknownTenant → HTTP 410 Gone (the "loud failure" contract).
source
in-house. Implements design-doc Option 3.
secures
forward security against TEE-only compromise · cross-tenant isolation by construction
threat
untrusted cloud operator. Breaks only under simultaneous TEE + client compromise.
cost
HKDF µs + CAPRISE µs per request. Today: in-memory tee_user_x_sk table; KMS-released and RP-escrowed persistence deferred.
ANN

InMemoryEncryptedIndexrag_core :: storage

In-memory ANN over CAPRISE ciphertexts. Cosine search runs directly over the ciphertext vectors (distance-preserving — no decryption). Top-k returns pairs of (ChunkCiphertext, EncryptedEmbedding).

method
Linear top-k by cosine — sub-ms at the BEIR scale we benchmark (3,633 docs). HNSW swap planned for > 100k docs.
source
in-house
secures
retrieval correctness over encrypted vectors; the index never decrypts
threat
same as surrounding service — untrusted storage process sees ciphertext only
cost
O(N·d) · sub-ms at N = 3.6k
at-rest cipher · shared

AES-GCM chunksrag_core :: content

AES-256-GCM authenticated encryption of document chunk text. The 32-byte key is HKDF-derived from user_x_sk ‖ tee_user_x_sk in the same call as the CAPRISE seed.

method
32-byte key (via AesChunkCipher::from_key from HKDF) · fresh 96-bit nonce per chunk · GCM authenticator
source
RustCrypto aes-gcm 0.10
secures
raw chunk text returned alongside top-k embeddings
threat
passive adversary with index access; GCM tag catches mutation
cost
negligible — bandwidth-bound; AES-NI ≫ network
§05

Compute flow & trust boundaries

FIG. 03 — GeloRAG / CAPRISE-at-rest with two-party HKDF · query path solid red = trusted secret · solid teal = ciphertext over wire · dashed red = trust boundary
Client TEE (SEV-SNP CVM) Storage server RATLS · TLS 1.3 ECDHE HTTP · CAPRISE ciphertext ① Request tenant_id · user_x_sk "What does CAPRISE do?" ⑦ Decode plaintext top-k over RATLS no client crypto in Option 3 ② Embed (GELO) → pooled unit vector ③ Two-party HKDF tee_user_x_sk[tenant] ‖ user_x_sk ④ CAPRISE encrypt query 1/8 coefficient · fresh nonce ⑥ CAPRISE-decrypt · AES-decrypt top-k inside CVM · zeroize keys at return tee_user_x_sk table per-tenant · in encrypted RAM ⑤ Encrypted ANN cosine over CAPRISE ciphertext distance-preserving no decryption ever CAPRISE rows (scheme, vec, nonce, dim) AES-GCM chunks (chunk_id, nonce, ct) text + user_x_sk encrypted query top-k ciphertexts plain top-k over RATLS SEV-SNP report
#StepWho runs itWhat crosses the boundary
Client opens RATLS, verifies attestation, sends {tenant_id, user_x_sk, text}Client → TEEplaintext text + 32B secret over RATLS
TEE embeds via GELO mask (see embedding page) → pooled unit vectorTEE (+ GPU under mask)masked activations across PCIe
TEE looks up (or creates on first contact) tee_user_x_sk[tenant]; runs HkdfPolicy::derive(user_x_sk, tee_user_x_sk, tenant_id) → (caprise_seed, aes_chunk_key)TEE
TEE CAPRISE-encrypts the query with the 1/8 coefficient and a fresh 16-byte nonceTEE
Storage server runs cosine ANN over CAPRISE ciphertext, returns top-k′ pairs (ChunkCiphertext, EncryptedEmbedding)Storage serverencrypted query inbound · encrypted hits outbound
TEE CAPRISE-decrypts the candidate embeddings (for optional rerank against the clean query); AES-decrypts the chunk texts; zeroizes caprise_seed, aes_chunk_key, and the incoming user_x_skTEE
Client gets plaintext ranked top-k over the same RATLS session — no client-side crypto needed in Option 3TEE → Clientplaintext top-k over RATLS
§07

Performance & correctness

Retrieval accuracy

ConfigurationnDCG@10top1_baseNotes
FastEmbed MiniLM-L6 plain cosine (MTEB sanity)≈ 0.301.00assertion in bench: within ±0.05 of published MTEB
CAPRISE only (no DP)matches plain≥ 0.95distance-preserving — protocol-fidelity asserted
CAPRISE + GELO/BGE-base≈ plain BGE≥ 0.95protocol-fidelity asserted
CAPRISE + GELO/Qwen3-Emb≈ plain Qwen3≥ 0.95model-vs-MiniLM gap reported separately

Two-party KDF e2e test suite

TestWhat it assertsStatus
happy_path_ingest_then_queryingest under tenant A, query later with same user_x_sk → expected chunk returnedpassing
query_re_derives_same_keys_across_sessionstwo separate ingest calls with same user_x_sk both queryablepassing
cvm_restart_drops_tenant_loud_failurefresh service after drop → UnknownTenant error fires (clients see HTTP 410)passing
fresh_tee_secret_under_same_user_sk_yields_unrecoverable_indexsame plaintext + same user_x_sk + different tee_user_x_sk → different CAPRISE ciphertexts ⇒ forward security provenpassing
cross_tenant_isolationsame user_x_sk under tenant-A and tenant-B → different ciphertexts via HKDF saltpassing
unknown_tenant_is_observable · forget_tenant_zeroes_stateintrospection + explicit lifecyclepassing
scheme_identity_is_stable_and_param_sensitivecanonical encoding deterministic and param-sensitive · attestation pin workspassing
rotate_is_stubbed_but_distinguishes_unknown_tenantstub correctly returns UnknownTenant vs NotImplementedpassing
9 / 9 passing · workspace-wide: 150 passed · 0 failed · 31 ignored

Cryptographic dependencies (no hand-rolled primitives)

PrimitiveCrateOrgStatus in workspace
HKDF (Extract + Expand)hkdf 0.12RustCryptopure-Rust
HMAC-SHA256 (HKDF backbone)hmac 0.12RustCryptoalready in workspace
SHA-256sha2 0.10RustCryptoalready in workspace
AES-256-GCMaes-gcm 0.10RustCryptoalready in workspace
Secret zeroizationzeroize 1.xRustCryptoZeroizing<T>
TLS 1.3 ECDHErustls via reqwest/rustls-tlsrustls.devalready in workspace
SEV-SNP report parsingsev 7virtee (AMD)already in workspace
§08

Status & gaps

CAPRISE-in-TEE (Option 3) delivery

MilestoneWhat landedStatusReference
M1HKDF + zeroize deps · CapriseKey::from_seed · AesChunkCipher::from_key · drop-zeroize + redacted Debuggreencore/src/caprise.rs · content.rs
M2HkdfPolicy::V1 · SchemeParams · TenantId · canonical scheme_identity_digest · 6 unit testsgreencore/src/keying.rs
M3GeloRagTwoPartyService · TwoPartyError::UnknownTenant · per-tenant tee_user_x_sk + index maps · rotate stubgreengelo-rag/src/two_party_service.rs
M4Runner swap · UserXskB64 redact-Debug newtype · /rotate stub → 501 · UnknownTenant → 410greengelo-snp-runner/src/main.rs
M5compose_scheme_identity(cfg ‖ 0x00 ‖ HkdfPolicy::digest)REPORT_DATA[32..64]greenrunner main
M6E2E test suite (9 tests above) — happy path · CVM-restart loud failure · forward security · cross-tenant separation · scheme_identity stability · rotation stubgreengelo-rag/tests/two_party_kdf_e2e.rs
M7KMS-released tee_user_x_sk persistence · survives CVM restart at same MEASUREMENTdeferredspec'd, blocked on attested-KMS spike
M8Tenant key rotation flow · decrypt-under-old · re-encrypt-under-new · atomic swapdeferredspec'd in caprise-two-party-kdf.md §5.3

Open levers (Storage layer)

  • HNSW swap in InMemoryEncryptedIndex — sub-ms search at 100k+ encrypted docs.
  • KMS-released persistence for tee_user_x_sk — disk-sealed secret released to the CVM under attestation, survives CVM restart at same MEASUREMENT.
  • Rotation flow/rotate decrypts-under-old + re-encrypts-under-new atomically.
  • Server-side ZK proof of correct ANN — V3DB-style. Open research gap; would close the integrity story for the storage side analogous to U-Verify on the GPU side.

Out of scope (explicitly)

  • Encrypted-everywhere ANN — would require FHE on the doc-side embedding. No practical scheme.
  • Cross-encoder rerank server-side — running a transformer under FHE is not practical. Client-side cross-encoder rerank on the recovered plaintext top-k is the design intent.
  • Stacking with RemoteRAG — mutually exclusive by construction. See the alternative-path page for the explored RemoteRAG implementation and why it isn't the production direction.