Shield Shield Assistant Technical White Paper

Shield NOYB: Architecture and Security of a Browser-First Private LLM Chat System

Technical White Paper — March 2026; verification revision September 2026


Table of Contents

  1. Abstract
  2. Introduction
  3. System Architecture
  4. Threat Model and Trust Boundaries
  5. Transport Layer: EHBP End-to-End Encryption
  6. Client-Side Data Sovereignty
  7. Client-Side Intelligence
  8. Proxy Verification Receipts
  9. Supply Chain Integrity: IPFS Bundle Pinning
  10. Authentication and Session Management
  11. Server-Side Minimization
  12. Defensive Controls
  13. Internationalization
  14. Mobile Platform Support
  15. Residual Risks and Mitigations
  16. Summary of Privacy Guarantees
  17. Appendix A: Configuration Reference
  18. Appendix B: API Contract Summary

1. Abstract

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.


2. Introduction

2.1 Motivation

2.1.1 The Plaintext Inference Constraint

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.

2.1.2 Unverifiable Privacy

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:

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.

2.2 Design Goals

  1. Zero server-side context storage. No conversation, message, embedding, or prompt data is ever written to server-side storage, logs, or caches.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

2.3 Scope of This Document

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.


3. System Architecture

3.1 High-Level Topology

The system consists of three principals:

  1. 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.

  2. 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.

  3. 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  │
                    └─────────────────────┘

3.2 Data Flow: Secure Chat Completion

The complete data flow for a single chat interaction in secure_ehbp mode:

  1. User types a message in the browser UI.
  2. Message saved to IndexedDB via memory_store.js (client-side only).
  3. Message text chunked by chunkText() in llm_client.js: 800-character chunks with 120-character overlap.
  4. Each chunk embedded by sending it to the enclave’s embedding endpoint (e.g., nomic-embed-text) via the encrypted transport. Fallback: local deterministic hash embedding if remote embedding fails.
  5. Embedding vectors stored in IndexedDB alongside the text chunks (client-side only).
  6. Context retrieval: similarity search runs in the browser against all memory chunks for the current conversation. In sqlite-vec mode, this uses the WASM vec0 virtual table. In indexeddb mode, it performs a brute-force cosine similarity scan.
  7. Prompt assembled in the browser: persona instruction, user profile (if enabled), relevant context (top-K chunks), document text (if attached), last 20 messages of conversation history.
  8. SecureClient encrypts the full request body using EHBP (HPKE RFC 9180).
  9. Encrypted body sent to Phoenix proxy with routing headers (Authorization, X-Tinfoil-Enclave-Url, Ehbp-Encapsulated-Key).
  10. Phoenix validates the proxy token, checks the EHBP header, injects the server-side API key, and forwards the opaque ciphertext to the enclave.
  11. Enclave decrypts the request body using the EHBP middleware, processes the query, generates a response, and encrypts the response body.
  12. Encrypted response streams back through Phoenix to the browser.
  13. SecureClient decrypts the response. Tokens are rendered in the UI.
  14. Response saved to IndexedDB locally. Response text is chunked and embedded into memory.

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.

3.3 Component Inventory

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

3.4 Design Principles

  1. Local-first memory. No server-side context database. All user-generated content and derived data (embeddings, chunks, profiles, personas) reside in the browser.

  2. 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.

  3. Explicit safety gates. Debug mode is hard-disabled in production at boot time. Transport mode degradation is visible to the user.

  4. Fault tolerance. The client falls back gracefully when secure transport, sqlite-vec WASM, ZK verification backends, or remote embedding services are unavailable.

  5. Minimal server footprint. The server contains only authentication tables, no content storage, and no state beyond the request lifecycle.

  6. Verifiable boundaries. Each trust boundary (client, proxy, enclave) can be independently verified by the user through distinct mechanisms (IPFS CID, receipts, attestation).


4. Threat Model and Trust Boundaries

4.1 Principals and Trust Levels

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).

4.2 What the Phoenix Server Can See

Even with secure_ehbp active and functioning correctly, the Phoenix backend has visibility into the following metadata:

4.3 What the Phoenix Server Cannot See

4.4 What the Tinfoil Enclave Can See

4.5 Trust Gap Analysis: Three Axes of Verification

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

5. Transport Layer: EHBP End-to-End Encryption

5.1 Protocol Overview

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.

5.2 Key Exchange and Attestation

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.

5.3 Request Encryption

For each request with a body (POST, PUT, PATCH):

  1. The client establishes an HPKE encryption context using the enclave’s verified public key.
  2. The client includes the HPKE encapsulated key in the Ehbp-Encapsulated-Key request header (lowercase hex, 64 characters for X25519).
  3. The request body is encrypted as a stream of length-prefixed chunks. Each chunk consists of a 4-byte big-endian length prefix followed by AES-256-GCM ciphertext.
  4. The encrypted body replaces the original plaintext body in the HTTP request.

5.4 Response Encryption

  1. The enclave decrypts the request body using its private key and the encapsulated key from the request header.
  2. The enclave generates a random 32-byte nonce and includes it in the Ehbp-Response-Nonce response header (lowercase hex, 64 characters).
  3. The enclave derives response encryption keys via HKDF:
    • Salt: concat(encapsulated_key, response_nonce)
    • Secret: HPKE export secret with label "ehbp response"
  4. The response body is encrypted with AES-256-GCM using the derived keys, framed as length-prefixed chunks.
  5. The client decrypts the response using the same key derivation process, having access to the original encryption context and the response nonce from the header.

5.5 What the Proxy Sees During Transit

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:

5.6 Server-Side EHBP Enforcement

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.


6. Client-Side Data Sovereignty

6.1 Dual Storage Architecture

All user-generated content and derived data is stored exclusively in the browser using two complementary backends:

IndexedDB (IndexedDbMemoryStore in memory_store.js):

sqlite-vec WASM (SqliteVecMemoryStore in memory_store.js):

Both backends are selected at runtime by the user via the Memory Backend dropdown in the UI.

6.2 Data Locality Table

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

6.3 Optional At-Rest Encryption (Local Vault)

The LocalVault class (local_vault.js) provides optional passphrase-based encryption for stored records using the Web Crypto API:

Key derivation:

Encryption:

Storage format:

Behavior:

Protection scope:

6.4 Data Export

Users can export their conversations as a JSON file:

{
  "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"
        }
      ]
    }
  ]
}

7. Client-Side Intelligence

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.

7.1 RAG Pipeline

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                      │
└──────────────────────────────────────────────────────────────────┘

7.1.1 Chunking

chunkText() in llm_client.js splits text into overlapping chunks:

7.1.2 Embedding

Each chunk is sent to the enclave’s embedding endpoint for vector representation:

7.1.3 Retrieval

When the user sends a new message:

  1. The message text is embedded (using the same primary/fallback path as above).
  2. A similarity search is run against all memory chunks for the current conversation.

Two retrieval backends are available:

The top-K results (default K=6, configurable via DEFAULT_RETRIEVAL_TOP_K) are returned as text chunks for prompt injection.

7.1.4 Prompt Assembly

The final prompt sent to the enclave is assembled in app.js:

  1. Persona instruction. The system prompt from the active persona. This may be a built-in persona or a custom user-defined persona.
  2. User profile. If profile learning is enabled and a profile exists, the learned user traits are injected as a system message.
  3. Relevant context. The top-K retrieved memory chunks, formatted as a system message: "Relevant local context:\n<chunk1>\n<chunk2>\n...".
  4. Document text. If the user has attached a document, the parsed text is included.
  5. Conversation history. The last 20 messages from the current conversation, in chronological order.
  6. Current user message. The new message the user just typed.

7.2 Document Parsing

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:

7.3 Persona System

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:

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.

7.4 Profile Learning

Shield NOYB can learn about the user’s communication patterns and preferences over time, entirely client-side:

Extraction process:

  1. After a conversation reaches a threshold (4 or more messages), a background profile extraction runs.
  2. The extraction uses the LLM itself (via the encrypted transport) to analyze the conversation and extract structured traits.
  3. Extracted categories: communication_style, recurring_topics, preferences, emotional_patterns, context_notes.

Storage:

User control:

Privacy properties:


8. Proxy Verification Receipts

8.1 Revision Scope and Assurance Target

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.

8.2 Trust Chain and Division of Responsibilities

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.

8.3 Verification API and Orchestration

The client-originated exchange uses these verification operations:

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.

8.4 Client-Originated Request Construction

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.

8.5 Verification Request Wire Contract

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.

8.6 Proxy Forwarding and Response Envelope

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.

8.7 Receipt Backends and Exact Guest Claims

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.

8.8 Binding the Checked Claim to the Cryptographic Journal

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.

8.9 Binding and Authenticating the Client Exchange

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.

8.10 NIF Recorder: Binary Representation and Claim Limits

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.

8.11 Failure Behavior, Coverage, and Operational Cost

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.

8.12 Validation and Source Map

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.

8.13 ODP and Shieldcode 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.


9. Supply Chain Integrity: IPFS Bundle Pinning

9.1 Motivation

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.

9.2 Pinning Flows

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:

Flow 2: Proxy verification assets (compiled artifact)

Pins the browser-side verifier assets used for receipt validation:

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:

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:

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.

9.3 Pinning Implementation

The scripts use Pinata’s pinFileToIPFS endpoint with directory-style multipart upload:

9.4 Browser Verification

The browser performs a plain hash comparison:

  1. Fetch the locally served app.js and app.css from the current page.
  2. Fetch the same files from the IPFS gateway using the pinned CID.
  3. Compute SHA-256 hashes of both versions.
  4. Compare. A match establishes equality with the fetched reference under the trust assumptions in Section 9.6. A mismatch is reported in the verification panel.

This check does not use zero-knowledge proofs. It is a direct content comparison.

9.5 Output Artifacts

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.

9.6 Limitations


10. Authentication and Session Management

10.1 User Authentication

Users authenticate via one of two methods:

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).

10.2 Proxy Token Lifecycle

The proxy token is a short-lived credential that authorizes the browser to make proxy requests:

  1. The browser calls POST /api/proxy/token with the session cookie and CSRF token.
  2. The server issues a Phoenix.Token signed with the salt "proxy_token_v1", containing the user ID and issuance timestamp.
  3. The token has a configurable TTL (default 300 seconds / 5 minutes), set via PROXY_TOKEN_TTL_SECONDS.
  4. The browser caches the token in memory (ProxySession.cachedToken) and refreshes it 15 seconds before expiry.
  5. Each proxy request includes the token as Authorization: Bearer <proxy_token>.
  6. The 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).

10.3 Upstream API Key Isolation

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:

  1. The proxy strips the incoming Authorization header (it is in the @hop_by_hop_headers reject list).
  2. The proxy injects a new Authorization: Bearer <TINFOIL_API_KEY> header.

This creates a credential boundary:


11. Server-Side Minimization

11.1 Database Schema

The Phoenix backend uses SQLite via Ecto. The only tables are those generated by mix phx.gen.auth:

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.

11.2 Application Tree

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.

11.3 Logging Discipline

The proxy client explicitly avoids logging request or response content in secure mode:

11.4 Body Handling

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.


12. Defensive Controls

12.1 Debug Mode Lockout

The legacy_json transport mode and the /api/proxy/debug/* path allow plaintext proxy requests for development. The following controls prevent misuse:

12.2 Header Filtering

The proxy client strips the following headers before forwarding upstream:

The proxy then injects its own Authorization: Bearer <TINFOIL_API_KEY> header. This ensures:

12.3 HTTPS URL Validation

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.

12.4 Client-Side Fallback Behavior

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.


13. Internationalization

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.


14. Mobile Platform Support

14.1 Architecture

Shield NOYB uses Capacitor 8.2 to wrap the web application for mobile platforms:

14.2 Privacy Properties

The mobile wrappers preserve all the privacy properties of the web application:

14.3 Mobile-Specific Considerations


15. Residual Risks and Mitigations

# 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).

16. Summary of Privacy Guarantees

16.1 Guarantee Table

# 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.

16.2 Trust Assumptions

For these guarantees to hold, the following assumptions must be satisfied:

  1. The user’s browser and device are not compromised. A compromised browser can access all in-memory and IndexedDB data regardless of EHBP encryption.

  2. 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.

  3. The server is serving the unmodified JavaScript bundle. This is the standard web trust model. IPFS CID checking provides detective (not preventive) assurance.

  4. 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.

  5. 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.

  6. 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.

16.3 Confidence Level Definitions

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.

Appendix A: Configuration Reference

A.1 Core Infrastructure

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)

A.2 Authentication

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

A.3 Proxy Token

Variable Required Default Description
PROXY_TOKEN_TTL_SECONDS No 300 Proxy token validity window in seconds

A.4 Proxy Verification

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

A.5 IPFS Bundle Pinning

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

A.6 Debug Mode

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

A.7 Model Defaults

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

A.8 Production Infrastructure

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

A.9 Build Tooling

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

A.10 Effective Precedence

  1. OS environment variable (current process)
  2. Default from config/runtime.exs
  3. Internal default in 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.


Appendix B: API Contract Summary

B.1 Token Issuance

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

B.2 Attestation Proxy

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

B.3 Secure Proxy

* /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

B.4 Debug Proxy

* /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

B.5 Verification Receipt

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.

B.6 Forwarding Challenge Proof (ZK Sidecar)

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

B.7 Header Handling Summary

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

B.8 Streaming Behavior


Document generated from source code analysis of the Shield NOYB codebase, March 2026. Repository: private_assistant. Primary source files referenced throughout.