Shield Assistant
Technical White Paper
Shield NOYB is a browser-first AI chat application engineered around a single
architectural commitment: the server must never see the content of user queries or model
responses. The system achieves this through the mutual reinforcement of three design pillars.
First, client-side data sovereignty: all conversation history, memory embeddings, retrieval
logic, document parsing, persona management, and profile learning reside exclusively in the
user’s browser, backed by IndexedDB and an optional sqlite-vec WASM vector index. The
server-side database stores only user accounts and authentication tokens. Second, end-to-end
encrypted transport: when the user selects the secure_ehbp transport mode, all HTTP request
and response bodies between the browser and the inference enclave are encrypted using the
Encrypted HTTP Body Protocol (EHBP), which implements HPKE (RFC 9180) hybrid public key
encryption with X25519, HKDF-SHA256, and AES-256-GCM. The Phoenix backend proxy sees only
encrypted ciphertext, routing metadata, and HTTP headers. Third, verifiable trust: a
three-axis verification system combines client bundle comparison (via IPFS references),
proxy policy and forwarding claims (via Ed25519 signed or RISC Zero receipts), and enclave
identity verification (via remote attestation). The revised web checker creates its own
encrypted chat and embedding tests, binds verified proof contents to those ciphertexts,
and authenticates the returned responses locally. Its precise assurance and assumptions
are defined in Chapter 8. This document provides a comprehensive
technical analysis of each layer, the trust assumptions underlying each guarantee, the residual
risks that remain, and the controls implemented to mitigate them. It is intended to serve as a
standalone reference accessible from first principles, without requiring prior familiarity
with the codebase or its dependency ecosystem.
Large language model inference is fundamentally incompatible with encrypted input. A transformer model operates on tokenized plaintext: the attention mechanism computes dot-product similarities across token embedding vectors, and the feedforward layers apply learned weight matrices to those vectors. Every operation – tokenization, embedding lookup, self-attention, layer normalization, softmax sampling – requires access to the unencrypted semantic content of the input. The model cannot attend to ciphertext, cannot compute meaningful attention scores over encrypted token embeddings, and cannot produce coherent output from data it cannot read.
This is not a limitation of current implementations – it is a structural property of how neural language models work. Homomorphic encryption (HE) could theoretically permit computation on encrypted data, but the overhead for operations at the scale of LLM inference (billions of parameters, thousands of tokens, multiple attention heads per layer) remains orders of magnitude too costly for production use. Fully homomorphic LLM inference is an active research area, but no practical system exists today.
The consequence is stark: wherever LLM inference occurs, the user’s complete query and the model’s complete response exist in plaintext. In conventional architectures, inference runs on servers controlled by the provider. This gives the operator – along with any infrastructure compromise, insider threat, lawful intercept mechanism, or data pipeline (logging, telemetry, fine-tuning, debugging) – full access to every conversation. The attack surface is not a single access point but an accumulation of access vectors that are difficult to enumerate and impossible for the user to audit externally.
The architectural response is not to eliminate plaintext (which is impossible) but to isolate the environment where plaintext exists. Shield NOYB runs inference inside a hardware-protected enclave – a Trusted Execution Environment (TEE) whose memory is encrypted by the CPU and inaccessible to the host operating system, hypervisor, or server operator. Plaintext exists only inside the enclave boundary, and the enclave’s identity is cryptographically verified by the user’s browser before any data is transmitted.
Users have no mechanism to verify that the application code running in their browser is the audited version, that the server is following its stated privacy policy, or that the inference environment is what it claims to be. Privacy is asserted by the provider and accepted on faith. There is no cryptographic proof, no independent check, and no way for the user to detect a silent policy change, a compromised server, or a substituted inference endpoint.
Shield NOYB was designed to address both of these failure points:
Failure point 1 (plaintext at inference) is addressed by running the LLM inside a hardware-protected enclave. Plaintext necessarily exists during inference, but the enclave isolates it: even the server operator cannot observe the computation. The enclave’s identity is verified by the user’s browser via remote attestation before any data is sent.
Failure point 2 (unverifiable privacy) is addressed by a three-axis verification system. The user can independently verify: (a) that the client code matches a pinned IPFS reference, (b) the guest-checked proxy claims and the client-originated encrypted test exchanges described in Chapter 8, and (c) that the inference enclave is running the attested code via remote attestation.
By moving all user data and intelligence to the browser, encrypting all communication end-to-end between the browser and the remotely attested inference enclave, and providing cryptographic verification of each trust boundary, the system reduces the server’s role to authentication and opaque request forwarding.
Zero server-side context storage. No conversation, message, embedding, or prompt data is ever written to server-side storage, logs, or caches.
End-to-end encryption with server bypass. The Phoenix backend must be unable to read the plaintext of any user query or model response, even if the operator is curious or the server is compromised.
Client-side intelligence. All RAG pipeline operations – chunking, embedding, retrieval, prompt assembly, document parsing, persona management, and profile learning – must execute entirely in the browser.
Verifiable trust. The user must be able to verify, on demand, the integrity of the client code, the proxy policy, and the inference enclave identity, without trusting any single party.
Graceful degradation. When secure transport, WASM vector indexes, or verification backends are unavailable, the system must degrade to a functional (if less private) mode with clear user notification.
Minimal attack surface. The server should contain the minimum code, state, and dependencies needed to fulfill its role as an authentication gateway and opaque proxy.
This document covers the complete architecture of Shield NOYB as implemented in the codebase at the original time of writing (March 2026), with Chapter 8 and associated verification references revised against the September 2026 WIP implementation. This revision is a source-level specification, not an attestation of a deployed release. Other architectural chapters retain their original scope and are not a fresh whole-application audit. It addresses system topology, data flows, encryption protocols, storage architecture, client-side intelligence pipelines, verification systems, authentication, defensive controls, internationalization, mobile support, and residual risks. It references specific source code paths throughout for verifiability.
The system consists of three principals:
Browser client – A single-page JavaScript application served by Phoenix, running entirely in the user’s browser after initial page load. This is where all user data resides, all intelligence operations execute, and all encryption/decryption occurs.
Phoenix backend – An Elixir/Phoenix 1.7.15 web server responsible for user authentication, proxy token issuance, verification receipt generation, and opaque request forwarding to the upstream inference provider. It uses SQLite via Ecto and runs behind the Bandit HTTP adapter.
Tinfoil inference enclave – A remote attested execution environment operated by Tinfoil that runs the AI model and processes encrypted requests. The enclave’s identity is verifiable via remote attestation, and its HPKE public key is bound to the attested code measurements.
┌──────────────────────────────────────────────────────────────────────────┐
│ BROWSER CLIENT │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ ┌───────────┐ │
│ │ IndexedDB │ │ sqlite-vec │ │ Local Vault │ │ Document │ │
│ │ (persisted) │ │ WASM (mem) │ │ AES-256-GCM │ │ Parser │ │
│ └──────┬───────┘ └──────┬───────┘ └───────┬────────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ ┌──────▼─────────────────▼───────────────────▼─────────────────▼─────┐ │
│ │ app.js (Orchestrator) │ │
│ │ - Prompt assembly - Persona system - Profile learning │ │
│ │ - RAG pipeline - Conversation UI - Data export │ │
│ └───────────────────────────┬────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼────────────────────────────────────────┐ │
│ │ tinfoil_secure_transport.js │ │
│ │ - EHBP encryption/decryption via SecureClient │ │
│ │ - Attestation verification - HPKE key exchange │ │
│ └───────────────────────────┬────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼────────────────────────────────────────┐ │
│ │ proxy_session.js + proxy_verification.js │ │
│ │ - Token lifecycle - Receipt verification │ │
│ │ - Authenticated fetch - Bundle CID checks │ │
│ └───────────────────────────┬────────────────────────────────────────┘ │
│ │ │
└──────────────────────────────┼──────────────────────────────────────────┘
│
│ HTTPS (TLS)
│ Body: EHBP ciphertext
│ Headers: proxy token, enclave URL, EHBP key
│
┌──────────▼──────────┐
│ PHOENIX BACKEND │
│ │
│ - Validate token │
│ - Check EHBP hdr │
│ - Inject API key │
│ - Forward as-is │
│ - Stream response │
│ - Issue receipts │
│ │
│ Body: opaque bytes │
│ Cannot decrypt │
└──────────┬──────────┘
│
│ HTTPS (TLS)
│ Body: EHBP ciphertext
│ Headers: API key, EHBP key
│
┌──────────▼──────────┐
│ TINFOIL ENCLAVE │
│ │
│ - EHBP decrypts │
│ - Processes query │
│ - Generates reply │
│ - EHBP encrypts │
│ - Streams back │
│ │
└──────────┬──────────┘
│
│ Encrypted response streams back
│ through Phoenix to browser
│
┌──────────▼──────────┐
│ BROWSER │
│ │
│ SecureClient │
│ decrypts response │
│ Tokens rendered │
│ Response saved to │
│ IndexedDB locally │
└─────────────────────┘
The complete data flow for a single chat interaction in secure_ehbp mode:
memory_store.js (client-side only). chunkText() in llm_client.js: 800-character chunks with
120-character overlap. nomic-embed-text) via the encrypted transport. Fallback: local deterministic hash
embedding if remote embedding fails. sqlite-vec mode, this uses the WASM vec0 virtual table.
In indexeddb mode, it performs a brute-force cosine similarity scan. Authorization,
X-Tinfoil-Enclave-Url, Ehbp-Encapsulated-Key).
At no point in this pipeline does any plaintext user content leave the browser unencrypted
(in secure_ehbp mode) or exist in server-side storage.
| Component | Location | Language | Role |
|---|---|---|---|
app.js |
assets/js/private_assistant/app.js |
JavaScript | Main UI controller, prompt assembly, persona system, profile learning, state management |
tinfoil_secure_transport.js |
assets/js/private_assistant/tinfoil_secure_transport.js |
JavaScript | EHBP encrypted transport via Tinfoil SecureClient |
llm_client.js |
assets/js/private_assistant/llm_client.js |
JavaScript | LLM API abstraction, legacy transport, text chunking, local embedding fallback |
memory_store.js |
assets/js/private_assistant/memory_store.js |
JavaScript | IndexedDB and sqlite-vec storage for conversations, messages, and memory |
local_vault.js |
assets/js/private_assistant/local_vault.js |
JavaScript | Optional AES-GCM at-rest encryption for stored records |
proxy_session.js |
assets/js/private_assistant/proxy_session.js |
JavaScript | Proxy token lifecycle and authenticated fetch |
document_parser.js |
assets/js/private_assistant/document_parser.js |
JavaScript | Browser-side document parsing (PDF/DOCX/XLSX/PPTX), no server upload |
proxy_verification.js |
assets/js/private_assistant/proxy_verification.js |
JavaScript | Receipt verification, bundle verification, IPFS CID checks |
risc0_zk_receipt_verifier.js |
assets/js/private_assistant/risc0_zk_receipt_verifier.js |
JavaScript | Bundled RISC Zero WASM runtime for local ZK receipt verification |
i18n.js |
assets/js/private_assistant/i18n.js |
JavaScript |
Client-side i18n with 4 locales (en, ja, id, zh) using data-i18n binding |
TinfoilProxyController |
lib/private_assistant_web/controllers/tinfoil_proxy_controller.ex |
Elixir | Proxy route handlers for secure, debug, and attestation paths |
ProxyVerificationController |
lib/private_assistant_web/controllers/proxy_verification_controller.ex |
Elixir |
Receipt creation endpoint (POST /api/proxy/verify/session) |
Proxy.Client |
lib/private_assistant/proxy/client.ex |
Elixir | HTTP forwarding, header management, EHBP enforcement, streaming |
Proxy.Token |
lib/private_assistant/proxy/token.ex |
Elixir | Proxy token issuance and verification via Phoenix.Token |
Proxy.Config |
lib/private_assistant/proxy/config.ex |
Elixir | Runtime configuration for all proxy-related settings |
Proxy.Verification |
lib/private_assistant/proxy/verification.ex |
Elixir | Receipt orchestrator, dispatches to signed or ZK backend |
Proxy.VerificationPayload |
lib/private_assistant/proxy/verification_payload.ex |
Elixir | Canonical receipt payload builder |
Proxy.VerificationSigned |
lib/private_assistant/proxy/verification_signed.ex |
Elixir | Ed25519 signed receipt backend |
Proxy.VerificationZk |
lib/private_assistant/proxy/verification_zk.ex |
Elixir | ZK receipt backend adapter (calls Rust sidecar) |
Proxy.VerificationManifest |
lib/private_assistant/proxy/verification_manifest.ex |
Elixir | Manifest generation for client-side verification |
RequireProxyToken |
lib/private_assistant_web/plugs/require_proxy_token.ex |
Elixir | Plug that gates proxy endpoints behind a valid bearer token |
CacheBodyReader |
lib/private_assistant_web/cache_body_reader.ex |
Elixir | Caches raw request body for proxy forwarding |
Proxy.ForwardingChallenge |
lib/private_assistant/proxy/forwarding_challenge.ex |
Elixir | Forwarding challenge orchestrator: constructs test request, runs through forwarding path, attaches proof to session receipt |
Proxy.ForwardingChallengeZk |
lib/private_assistant/proxy/forwarding_challenge_zk.ex |
Elixir | Forwarding challenge ZK adapter (calls Rust sidecar for challenge proof generation and verification) |
Native.RuntimeNif |
lib/private_assistant/native/runtime_nif.ex |
Elixir | Fault-tolerant Elixir wrapper for the dormant Rust NIF scaffold used for verification-time challenge measurement |
Proxy.NifChallengeSigned |
lib/private_assistant/proxy/nif_challenge_signed.ex |
Elixir | Signed fallback artifact for the native recorder’s supplied challenge transcript |
Proxy.NifChallengeZk |
lib/private_assistant/proxy/nif_challenge_zk.ex |
Elixir | ZK adapter for the NIF-measured verification challenge |
pa_zk_receipt_prover |
native/pa_zk_receipt_prover/ |
Rust | Sidecar for ZK proof generation (RISC Zero zkVM): session receipts and forwarding challenges |
forwarding_challenge_guest |
native/pa_zk_receipt_prover/methods/forwarding_challenge_guest/ |
Rust | RISC Zero guest program for forwarding challenge proofs |
pa_runtime_nif |
native/pa_runtime_nif/ |
Rust | Rustler NIF scaffold for in-process verification challenge measurement |
nif_challenge_guest |
native/pa_zk_receipt_prover/methods/nif_challenge_guest/ |
Rust | RISC Zero guest for supplied NIF forwarding/no-decrypt fields; measurement limits in Section 8.10 |
browser_verifier |
native/pa_zk_receipt_prover/browser_verifier/ |
Rust | WASM verifier source for client-side ZK receipt verification |
Local-first memory. No server-side context database. All user-generated content and derived data (embeddings, chunks, profiles, personas) reside in the browser.
Controlled capabilities. The server can reject or limit proxy use via token validation, transport mode restrictions, and API key gating, but it cannot inspect the data it proxies.
Explicit safety gates. Debug mode is hard-disabled in production at boot time. Transport mode degradation is visible to the user.
Fault tolerance. The client falls back gracefully when secure transport, sqlite-vec WASM, ZK verification backends, or remote embedding services are unavailable.
Minimal server footprint. The server contains only authentication tables, no content storage, and no state beyond the request lifecycle.
Verifiable boundaries. Each trust boundary (client, proxy, enclave) can be independently verified by the user through distinct mechanisms (IPFS CID, receipts, attestation).
| Principal | Trust level | Justification |
|---|---|---|
| User’s browser | Trusted by the user | The user controls their own device and browser. The browser is the only location where plaintext user data resides. |
| Phoenix backend operator | Trusted for auth policy; untrusted for content |
The operator runs authentication and proxy routing. The system is designed so that even a curious or compromised operator cannot read query/response content when secure_ehbp is active. |
| Tinfoil enclave | Trusted for computation | The enclave processes plaintext queries after EHBP decryption. Trust is established via remote attestation. |
| Network observers | Untrusted | All traffic is protected by TLS at the transport layer and EHBP at the application layer. |
| Receipt backend | Signed-key trust or constrained ZK execution | Ed25519 authenticates a signer’s statement; RISC Zero verifies an accepted guest’s execution on committed inputs. Neither independently measures the whole proxy. The new client adds its own encrypted-exchange evidence (Chapter 8). |
Even with secure_ehbp active and functioning correctly, the Phoenix backend has visibility
into the following metadata:
/v1/chat/completions vs.
/v1/embeddings), revealing operation type. X-Tinfoil-Enclave-Url header. Reveals the enclave identity but not content. Ehbp-Encapsulated-Key header. The HPKE encapsulated key (64 hex characters for
X25519). Required for decryption at the enclave but not useful to the proxy. The system identifies three distinct trust boundaries that must each be independently verifiable for the user to have confidence in the privacy guarantee:
| Axis | Reference/evidence | Local decision |
|---|---|---|
| Client | Published bundle and checker/verifier references | Served code matches the selected trusted reference |
| Proxy check | Session and route receipts plus client-originated EHBP exchange | Proof claims bind to client ciphertexts; both responses authenticate under retained contexts |
| Enclave | Tinfoil attestation and accepted code/key binding | Encryption uses the accepted enclave’s public key |
The axes compose: neither a key check alone nor a policy receipt alone establishes the client-originated transaction assertion. Chapter 8 specifies that composition.
Axis 1: Client integrity. The browser compares a SHA-256 hash of the locally served
app.js and app.css against the hash of the same files retrieved from the IPFS gateway
using the pinned CID. A match confirms that the served code is identical to the pinned
version. A mismatch may indicate server-side tampering.
Axis 2: Proxy verification. The browser verifies session policy and forwarding claims against the selected manifest, then binds those claims to its own encrypted test bodies and authenticates the returned responses. A session policy field describes a checked statement; it is not a runtime monitor. Ed25519 session receipts and RISC Zero session receipts remain supported, while the new client requires ZK forwarding proofs for both challenge routes. The exact accepted claim is defined in Chapter 8.
Axis 3: Enclave integrity. The Tinfoil SecureClient verifies the enclave’s remote attestation document before establishing the EHBP encryption channel. This includes verifying the code measurement against the expected digest from GitHub and verifying the runtime attestation from the enclave itself.
Composed trust state. The verification panel in the UI displays all three axes. The overall badge shows “Verified Private” only when all three axes are green. Partial states are displayed with specific messages:
| State | Meaning |
|---|---|
| Verified Private | All three axes verified |
| Check Shield | Enclave OK + trust enabled, but client/proxy not yet checked |
| Client Check Needed | Enclave and proxy OK, client CID mismatch or not checked |
| Proxy Check Needed | Enclave and client OK, proxy receipt missing or expired |
| Full Check Needed | Client and proxy both need verification |
| Unverified | No verification performed or enclave attestation failed |
EHBP (Encrypted HTTP Body Protocol) encrypts HTTP message bodies end-to-end while leaving HTTP headers in the clear for routing. This allows encrypted payloads to transit proxies unchanged while maintaining all standard HTTP semantics (status codes, content negotiation, streaming).
The protocol uses HPKE (Hybrid Public Key Encryption, RFC 9180) with the following parameters:
| Parameter | Value | Specification |
|---|---|---|
| KEM | X25519_HKDF_SHA256 | DHKEM(X25519, HKDF-SHA256) |
| KDF | HKDF_SHA256 | HKDF-SHA256 |
| AEAD | AES_256_GCM | AES-256-GCM |
These parameters provide 128-bit classical security for key exchange and 256-bit key strength for symmetric encryption.
Before any data is encrypted, the browser’s SecureClient performs a four-step verification sequence:
Step 1: Fetch attestation bundle. The client requests the enclave’s attestation document
via GET /api/proxy/attestation. This request is proxied by Phoenix to the Tinfoil
attestation service at the URL configured in TINFOIL_ATTESTATION_URL (default:
https://atc.tinfoil.sh/attestation).
Step 2: Verify enclave identity. The SecureClient verifies the attestation document against the expected enclave measurements:
Step 3: Verify HPKE public key. The SecureClient fetches the enclave’s HPKE public key
from /.well-known/hpke-keys (via the proxy) and verifies it is bound to the attested
enclave. This binding ensures that the public key used for encryption belongs to the verified
enclave, not to an impostor.
Step 4: Client-side verification check. After client.ready() completes, the
application calls client.getVerificationDocument() and checks that securityVerified is
true. If any verification step failed, ensureClient() throws an error with a diagnostic
message identifying the specific failed step. No data is sent until verification passes.
For each request with a body (POST, PUT, PATCH):
Ehbp-Encapsulated-Key request
header (lowercase hex, 64 characters for X25519). Ehbp-Response-Nonce
response header (lowercase hex, 64 characters). concat(encapsulated_key, response_nonce) "ehbp response"
The Phoenix proxy receives the encrypted request, reads the full body into memory via the
CacheBodyReader, and forwards it to the enclave. During this transit:
conn.assigns[:raw_body]) is EHBP ciphertext. It consists of
length-prefixed AES-256-GCM encrypted chunks. It is not valid JSON and cannot be parsed
or read by the server. Ehbp-Encapsulated-Key header is forwarded to the enclave (it is not in the
@hop_by_hop_headers reject list). Ehbp-Response-Nonce header in the upstream response is forwarded back to the browser
via put_upstream_headers/2. content-type: application/json on the upstream request. This describes
the content type after decryption at the enclave (the enclave’s EHBP middleware handles
decryption before the application layer sees the content-type). Finch.stream/4 and forwarded via
Plug.Conn.chunk/2. Each chunk is EHBP ciphertext. The secure path enforces the presence and syntax of an EHBP encapsulated-key header on nonempty bodies. This catches missing transport setup, but does not authenticate body ciphertext or prove that a caller performed encryption:
# lib/private_assistant/proxy/client.ex
defp require_ehbp_header(_conn, mode, _body) when mode != :secure, do: :ok
defp require_ehbp_header(_conn, :secure, body) when byte_size(body) == 0, do: :ok
defp require_ehbp_header(conn, :secure, _body) do
case get_header(conn, "ehbp-encapsulated-key") do
nil ->
{:error, :missing_ehbp_header}
key ->
if valid_ehbp_header?(key) do
:ok
else
{:error, :invalid_ehbp_header}
end
end
end
defp valid_ehbp_header?(key) when is_binary(key) do
String.match?(key, ~r/\A[0-9a-fA-F]{64,}\z/)
end
This check runs after reading the body but before forwarding. If a request to the
/api/proxy/secure/* path carries a body but lacks a valid Ehbp-Encapsulated-Key header,
the proxy returns 400 missing_ehbp_encapsulated_key or 400 invalid_ehbp_encapsulated_key
and does not forward the request.
This rejects callers that omit or malform the required header. A caller could still supply a syntactically valid header alongside plaintext; this check cannot detect that case by itself. The new verification protocol instead constructs encryption in the trusted client and authenticates the response under its retained context (Chapter 8).
Bodyless requests (GET, HEAD, DELETE, OPTIONS) and non-secure modes (:debug) are exempt
from this check.
All user-generated content and derived data is stored exclusively in the browser using two complementary backends:
IndexedDB (IndexedDbMemoryStore in memory_store.js):
private_assistant_local_v1 conversations, messages, memories, kv conversations: titles, timestamps, metadata, optional vault encryption. messages: role, content, conversation association, timestamps, optional vault encryption. memories: text chunks, embedding vectors, conversation association, timestamps, optional
vault encryption. kv: user settings (proxy mode, model selections, active conversation ID, personas, user
profile).
sqlite-vec WASM (SqliteVecMemoryStore in memory_store.js):
vec0 virtual table extension for vector similarity search. SqliteVecMemoryStore delegates all persistence operations to an underlying
IndexedDbMemoryStore instance and adds a WASM-based vector index on top. cdn.jsdelivr.net/npm/sqlite-vec-wasm-demo@latest/sqlite3.mjs. Both backends are selected at runtime by the user via the Memory Backend dropdown in the UI.
| Data type | Storage location | Server sees it? | Encrypted at rest? |
|---|---|---|---|
| Conversation metadata (title, timestamps) |
IndexedDB conversations |
No | Optional (vault) |
| Message content (role, text) |
IndexedDB messages |
No | Optional (vault) |
| Memory chunks (text fragments) |
IndexedDB memories |
No | Optional (vault) |
| Embedding vectors |
IndexedDB memories |
No | Optional (vault) |
| Vector search index | sqlite-vec WASM (in-memory) | No | No (volatile) |
| User personas (custom instructions) |
IndexedDB kv (personas) |
No | No |
| User profile (learned traits) |
IndexedDB kv (user_profile) |
No | No |
| Active persona selection |
IndexedDB kv (active_persona_id) |
No | No |
| Transport mode preference |
localStorage |
No | No |
| Memory backend preference |
localStorage |
No | No |
| Vault salt |
localStorage |
No | No (salt is not secret) |
| Profile learning toggle |
localStorage (pa_profile_learning) |
No | No |
| Exported conversations | Downloaded file (user’s device) | No | No |
The LocalVault class (local_vault.js) provides optional passphrase-based encryption for
stored records using the Web Crypto API:
Key derivation:
localStorage Encryption:
Storage format:
{enc: 1, iv: <base64>, blob: <base64>} {enc: 0, data: <plaintext object>} Behavior:
Protection scope:
Users can export their conversations as a JSON file:
shield_conversations_YYYY-MM-DD.json. {
"exportedAt": "ISO-8601 timestamp",
"conversationCount": 42,
"conversations": [
{
"id": "conversation-uuid",
"title": "Conversation title",
"createdAt": "ISO-8601",
"updatedAt": "ISO-8601",
"messages": [
{
"id": "message-uuid",
"role": "user|assistant|system",
"content": "Message text",
"createdAt": "ISO-8601"
}
]
}
]
}
All intelligence operations – RAG, document parsing, persona management, and profile learning – execute entirely in the browser. The server is never involved in any of these operations beyond providing the encrypted transport channel for embedding requests.
The Retrieval-Augmented Generation pipeline runs entirely client-side, implemented across
app.js, llm_client.js, and memory_store.js:
┌───────────────────────────────────────────────────────────────────┐
│ CLIENT-SIDE RAG PIPELINE │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 1. CHUNK │───▶│ 2. EMBED │───▶│ 3. STORE │───▶│ 4. RETRIEVE│ │
│ │ │ │ │ │ │ │ │ │
│ │ chunkText│ │ Enclave │ │ IndexedDB│ │ Similarity │ │
│ │ 800 char │ │ endpoint │ │ + vec0 │ │ search │ │
│ │ 120 over │ │ (EHBP) │ │ (WASM) │ │ (top-K) │ │
│ │ │ │ fallback:│ │ │ │ │ │
│ │ │ │ local │ │ │ │ │ │
│ │ │ │ hash emb │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────┬─────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────▼──────┐ │
│ │ 5. PROMPT ASSEMBLY │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ System message: │ │ │
│ │ │ - Persona instruction (from active persona) │ │ │
│ │ │ - User profile (if profile learning enabled) │ │ │
│ │ │ - "Relevant local context:" + top-K chunks │ │ │
│ │ │ - Document text (if document attached) │ │ │
│ │ ├─────────────────────────────────────────────────────┤ │ │
│ │ │ Message history: │ │ │
│ │ │ - Last 20 messages of conversation │ │ │
│ │ ├─────────────────────────────────────────────────────┤ │ │
│ │ │ Current user message │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ EHBP encrypt and send │
│ to enclave via proxy │
└──────────────────────────────────────────────────────────────────┘
chunkText() in llm_client.js splits text into overlapping chunks:
position = i * (chunkSize - overlap) and extends chunkSize characters forward. Each chunk is sent to the enclave’s embedding endpoint for vector representation:
POST /api/proxy/secure/v1/embeddings with
the configured embedding model (default: nomic-embed-text). The request body is EHBP-
encrypted. The enclave returns an embedding vector. When the user sends a new message:
Two retrieval backends are available:
vec0 virtual table for vector similarity search.
Queries are expressed as SQL against the in-memory SQLite database. This provides indexed
search with sub-linear query time for large memory stores. memories object store. Linear in the number of stored memories. This is
the always-available fallback.
The top-K results (default K=6, configurable via DEFAULT_RETRIEVAL_TOP_K) are returned as
text chunks for prompt injection.
The final prompt sent to the enclave is assembled in app.js:
"Relevant local context:\n<chunk1>\n<chunk2>\n...".
The document_parser.js module provides client-side document parsing for multiple file
formats. The server never receives file contents.
| Format | Parser | Loading strategy |
|---|---|---|
.txt, .md, .csv, .html, .htm, .asciidoc |
Native browser APIs | No external dependency |
.pdf |
pdfjs-dist v4.9.155 | Lazy-loaded from cdn.jsdelivr.net on first use |
.docx |
mammoth v1.8.0 | Lazy-loaded from cdn.jsdelivr.net on first use |
.xlsx, .xls |
xlsx v0.18.5 | Lazy-loaded from cdn.jsdelivr.net on first use |
.pptx |
Native ZIP reader + DOMParser |
Browser built-in DecompressionStream + DOMParser for OOXML slide extraction |
Key properties:
DecompressionStream API to extract the ZIP archive and DOMParser to parse
the OOXML slide XML. Shield NOYB supports configurable personas that control the AI’s behavior and communication style:
Built-in personas (5):
| Persona | Purpose |
|---|---|
| Default | Standard balanced assistant behavior |
| Verbose | Detailed, comprehensive responses |
| Concise | Brief, direct responses |
| Friendly | Warm, conversational tone |
| ICANNOTTELLALIE | Maximum truth-seeking mode: the model is instructed to prioritize accuracy over agreeability |
Custom personas:
Storage and selection:
store.setSetting("personas", ...). active_persona_id setting in IndexedDB. Implementation: Persona instructions are injected as the first system message in prompt assembly. The active persona’s instruction text becomes the foundational behavioral directive for the model.
Shield NOYB can learn about the user’s communication patterns and preferences over time, entirely client-side:
Extraction process:
communication_style, recurring_topics, preferences,
emotional_patterns, context_notes. Storage:
user_profile setting in the kv store. version, updatedAt, extractionCount, processedConversationIds
(capped at 100 to bound storage), and traits (the extracted profile data). processedConversationIds tracks which conversations have already been analyzed to avoid
re-processing. User control:
pa_profile_learning flag in localStorage. Privacy properties:
This chapter describes the client-originated encrypted verification implemented in the Shield NOYB WIP web chat client and proxy. It distinguishes executable implementation from release state: source changes and local tests do not establish that a deployed bundle, IPFS pin, proof sidecar, or enclave is running this revision. ODP and Shieldcode also implement this protocol in their local working trees; their different execution/trust boundaries are specified in Section 8.13.
The target assertion is specific to the Shield proxy, not the inference enclave:
For the two client-created test exchanges accepted by this checker, the trusted client encrypted the request bodies to the attested enclave key before sending them to the proxy, retained the EHBP contexts locally, verified forwarding claims bound to those ciphertexts, and authenticated the responses under those contexts. Assuming the client and enclave keep their secrets confined and the cryptography holds, the proxy does not obtain the capability to decrypt those exchanges from this protocol.
The enclave intentionally decrypts inside the TEE to perform inference. This statement is a design-based confidentiality argument combined with a transaction check. It is not an observation of every proxy instruction, a proof that ciphertext was never copied, or a claim about all past and future application requests. “Unchanged bytes” alone is not the confidentiality argument; key confinement and actual client-side encryption are essential.
The intended starting point is the user’s trusted, self-checked client. Its executable bundle, verification manifest, crypto dependencies, and WASM verifier form part of that trusted computing base. IPFS comparison supplies an auditable reference, subject to the bootstrap limitations in Section 9.6; code cannot establish its own honesty merely by reporting that its self-check passed.
| Layer | What it contributes | What it does not establish alone |
|---|---|---|
| Client bundle/IPFS verification | Comparison with the selected published code reference | Authenticity of a reference chosen by an attacker, or safety before a post-load check |
| Tinfoil attestation verification | Binding of the accepted enclave identity/measurements to its HPKE public key | That a particular request used that key |
| Client EHBP encryption | A fresh encrypted request made with that verified key; sender context remains local | Network delivery or general proxy runtime behavior |
| Forwarding receipt | Guest-checked commitments to session, destination, route, body hashes, and header assertions | Independent capture of network events or enclave key confinement |
| Local response authentication | Successful decryption/authentication under the retained request context, plus route-result validation | A public signature by the enclave or proof of all server execution steps |
| Combined check | The bounded client-originated exchange and confidentiality argument above | A universal claim about other clients, other requests, or arbitrary proxy code |
The proxy may relay attestation material without being trusted to authenticate it. The
Tinfoil verifier and its configured trust anchors perform that verification locally. In
this revision, challenge creation uses getVerificationDocument() from the ready
SecureClient and requires securityVerified === true, a 32-byte hexadecimal HPKE public
key, and a matching enclaveHost. The destination must use HTTPS and contain no credentials,
query, or fragment. These checks consume the SDK’s verification result; the challenge
module does not implement an independent attestation verifier.
A valid recipient key is public: the proxy can know it too. The missing capability at the proxy is the recipient’s private key and the browser’s per-request EHBP secrets, not knowledge of the public key. The client never creates or receives the enclave private key.
The client-originated exchange uses these verification operations:
POST /api/proxy/verify/session and POST /api/proxy/verify/session/async. GET /api/proxy/verify/session/async/:run_id. /api/cli/proxy/verify/session, with CLI authentication.
The web entry point enables clientOriginVerification for its foreground checker and
background monitor. A new verification invocation creates its challenge pair before receipt
submission. The client_challenges request field selects client-originated processing;
its presence requires a valid pair of route challenges. null, an empty array, an invalid
frame, a missing route, and duplicate routes/IDs are validation errors. The session payload
validator rejects malformed submissions before async work starts. The complete request
schema and admission constraints are specified in Section 8.5.
assets/js/private_assistant/client_forwarding_challenge.js uses the installed ehbp
package (0.2.0), alongside the existing Tinfoil SDK. It creates two independent sender
contexts using Identity.fromPublicKeyHex() and encryptRequestWithContext():
| Route | Synthetic plaintext before local encryption |
|---|---|
v1/chat/completions |
Current chat model, stream: false, max_tokens: 8, one user message asking for a brief OK and containing a fresh verification token |
v1/embeddings |
Current embedding model and a short synthetic input containing a fresh verification token |
The synthetic requests contain no conversation content. Each verification token and each challenge ID is independently generated from 24 random bytes and encoded as unpadded base64url. The session nonce is generated by the existing checker. Each route has a fresh HPKE encapsulation and retained context. A challenge ID is correlation metadata outside the encrypted body; it is not itself an enclave-signed value or an echoed response token.
EHBP uses DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and AES-256-GCM. The SDK sets the request
HPKE information string to ehbp request. The request body includes EHBP’s four-byte
big-endian chunk length followed by encrypted chunk bytes. The encapsulated key is
published as Ehbp-Encapsulated-Key. Possession of that header does not reveal the shared
secret.
The client retains, in a closure, each sender context, expected ciphertext digest, and expected encapsulated-key-header digest. Only the public wire fields below are serialized. No private key, exported secret, recovery token, or plaintext test body is sent to the proxy or proof sidecar by this protocol.
The web client sends the ordinary JSON session request with one additive field:
{
"session_nonce": "<client-generated-base64url>",
"transport_mode": "secure_ehbp",
"client_policy_hash": "sha256:<hex>",
"enclave_url": "https://<verified-enclave-host>",
"conversation_id": null,
"client_challenges": [
{
"challenge_id": "<fresh-base64url-id>",
"path_family": "v1/chat/completions",
"body": "<base64url-EHBP-framed-ciphertext>",
"ehbp_encapsulated_key": "<64-lowercase-hex-characters>"
},
{
"challenge_id": "<different-fresh-base64url-id>",
"path_family": "v1/embeddings",
"body": "<base64url-EHBP-framed-ciphertext>",
"ehbp_encapsulated_key": "<64-lowercase-hex-characters>"
}
]
}
client_policy_hash = H(session_nonce || "|" || canonical_json(expected_policy)), where
H(x) is sha256: followed by lowercase hexadecimal SHA-256 of the UTF-8 bytes for a
string, or the exact byte sequence for a body. The session policy remains:
{
"client_authorization_forwarded": false,
"ehbp_required": true,
"enclave_policy": "resolved_enclave_url_v1",
"header_allowlist_policy": "strict_v1"
}
PrivateAssistant.Proxy.ClientChallenge enforces the following admission constraints:
| Field/property | Validation |
|---|---|
| Challenge array | Exactly two entries, covering exactly the chat and embeddings route families |
| Challenge IDs | Distinct; 22–64 base64url alphabet characters |
| Encapsulated-key header | Exactly 64 lowercase hexadecimal characters |
| Encoded body | Bounded base64url input, decoded without padding |
| Decoded request body | Exactly one frame: four-byte unsigned big-endian length, then exactly that many bytes |
| Ciphertext frame length | At least 16 and at most 16,384 bytes; trailing bytes and inconsistent lengths rejected |
These checks establish valid bounded framing, not valid encryption. The server lacks the recipient secret and cannot authenticate the encrypted request. Actual cryptographic construction is performed by the trusted client; the enclave processes the encrypted request.
ForwardingChallenge.create constructs chat then embeddings route metadata from the
provided entries. It does not synthesize a replacement encrypted payload when new-mode
processing fails. Client.run_forwarding_challenge forwards each decoded binary body and
the supplied encapsulated-key header through the shared secure request preparation code.
The synthetic header fixture still exercises rewriting: it contains a test authorization
value, a test cookie, a text/plain content type, the client’s EHBP key header, and the
enclave routing header. The preparation path removes the test credentials/cookie and
routing header, forces JSON content type, and injects the upstream API authorization. The
expected names for this fixture are, in sorted order, authorization, content-type, and
ehbp-encapsulated-key. The name strict_v1 in the claim is not proof that arbitrary
production headers are limited to these three; the shared implementation filters a
blocklist, and the challenge covers its particular fixture.
The inbound digest is calculated over the received decoded ciphertext. The outbound digest is calculated over the constructed Finch request’s body after converting its possible iodata representation to bytes. These are application-level observations at request preparation, not independent measurements at the network interfaces. The EHBP header hash covers the header’s UTF-8 text; the forwarded-header hash covers the sorted header names with a newline after each name. Header values, including the upstream API credential, are not part of that header-name inventory.
In new mode, the proxy collects the response with a 30-second Finch receive timeout and a 262,144-byte body cap. It requires a 2xx status, a 64-hex-character EHBP response nonce, and a body longer than 20 bytes before attaching the result. Those preliminary checks do not authenticate the response; the browser performs that operation.
The session receipt retains its existing fields and gains:
{
"forwarding_challenge": "<existing-chat-forwarding-proof-object>",
"nif_challenge_attestation": "<optional-existing-chat-NIF-proof-object>",
"route_proofs": [
{
"path_family": "v1/embeddings",
"route_id": "embeddings",
"forwarding_challenge": "<existing-embeddings-forwarding-proof-object>",
"nif_challenge_attestation": "<optional-existing-embeddings-NIF-proof-object>"
}
],
"client_challenge_responses": [
{
"challenge_id": "<chat-challenge-id>",
"path_family": "v1/chat/completions",
"status": 200,
"nonce": "<64-hex-character-EHBP-response-nonce>",
"body": "<base64url-encrypted-response>"
},
{
"challenge_id": "<embeddings-challenge-id>",
"path_family": "v1/embeddings",
"status": 200,
"nonce": "<64-hex-character-EHBP-response-nonce>",
"body": "<base64url-encrypted-response>"
}
]
}
The strings denoting proof objects above are schema placeholders, not actual wire values. Optional NIF fields may be absent or null according to their existing attachment slots. The response fields are not added to the RISC Zero guest public inputs. In particular, HTTP status, response body, and response nonce are not newly proven enclave statements.
The existing session envelope contains version, proof_type, receipt_id,
policy_hash, client_policy_hash, proxy_kernel_id, session_nonce, user_binding,
transport_mode, enclave_url_hash, issued_at, expires_at, verification, backend
metadata, and either signature or proof. The session user_binding is a server-derived
hash of the authenticated user/token/session association. It is not an independent client
measurement of server identity. Timestamps are checked locally for freshness; the default
session lifetime is 300 seconds.
The signed backend uses Ed25519 over canonical JSON bytes and a manifest-selected key. It authenticates the signer’s statement. The ZK backend executes a fixed guest and returns an artifact containing public inputs, policy, deterministic commitments, image ID, bincode-encoded receipt, and a bincode representation of the guest journal. Browser verification rejects non-RISC-Zero proof kinds for the ZK path. A signed session receipt can coexist with ZK forwarding proofs; the client-originated challenge checker requires both forwarding proofs and both authenticated responses.
| Guest statement | Assertions relevant to this revision |
|---|---|
session_policy_receipt_v1 |
Expected statement/system/kernel identity, version and declared transport, required policy values, recomputed policy/client-policy hashes, well-formed bindings and nonce, ordered timestamps, deterministic commitments |
request_forwarding_challenge_v1 |
Expected identity/version/declared transport, POST and nonempty path/challenge, equal inbound/outbound hashes, well-formed hashes and nonce, required supplied forwarding policy, exact supplied header-name list and its recomputed hash, deterministic commitments |
nif_challenge_attestation_v1 |
Corresponding forwarding assertions plus supplied zero decrypt-call count and false decrypt-capability/fallback/debug flags |
The forwarding guest receives hashes and structured claims, not the plaintext or ciphertext
body, a socket trace, an HPKE key, or an enclave response. It does not run the Phoenix
forwarding implementation. The guest checks the claimed relationship between inputs;
matching those inputs to the client’s independently held ciphertext is a separate browser
obligation. A valid proof of transport_mode == "secure_ehbp" alone is not proof of EHBP
use on a network request.
For all three statements, transcript functions compute policy_digest,
public_inputs_digest, verifier_digest, claim_digest, and proof_id. The browser
recomputes these and checks image IDs against the verification manifest. The proof image
identifies the verified guest program, not a measurement of the entire running proxy.
This revision changes neither the guest programs nor their statement names, schemas, or
image IDs; the additional evidence fits existing public inputs and local client checks.
Checking JSON metadata separately from an opaque valid receipt is insufficient: a host
could otherwise supply one attractive claim alongside a receipt for a different claim.
receipt_journal.js closes this binding at the browser verifier boundary.
The verification chain is:
client's session/challenge expectations
-> envelope checks and artifact/public-input consistency
-> commitment recomputation and manifest image selection
-> locally serialize expected journal from the checked artifact
-> require supplied artifact.zkvm_journal to equal those expected bytes
-> WASM compares decoded expected journal to the actual receipt journal
-> RISC Zero receipt.verify(expected image ID)
The expected representation is bincode 1’s fixed-width encoding of the existing Rust journal structs. It is not a new guest serialization or a replacement for RISC Zero’s internal journal encoding. WASM decodes the actual receipt journal with the RISC Zero journal decoder and compares typed structs against the bincode-decoded expected value.
Field order is security-relevant. The top level is proof_system, statement, policy,
public_inputs, then commitments. Strings use an eight-byte little-endian UTF-8 byte
length followed by bytes; booleans use one byte; string vectors use an eight-byte
little-endian element count followed by strings. There are no JSON field names or padding
in this encoding.
| Journal part | Serialized order |
|---|---|
| Session policy |
client_authorization_forwarded, ehbp_required, enclave_policy, header_allowlist_policy |
| Session public inputs |
version, policy_hash, client_policy_hash, proxy_kernel_id, session_nonce, user_binding, transport_mode, enclave_url_hash, issued_at, expires_at |
| Forwarding policy |
route_mode, header_rewrite_policy, client_authorization_forwarded, server_authorization_injected, enclave_header_forwarded, content_type_forced, forwarded_header_names |
| Forwarding public inputs |
version, proxy_kernel_id, session_nonce, user_binding, transport_mode, enclave_url_hash, challenge_id, method, path_family, inbound_body_hash, outbound_body_hash, ehbp_header_hash, forwarded_headers_hash, issued_at |
| NIF policy addition |
After forwarding policy: local_decrypt_key_loaded, decrypt_provider_enabled, kms_unwrap_enabled, legacy_plaintext_fallback_enabled, debug_proxy_enabled |
| NIF public-input addition |
decrypt_call_count immediately before issued_at; Rust i64, browser accepts only nonnegative safe integers and serializes the corresponding eight-byte little-endian value |
| Commitments |
policy_digest, public_inputs_digest, verifier_digest, claim_digest, proof_id |
Shared fixtures in assets/test/fixtures/receipt-journals.json are tested against both the
JavaScript encoder and actual Rust structs serialized by bincode. They cover all three
statement formats. The browser WASM ABI remains unchanged, including pa_verify_receipt;
no new guest image or public response-verification circuit is introduced.
After ordinary session, forwarding, optional NIF, and supplemental route proof verification, the new checker requires exactly two response entries and exactly one supplemental proof entry for embeddings. For each route it compares the verified forwarding claim with its locally retained record:
proof_type == zk_forwarding_challenge
challenge_id == client's fresh challenge ID
path_family == client's expected route
session_nonce == client's verification-session nonce
method == POST
inbound_body_hash == H(client ciphertext bytes)
outbound_body_hash == H(client ciphertext bytes)
ehbp_header_hash == H(client encapsulated-key header text)
enclave_url_hash == H(client's normalized verified enclave URL)
The general receipt verifier also checks artifact/envelope consistency, expected policy, manifest identities, session binding, and freshness. The new helper is not a standalone substitute for cryptographic receipt verification; it is called after that verification. Its associated session nonce and enclave URL must match the verification invocation.
Each reply must have exactly one matching challenge ID, the expected route, an integer 2xx status, and a 32-byte hexadecimal nonce. The browser bounds base64url input to 350,000 characters and parses every four-byte frame length. It rejects incomplete headers, truncated chunks, chunks shorter than a GCM tag, trailing incomplete data, and streams with no nonempty encrypted chunk. Zero-length frames are skipped.
EHBP response keys derive from the retained HPKE context, not from the response header alone. In the installed SDK:
S = HPKE.Export("ehbp response", 32)
salt = request_encapsulation || response_nonce
prk = HKDF-SHA256.Extract(salt, S)
key = HKDF-SHA256.Expand(prk, "key", 32)
nonce_base = HKDF-SHA256.Expand(prk, "nonce", 12)
chunk_nonce(i) = nonce_base XOR encoded_sequence_number(i)
The client calls decryptResponseWithContext() with the original request context and
fully consumes the result through response.json(). It therefore authenticates all
consumed nonempty encrypted chunks rather than accepting a nonce header or successful
HTTP status as sufficient evidence. Chat requires a nonempty choices array; embeddings
require a nonempty first embedding vector whose entries are finite numbers. A returned
error fails the check. It does not demand that the model echo the synthetic verification
token or independently validate the semantics of the model’s answer.
The response’s AEAD authentication is tied to the HPKE request context. It is not a public signature over every response-envelope field: HTTP status, route labels, and challenge IDs are additionally checked by the local protocol logic. There is no separate circuit check of the encrypted response, nor an enclave-signed transcript digest in the receipt. A saved receipt alone cannot reproduce the local authentication result without the retained context; the client also knows the response authentication secret. This check supplies assurance to the trusted client, not nonrepudiable evidence of enclave authorship for an arbitrary third party.
native/pa_runtime_nif/src/lib.rs now accepts request bodies as rustler::Binary and hashes
raw byte slices for inbound and outbound recording. EHBP ciphertext is arbitrary binary;
requiring a Rust UTF-8 String would incorrectly reject valid ciphertext. The exported NIF operations take Elixir binaries, preserving the exact ciphertext bytes
for hashing without text decoding.
The recorder’s decrypt_call_count is initialized to zero and is not an instrumented
counter of arbitrary decrypt operations. Its capability fields are emitted as fixed false
values. The NIF guest asserts those supplied values, but it does not scan the proxy process,
measure its loaded keys, or monitor every function call. Consequently this optional
attestation does not independently establish the absence of decryption capability in the
whole proxy. The new confidentiality argument is the complete client/attestation/EHBP
chain, not an inference from fixed NIF flags.
For a new client, missing proof/response entries, mismatched ciphertext or key hashes,
wrong route/session/destination, invalid RISC Zero verification, invalid framing,
authentication failure, and unsuccessful route results prevent successful verification.
An attached forwarding_challenge_status: "sidecar_unavailable" is an explicit failure
for the check. A session-only receipt cannot satisfy the client-originated exchange,
regardless of whether that session receipt is signed or proven. A configured-off forwarding
challenge similarly cannot satisfy the checker.
| Scope | Coverage supplied by this revision |
|---|---|
| Synthetic nonstreaming chat request and response | Client-created EHBP body, request forwarding proof binding, fully consumed/authenticated JSON response |
| Synthetic embeddings request and response | Same checks, using supplemental forwarding proof and embedding-result validation |
| Real chat conversations, SSE streams, tools, images, documents | No additional transaction-specific proof merely because the synthetic check succeeded |
| Other secure routes and legacy/debug traffic | No new challenge coverage |
| Proxy process, network copying, hidden storage, unrelated code | Not measured or proved by this guest |
Foreground and background verification use the same opt-in path. The background scheduler retains its existing randomized timing: exponential sampling with a configured mean of 420 seconds and clamping between 120 and 900 seconds. Each successful check makes two small real inference requests and requires forwarding proofs; it consumes upstream and prover capacity. It does not run once per ordinary model request.
The local tests exercise actual EHBP cryptography with a generated local recipient and simulated encrypted responses. They include wrong-context replay, modified body/header bindings, invalid framing, missing/duplicate/failed routes, and invalid attestation/destination preconditions. Proxy tests use controlled dispatch and sidecar responses to check exact binary forwarding, admission limits, response attachment, strict failures when required evidence is unavailable. Journal fixtures cross-check JavaScript against Rust bincode encoding and reject substituted claim/journal fields. These tests do not perform production enclave attestation or generate and verify a fresh production RISC Zero proof for the full exchange.
The source pin lists include the challenge and journal modules and the shared journal fixtures. Guest source/image IDs remain unchanged. The local tests described here do not replace a real attested enclave/proof-sidecar exchange; source correctness and evidence from a live end-to-end execution are distinct validation results.
| Responsibility | Source |
|---|---|
| Web opt-in |
assets/js/app.js |
| Foreground/background orchestration |
assets/js/private_assistant/app.js, background_monitor.js |
| Verified Tinfoil context |
assets/js/private_assistant/tinfoil_secure_transport.js |
| Encryption, expected bindings, response authentication |
assets/js/private_assistant/client_forwarding_challenge.js |
| Session/proof checking and request submission |
assets/js/private_assistant/proxy_verification.js |
| Expected journal construction |
assets/js/private_assistant/receipt_journal.js |
| JS/WASM verification boundary |
assets/js/private_assistant/risc0_zk_receipt_verifier.js, native/pa_zk_receipt_prover/browser_verifier/src/lib.rs |
| Request admission and session payload |
lib/private_assistant/proxy/client_challenge.ex, verification_payload.ex |
| Challenge dispatch and proof attachment |
lib/private_assistant/proxy/forwarding_challenge.ex, client.ex |
| Native binary recorder |
native/pa_runtime_nif/src/lib.rs |
| Unchanged guest constraints |
native/pa_zk_receipt_prover/methods/guest/src/main.rs, methods/forwarding_challenge_guest/src/main.rs, methods/nif_challenge_guest/src/main.rs |
| Focused checks |
assets/test/, test/private_assistant/proxy/client_challenge_test.exs, test/private_assistant/proxy/forwarding_challenge_zk_test.exs |
For runnable local checks, see Client-originated encrypted verification.
The ODP and Shieldcode working-tree implementations use the same two-route extension and existing async verification API. They add no new Shield proxy endpoint. Their execution boundaries differ from ShieldAssistant web chat:
| Consumer | Trusted checker execution and source | How results reach the UI |
|---|---|---|
| ODP |
ODP server process: src/lib/server/shield-trust.ts and src/lib/server/shield-verify/ |
The browser displays the ODP server’s verdict; the ODP server is the Shield protocol client |
| Shieldcode CLI/TUI and normal web/desktop flow |
Local OpenCode runtime: packages/opencode/src/provider/shield/verify.ts, using packages/shield-verify/ |
The app obtains the runtime’s verdict through the existing local /shield status interface |
| Shieldcode exported browser helper |
packages/app/src/utils/shield-verify.ts, through the same shared verification package |
runShieldVerify can perform browser-side verification when invoked; the normal UI continues to use the local runtime verdict |
ODP isolates memoized runs by configured proxy URL and credential and does not switch to another proxy when verification fails. Both runtime orchestrators retain advisory behavior: a failure yields a failed/unavailable trust result rather than blocking ordinary model calls. Consequently successful checker execution is not a new transport-enforcement gate. Their existing WASM digest self-check covers the verifier artifact, not the complete application runtime. In particular, ODP’s server-side checker does not become a browser/IPFS trust boundary merely by implementing the same challenge protocol.
Because these checkers did not already have the web client’s ready SecureClient context,
they now retrieve a public attestation bundle directly from
https://atc.tinfoil.sh/attestation and verify it locally with
@tinfoilsh/verifier 1.1.3. Verification is configured for the fixed repository
tinfoilsh/confidential-model-router. A configured enclave requests a bundle with a POST
containing enclaveUrl and repo; otherwise the checker retrieves the default bundle.
It requires successful bundle verification, a valid bound HPKE public key, and an enclave
host matching the encryption destination. The public attestation request carries no Shield
proxy authorization credential. Direct retrieval here is only for attestation: encrypted
ghost requests and proof polling still use the user’s configured Shield proxy.
Both runtimes send the public challenge wire data to
POST /api/cli/proxy/verify/session/async under the configured proxy origin and poll its
run-specific sibling with the existing Shield credential. Polling is bounded, redirects
are rejected, and failure cannot produce a successful verification verdict. The
Shieldcode browser helper similarly selects the async sibling of its supplied verification
URL. SHIELD_ENCLAVE_URL can select the enclave; the runtime checkers accept
SHIELD_VERIFY_CHAT_MODEL and SHIELD_VERIFY_EMBEDDING_MODEL, with defaults
gpt-oss-120b and nomic-embed-text. These are synthetic-check model choices and do not
assert that an ordinary selected model uses the same route or encryption.
Their verification cores reconstruct the expected bincode journal, verify the accepted
session/forwarding proof artifacts, verify the supplemental embeddings proof, bind both
route user_binding values to the session receipt, compare the client-created ciphertext
and key-header commitments, and authenticate both responses under locally retained EHBP
contexts. The existing optional NIF attestation remains outside these two clients’ verified
NIF coverage; they report its presence as deferred, rather than treating it as an additional
verified no-decryption assertion.
The migrations include focused cryptographic, journal-binding, route, and failure-path tests and application/package type checks. They do not change ordinary conversation/model transport, establish end-to-end encryption for all ODP or Shieldcode conversations, or prove that all model traffic follows the tested path. Real ATC/enclave/prover integration is outside the local test evidence described here. Section 8.1’s confidentiality argument applies to these test exchanges with “trusted client” interpreted as the runtime identified in the table above.
The standard web application trust model requires the user to trust the server to serve honest JavaScript on every page load. If the server operator or an attacker with server access modifies the served bundle, the client has no inherent mechanism to detect the tampering. This is the single largest trust assumption in the system, because a compromised bundle could exfiltrate all browser-side data.
IPFS bundle pinning provides a partial mitigation by creating an immutable, content-addressed reference for the audited client code. The browser can compare the served code against this reference to detect modifications.
The system implements four pinning flows. The first two pin compiled artifacts that the browser verifies at runtime. The second two pin source code so that third-party auditors can reproduce the compiled artifacts and verify kernel IDs.
Flow 1: Client bundle (compiled artifact)
Pins the executable client code:
assets/app.js + assets/app.css scripts/pin_client_bundle.sh priv/static/bundle-cid.json PUBLIC_BUNDLE_CID, PUBLIC_BUNDLE_GATEWAY Flow 2: Proxy verification assets (compiled artifact)
Pins the browser-side verifier assets used for receipt validation:
assets/risc0_zk_receipt_verifier.wasm + verification-manifest.json scripts/pin_proxy_verification_bundle.sh priv/static/proxy-verification-bundle.json PUBLIC_PROXY_VERIFICATION_BUNDLE_CID,
PUBLIC_PROXY_VERIFICATION_BUNDLE_GATEWAY Flow 3: Rust ZK source bundle (auditor reference)
Pins the source code needed to reproduce the ZK guest program kernel IDs and the browser WASM verifier binary. This includes the three RISC Zero guest programs (session receipt, forwarding challenge, NIF challenge), the shared Rust modules they reference, the browser WASM verifier source, all Cargo.toml and Cargo.lock files for reproducible builds, and the WASM build script:
assets/test/fixtures/receipt-journals.json; the authoritative list is in the pin script scripts/pin_zk_source_bundle.sh priv/static/zk-source-bundle.json PUBLIC_ZK_SOURCE_BUNDLE_CID, PUBLIC_ZK_SOURCE_BUNDLE_GATEWAY
An auditor can fetch this bundle by CID, compile the guest programs with the specified Rust
and RISC Zero toolchain versions, and confirm the resulting kernel IDs match those in the
verification-manifest.json. Similarly, compiling the browser verifier to WASM and hashing
the output should reproduce the WASM artifact CID from Flow 2.
Flow 4: JavaScript source bundle (auditor convenience)
Pins the pre-compilation JavaScript source files for the client-side verification logic,
along with package.json and package-lock.json for build reproducibility:
client_forwarding_challenge.js, receipt_journal.js, and the package/lock files scripts/pin_js_source_bundle.sh priv/static/js-source-bundle.json PUBLIC_JS_SOURCE_BUNDLE_CID, PUBLIC_JS_SOURCE_BUNDLE_GATEWAY
This is a convenience pin. The compiled app.js CID from Flow 1 is the runtime trust
anchor. The source pin lets auditors read the human-readable verification logic rather than
reverse-engineering the minified bundle.
Combined pinning:
scripts/pin_all_verification_bundles.sh runs all flows in sequence.
The scripts use Pinata’s pinFileToIPFS endpoint with directory-style multipart upload:
PINATA_JWT (preferred) or PINATA_API_KEY + PINATA_API_SECRET. PINATA_GATEWAY. .env backup before updating environment
variables. The browser performs a plain hash comparison:
app.js and app.css from the current page. This check does not use zero-knowledge proofs. It is a direct content comparison.
priv/static/bundle-cid.json:
{
"version": "1",
"cid": "bafy...",
"created_at": "2026-03-07T16:50:00Z",
"gateway_base_url": "https://example-gateway.mypinata.cloud",
"bundle_url": "https://example-gateway.mypinata.cloud/ipfs/bafy...",
"files": ["assets/app.css", "assets/app.js"]
}
priv/static/proxy-verification-bundle.json:
Same schema, listing the verification asset files.
bundle-cid.json. Users authenticate via one of two methods:
/auth/google), configured with the openid email profile
scope. This is the primary authentication method. mix phx.gen.auth.
Authenticated sessions are stored in a signed cookie (_private_assistant_key) using
Plug.Session with :cookie store and a signing salt. The cookie is signed (tamper-proof)
but not encrypted (readable by the client).
The proxy token is a short-lived credential that authorizes the browser to make proxy requests:
POST /api/proxy/token with the session cookie and CSRF token. Phoenix.Token signed with the salt "proxy_token_v1", containing
the user ID and issuance timestamp. PROXY_TOKEN_TTL_SECONDS. ProxySession.cachedToken) and refreshes it 15
seconds before expiry. Authorization: Bearer <proxy_token>. RequireProxyToken plug verifies the token signature, checks expiry, and looks up
the user.
The proxy token is never stored in localStorage or IndexedDB. It exists only in
JavaScript memory and is lost on page refresh (requiring a new token from the session).
The Tinfoil API key (TINFOIL_API_KEY) is stored only in the server’s environment variables
and is never sent to the browser. When forwarding a request upstream:
Authorization header (it is in the @hop_by_hop_headers
reject list). Authorization: Bearer <TINFOIL_API_KEY> header. This creates a credential boundary:
The Phoenix backend uses SQLite via Ecto. The only tables are those generated by
mix phx.gen.auth:
users – email, hashed_password, confirmed_at users_tokens – token (binary), context, sent_to, user_id, inserted_at
There are no tables for conversations, messages, memories, embeddings, personas, profiles, or
any other content-related data. The Ecto migration
(20260304080143_create_users_auth_tables.exs) creates only the authentication tables.
The Phoenix application tree (Application.start/2) starts the following supervised
processes:
| Process | Purpose | Stores user content? |
|---|---|---|
Telemetry |
Metrics collection (request timing, VM stats) | No |
Repo |
Ecto database connection pool | No (auth tables only) |
Ecto.Migrator |
Runs pending migrations at startup | No |
DNSCluster |
DNS-based clustering (multi-node) | No |
Phoenix.PubSub |
Pub/sub for LiveView | No |
Finch |
HTTP client pool for upstream requests | No (transient connections) |
Endpoint |
HTTP server (Bandit adapter) | No |
None of these processes store or cache user content. The Finch HTTP client pool handles
upstream connections but does not retain request or response bodies after streaming completes.
The proxy client explicitly avoids logging request or response content in secure mode:
log_plaintext option defaults to false for all modes except :debug. maybe_log_plaintext_request/3 function is a no-op when log_plaintext is false. "Secure proxy failed: :timeout"), never body content. :info, suppressing debug-level messages.
The CacheBodyReader at the endpoint level reads and caches the full request body in
conn.assigns[:raw_body] for all requests. This is necessary because Plug.Parsers consumes
the body, and the proxy needs the raw bytes for forwarding.
For secure_ehbp requests:
Plug.Parsers is configured with pass: ["*/*"], so EHBP-encrypted bodies pass through
without being decoded into conn.params.
The legacy_json transport mode and the /api/proxy/debug/* path allow plaintext proxy
requests for development. The following controls prevent misuse:
Production hard-fail. runtime.exs raises an exception at boot if
PLAINTEXT_DEBUG_MODE is enabled in production:
if config_env() == :prod and plaintext_debug_mode do
raise "PLAINTEXT_DEBUG_MODE cannot be enabled in production."
end
Origin allowlist. Debug requests must include an Origin header matching the
configured allowlist (default: http://localhost:4000, http://127.0.0.1:4000).
UI lockout. When debug mode is disabled server-side, the debug option in the Proxy Mode dropdown is visually disabled.
The proxy client strips the following headers before forwarding upstream:
authorization, connection, content-length, cookie, host,
keep-alive, proxy-authenticate, proxy-authorization, te, trailer,
transfer-encoding, upgrade. x-tinfoil-enclave-url (used for routing but not forwarded to the
enclave).
The proxy then injects its own Authorization: Bearer <TINFOIL_API_KEY> header. This
ensures:
The X-Tinfoil-Enclave-Url header is validated before use:
defp valid_https_url?(url) do
case URI.parse(url) do
%URI{scheme: "https", host: host}
when is_binary(host) and byte_size(host) > 0 -> true
_ -> false
end
end
Non-HTTPS URLs are rejected with 400 invalid_x_tinfoil_enclave_url. This prevents SSRF
attacks where a malicious client could direct the proxy to forward requests to an arbitrary
HTTP endpoint.
When secure_ehbp is selected but the SecureClient fails to initialize (network error,
enclave verification failure, missing browser APIs), the client falls back to legacy_json
transport with a visible status message:
setStatus(`Secure transport unavailable: ${error.message}. Using legacy transport.`)
This fallback is intentional for development environments. In legacy_json mode, request
bodies are plaintext JSON, and the proxy can read them. The status bar informs the user.
For production hardening, this fallback could be replaced with fail-closed behavior where the client refuses to send any data if secure transport is unavailable.
The client-side i18n.js module provides a lightweight internationalization system:
Supported locales: en (English), ja (Japanese), id (Indonesian), zh (Chinese).
Binding mechanism: HTML elements use data-i18n attributes to specify translation keys.
The i18n module scans the DOM for these attributes and replaces the text content with the
translated string for the active locale.
Scope:
Locale selection: The active locale is stored as a user setting and persists across sessions.
Relationship to privacy: Internationalization is a UI-only concern. Translation tables
are bundled with the client code. No server calls are made for translation. The selected
locale is stored in localStorage (key: pa_locale) and is not sent to the server.
Shield NOYB uses Capacitor 8.2 to wrap the web application for mobile platforms:
The mobile wrappers preserve all the privacy properties of the web application:
| # | Risk | Impact | Mitigation Status | Details |
|---|---|---|---|---|
| 1 | Compromised server operator | Server operator modifies served JS to exfiltrate browser data | Partially mitigated | IPFS bundle CID check provides detection (Section 9). However, the check is post-load (detective, not preventive). Full mitigation would require SRI, browser extension verification, or static deployment to content-addressed storage. |
| 2 | Browser compromise | Malware, malicious extension, or compromised browser process accesses IndexedDB, JS memory, and Web Crypto key material | Partially mitigated | Local Vault encrypts records at rest (protects against offline extraction). Does not protect against active browser process compromise. |
| 3 | Enclave compromise | Vulnerability in enclave runtime or supply chain attack on enclave software gives attacker access to plaintext queries and responses | Mitigated by attestation | Browser verifies code measurements and runtime attestation before sending data. Does not protect against vulnerabilities in the attested code itself. |
| 4 | Traffic analysis | Network observer infers usage patterns, approximate query length from encrypted body sizes and request timing | Not mitigated | EHBP encrypts bodies but does not pad to fixed size or add timing jitter. Standard TLS traffic analysis techniques apply. |
| 5 | sqlite-vec CDN loading |
sqlite-vec WASM loaded from cdn.jsdelivr.net with @latest tag and no integrity verification; compromised CDN could serve arbitrary code |
Not mitigated (contained) | Module operates on local data only, does not handle encryption or network communication. Cannot bypass EHBP or exfiltrate data through the proxy without separately compromising the Tinfoil SDK or proxy token. Fallback to brute-force cosine similarity preserves function. |
| 6 | Document parser CDN loading |
pdfjs-dist, mammoth, xlsx loaded from cdn.jsdelivr.net on first use; no integrity verification |
Not mitigated (contained) | Libraries operate on local data only, performing no network or cryptographic operations. Cannot exfiltrate data through the proxy without a separate compromise. Impact limited to local document parsing corruption. |
| 7 | Session cookie scope | Session cookie is signed but not encrypted; cookie contents can be read by interceptors | Low risk |
Session contains only the user token reference, not sensitive content. Setting encryption_salt would add defense in depth. |
| 8 | IPFS gateway compromise | Compromised gateway serves modified content matching a tampered bundle, defeating CID check | Not mitigated | The CID check depends on the gateway serving authentic content. Multiple gateways could be checked for redundancy. |
| 9 | Receipt replay | A captured receipt could be replayed to a different user’s verification panel | Mitigated |
Receipts are bound to session_nonce (client-generated) and user_binding (server-derived from user ID + proxy token + nonce). Short TTL (5 min) limits the window. |
| 10 | ZK sidecar supplies misleading inputs | Valid proofs may concern host-supplied claims rather than independently measured events | Bounded by client evidence | Expected-journal reconstruction binds checked fields to the actual receipt and accepted image. Client ciphertext/context checks add independent transaction evidence. Proof soundness does not turn host assertions into measurements. RISC Zero proof generation has no signing private key whose compromise alone defeats the proof. |
| 11 | Profile learning leakage | Accumulated user profile data represents a privacy-sensitive summary of communication patterns | Mitigated by design |
Profile stored only in IndexedDB (never server-side). User can disable profile learning via pa_profile_learning toggle. User can view and download their profile. Profile is a summary, not a transcript. |
| 12 | Forwarding challenge coverage | Two synthetic route exchanges cannot establish all traffic or proxy execution behavior | Explicitly scoped | New-mode chat and embeddings requests use real client EHBP contexts and authenticated responses. The forwarding guest proves constraints on supplied request claims. Other request shapes, SSE streaming, and proxy side effects remain outside this test (Section 8.11). |
| # | Guarantee | Confidence | Basis |
|---|---|---|---|
| 1 | Query plaintext is not visible to the Phoenix server | High | EHBP encrypts the body before it leaves the browser. The server holds only ciphertext. This relies on actual client encryption and key confinement; header presence alone does not establish encryption. |
| 2 | Response plaintext is not visible to the Phoenix server | High | EHBP encrypts the response at the enclave. The server streams ciphertext chunks without decryption capability. |
| 3 | Conversation history is not stored server-side | Definitive | No server-side schema, table, or process exists for conversation or message storage. All storage is IndexedDB in the browser. |
| 4 | Embedding vectors are not stored server-side | Definitive | Embeddings are stored in IndexedDB and the in-memory WASM sqlite-vec index, both in the browser. No server-side embedding storage exists. |
| 5 | Context retrieval runs entirely client-side | Definitive | Similarity search, top-K selection, and prompt assembly are JavaScript functions in the browser. No server endpoint is involved in RAG operations. |
| 6 | Document parsing runs entirely client-side | Definitive |
document_parser.js reads files via the File API and parses them in JavaScript. File bytes never reach the server. |
| 7 | Persona and profile data are not stored server-side | Definitive |
Personas and user profiles are stored in IndexedDB kv store. No server-side persona or profile tables exist. |
| 8 | The upstream API key is not exposed to the browser | Definitive |
The proxy strips the incoming Authorization header and injects the server-side API key. The browser never receives or sends the Tinfoil API key. |
| 9 | The per-user proxy token is not exposed to the enclave | Definitive |
The proxy strips the incoming Authorization header before forwarding. The enclave sees only the server-level API key. |
| 10 | The Tinfoil SDK is bundled locally, not loaded from a CDN | Definitive |
The tinfoil npm package (v1.1.3) is installed, bundled by esbuild, and served from static assets. No runtime external fetch for the encryption library. |
| 11 | Enclave identity is verified before data is sent | High |
The SecureClient verifies the attestation document and HPKE public key. The application checks getVerificationDocument().securityVerified and refuses to proceed if verification fails. |
| 12 | Proxy policy claims can be verified by the user | Scoped | Signed receipts authenticate a statement; RISC Zero receipts prove the accepted guest checked its committed inputs. Neither alone measures arbitrary live proxy behavior. Expected-journal reconstruction binds the checked claims to the proof. |
| 13 | Client bundle integrity can be checked by the user | Moderate | IPFS CID comparison detects modifications to the served bundle. The check is detective (post-load), not preventive. |
| 14 | Client-originated encrypted test exchanges can be verified | Scoped | The trusted web client checks two ciphertext-bound forwarding proofs and authenticates two responses under retained EHBP contexts. Under the attestation/key-confinement assumptions, the proxy lacks their decryption secrets. This is not a proof of all proxy instructions or all traffic. |
For these guarantees to hold, the following assumptions must be satisfied:
The user’s browser and device are not compromised. A compromised browser can access all in-memory and IndexedDB data regardless of EHBP encryption.
The Tinfoil enclave runtime is executing the attested code faithfully. Remote attestation verifies code measurements, but does not protect against vulnerabilities in the attested code itself.
The server is serving the unmodified JavaScript bundle. This is the standard web trust model. IPFS CID checking provides detective (not preventive) assurance.
The cryptographic primitives are implemented correctly. HPKE, AES-256-GCM, PBKDF2 (Web Crypto API), Ed25519 (Erlang :crypto), and RISC Zero (Rust zkVM) are assumed to be correctly implemented.
The verification manifest is part of the audited client release. The manifest’s integrity must be established with the separately pinned verification assets as well as the client code.
EHBP secrets remain confined to the trusted client and attested enclave. The verifier must trust the attestation roots, accepted code, key binding, and their implementations. The proxy does not receive private keys, sender contexts, or recovery tokens through the challenge protocol. Neither a policy flag nor the ZK guest proves this confinement independently.
| Level | Definition |
|---|---|
| Definitive | The guarantee follows directly from the architecture. No code path exists that could violate it under any configuration or operating condition. |
| High |
The guarantee holds under normal operating conditions with secure_ehbp active. It depends on cryptographic assumptions and correct protocol implementation. |
| Moderate | The guarantee provides meaningful assurance but has known limitations (e.g., detective vs. preventive, dependency on external services). |
| Scoped | The exact bounded assertion and assumptions in Chapter 8 apply; this is not an application-wide or universal guarantee. |
| Variable | Required | Default | Description |
|---|---|---|---|
TINFOIL_BASE_URL |
No |
https://inference.tinfoil.sh |
Debug proxy target (OpenAI-style API base) |
TINFOIL_ATTESTATION_URL |
No |
https://atc.tinfoil.sh/attestation |
Attestation proxy route target |
TINFOIL_API_KEY |
Yes (for proxy) | None | Upstream API key, injected by proxy |
TINFOIL_ENCLAVE_URL |
No | Unset |
Secure proxy fallback (used if request omits X-Tinfoil-Enclave-Url) |
| Variable | Required | Default | Description |
|---|---|---|---|
GOOGLE_CLIENT_ID |
Yes | None | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Yes | None | Google OAuth client secret |
SECRET_KEY_BASE |
Prod only | None | Phoenix endpoint signing secret |
| Variable | Required | Default | Description |
|---|---|---|---|
PROXY_TOKEN_TTL_SECONDS |
No |
300 |
Proxy token validity window in seconds |
| Variable | Required | Default | Description |
|---|---|---|---|
PROXY_VERIFICATION_BACKEND |
No |
signed |
Receipt backend: signed or zk |
PROXY_VERIFICATION_PRIVATE_SEED |
No | None | Ed25519 private seed (32 bytes, base64url). Required for signed receipts |
PROXY_VERIFICATION_KEY_ID |
No |
proxy-verify-key-2026-03 |
Key identifier for signed receipts |
PROXY_VERIFICATION_KERNEL_ID |
No |
proxy-kernel-v1 |
Proxy kernel identifier for signed receipts |
PROXY_VERIFICATION_TTL_SECONDS |
No |
300 |
Receipt validity window in seconds |
PROXY_VERIFICATION_INSTANCE_ID |
No |
HOSTNAME or "local" |
Backend instance identifier for audit |
PROXY_VERIFICATION_ZK_SYSTEM |
No |
risc0 |
ZK proof system identifier |
PROXY_VERIFICATION_ZK_URL |
No |
http://127.0.0.1:8091 |
ZK sidecar HTTP endpoint |
PROXY_VERIFICATION_ZK_KERNEL_ID |
No |
risc0:image:placeholder |
RISC Zero image ID for ZK session receipts |
PROXY_VERIFICATION_FORWARDING_CHALLENGE_ZK_KERNEL_ID |
No |
risc0:image:placeholder |
RISC Zero image ID for ZK forwarding challenge proofs (falls back to PROXY_VERIFICATION_ZK_KERNEL_ID if unset) |
PROXY_VERIFICATION_NIF_CHALLENGE_ZK_KERNEL_ID |
No |
risc0:image:placeholder |
RISC Zero image ID for ZK NIF challenge proofs (falls back to PROXY_VERIFICATION_ZK_KERNEL_ID if unset) |
PROXY_VERIFICATION_FORWARDING_CHALLENGE_ENABLED |
No |
true |
Enable forwarding challenge proof attached to session receipts |
PROXY_VERIFICATION_NIF_CHALLENGE_ENABLED |
No |
false |
Enable verification-time NIF challenge measurement and attachment |
| Variable | Required | Default | Description |
|---|---|---|---|
PUBLIC_BUNDLE_CID |
No | None | IPFS CID of the pinned client bundle |
PUBLIC_BUNDLE_GATEWAY |
No | None | IPFS gateway base URL for client bundle |
PUBLIC_PROXY_VERIFICATION_BUNDLE_CID |
No | None | IPFS CID of the pinned verification assets |
PUBLIC_PROXY_VERIFICATION_BUNDLE_GATEWAY |
No | None | IPFS gateway base URL for verification assets |
PUBLIC_ZK_SOURCE_BUNDLE_CID |
No | None | IPFS CID of the pinned Rust ZK source bundle |
PUBLIC_ZK_SOURCE_BUNDLE_GATEWAY |
No | None | IPFS gateway base URL for ZK source bundle |
PUBLIC_JS_SOURCE_BUNDLE_CID |
No | None | IPFS CID of the pinned JavaScript source bundle |
PUBLIC_JS_SOURCE_BUNDLE_GATEWAY |
No | None | IPFS gateway base URL for JS source bundle |
PINATA_JWT |
No | None | Pinata JWT for IPFS pinning |
PINATA_API_KEY |
No | None | Pinata API key (fallback auth) |
PINATA_API_SECRET |
No | None | Pinata API secret (fallback auth) |
PINATA_GATEWAY |
No | None | Dedicated Pinata gateway domain |
| Variable | Required | Default | Description |
|---|---|---|---|
PLAINTEXT_DEBUG_MODE |
No |
true (dev), false (prod) |
Enable debug proxy path. Production boot fails if true |
PLAINTEXT_DEBUG_ALLOWED_ORIGINS |
No |
http://localhost:4000,http://127.0.0.1:4000 |
Comma-separated origin allowlist for debug requests |
| Variable | Required | Default | Description |
|---|---|---|---|
DEFAULT_CHAT_MODEL |
No |
gpt-oss-120b |
Default chat model for UI |
DEFAULT_EMBEDDING_MODEL |
No |
nomic-embed-text |
Default embedding model for UI |
DEFAULT_RETRIEVAL_TOP_K |
No |
6 |
Number of memory chunks to retrieve |
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_PATH |
Prod only | None | SQLite database file path |
PHX_HOST |
Prod only |
example.com |
Public hostname |
PORT |
Prod only |
4000 |
HTTP listener port |
PHX_SERVER |
Release mode | Unset |
Set true for release boot |
| Variable | Required | Default | Description |
|---|---|---|---|
CARGO_BIN |
No |
cargo or rustup run stable cargo |
Cargo binary for WASM verifier build |
RUSTUP_BIN |
No |
$HOME/.cargo/bin/rustup |
Rustup binary for stable toolchain |
config/runtime.exs Proxy.Config (@defaults map)
Proxy.Config reads from Application.get_env(:private_assistant, :proxy, []) and falls
back to @defaults only if the key is absent.
POST /api/proxy/token
| Field | Value |
|---|---|
| Auth |
Browser session (require_authenticated_user) |
| Request body | None |
| Success |
201 with {"token": "<proxy_token>", "expires_at": "ISO-8601", "ttl_seconds": 300} |
| Failure | Unauthenticated requests redirect to login flow |
GET /api/proxy/attestation
| Field | Value |
|---|---|
| Auth |
Authorization: Bearer <proxy_token> |
| Behavior |
Proxied GET to TINFOIL_ATTESTATION_URL with server-side API key |
| Success |
200 passthrough from upstream |
| Errors |
401 invalid token, 503 missing API key, 502 proxy failed |
* /api/proxy/secure/*path
| Field | Value |
|---|---|
| Auth |
Authorization: Bearer <proxy_token> |
| Methods |
All HTTP methods (match :*) |
| Required header |
X-Tinfoil-Enclave-Url: https://... (unless server fallback configured) |
| EHBP enforcement |
Request body with missing/invalid Ehbp-Encapsulated-Key returns 400 |
| Target URL |
Enclave URL + /*path |
| Success | Upstream status/body streamed back (supports chunked/SSE) |
| Errors |
401 invalid token, 400 missing/invalid enclave URL, 400 missing/invalid EHBP header, 503 missing API key, 502 proxy failed |
* /api/proxy/debug/*path
| Field | Value |
|---|---|
| Auth |
Authorization: Bearer <proxy_token> |
| Safety controls |
PLAINTEXT_DEBUG_MODE must be enabled; request Origin must be in allowlist |
| Target URL |
TINFOIL_BASE_URL + /*path |
| Success | Upstream status/body streamed back |
| Errors |
401 invalid token, 403 debug disabled or origin not allowed, 503 missing API key, 502 proxy failed |
| Operation | Web route | Result |
|---|---|---|
| Synchronous creation |
POST /api/proxy/verify/session |
200 with {"receipt": {...}} |
| Asynchronous creation |
POST /api/proxy/verify/session/async |
202 with run_id, status: "pending", and poll_after_ms |
| Poll owned run |
GET /api/proxy/verify/session/async/:run_id |
Pending/running status; status: "done" with receipt; or status: "error" with reason; unknown run returns 404 |
The existing proxy authentication applies. CLI equivalents use /api/cli/proxy and CLI
authentication. The required session fields include session_nonce, transport_mode,
client_policy_hash, and enclave_url; conversation_id is optional. Chapter 8.5 specifies
the additive client_challenges field. The client-originated field requires strict two-route validation. Invalid input returns
a validation error (web 400).
The response extension is specified in Chapter 8.6. This router does not expose a
GET /verify/session/:receipt_id lookup route.
POST /prove/forwarding-challenge (on the ZK sidecar, default port 8091)
| Field | Value |
|---|---|
| Request body |
{"payload": {<canonical forwarding challenge payload>}} |
| Payload fields |
version, proxy_kernel_id, session_nonce, user_binding, transport_mode, enclave_url_hash, challenge_id, method, path_family, inbound_body_hash, outbound_body_hash, ehbp_header_hash, forwarded_headers_hash, issued_at, verification |
| Success |
200 with {"proof_type": "zk_forwarding_challenge", "proxy_kernel_id": "risc0:image:<hex>", "backend": {...}, "proof": {"system": "risc0", "receipt": "pa_zk_forwarding_challenge_v1:<hex>", "public_inputs": {...}}} |
| Errors |
400 invalid payload fields, 400 unsupported transport mode, 400 policy mismatch, 500 proof generation failure |
POST /verify/forwarding-challenge (on the ZK sidecar, default port 8091)
| Field | Value |
|---|---|
| Request body |
{"receipt": "pa_zk_forwarding_challenge_v1:<hex>", "expected_kernel_id": "risc0:image:<hex>"} |
| Success |
200 with {"verified": true} |
| Errors |
400 invalid receipt artifact, 400 kernel ID mismatch, 400 journal mismatch, 500 verification failure |
| Direction | Header | Behavior |
|---|---|---|
| Incoming |
Authorization (proxy token) |
Validated by plug, stripped before forwarding |
| Outgoing |
Authorization (API key) |
Injected by proxy with TINFOIL_API_KEY |
| Incoming |
X-Tinfoil-Enclave-Url |
Validated (HTTPS), used for routing, not forwarded |
| Incoming |
Ehbp-Encapsulated-Key |
Required on secure path with body, forwarded to enclave |
| Upstream response |
Ehbp-Response-Nonce |
Forwarded to browser |
| Incoming | All hop-by-hop headers | Stripped before forwarding |
| Incoming |
Cookie |
Stripped before forwarding |
secure_ehbp mode are EHBP ciphertext – the proxy streams them
without interpretation.
Document generated from source code analysis of the Shield NOYB codebase, March 2026.
Repository: private_assistant. Primary source files referenced throughout.