Skip to main content

Introduction

When building applications on top of Tinfoil’s private inference API, you often need to add your own infrastructure layer between your client applications and the inference enclave. Perhaps you want to authenticate your users, track API usage, implement rate limiting, or add custom routing logic. The challenge is doing this without compromising the end-to-end encryption. This is where the Encrypted HTTP Body Protocol (EHBP) becomes useful. EHBP encrypts HTTP message bodies at the application layer using Hybrid Public Key Encryption (HPKE), completely separate from transport layer security like TLS. This separation means you can run a proxy server that inspects headers and metadata, while the actual request and response bodies remain encrypted end-to-end between your client and the Tinfoil enclave. Moreover, our SDKs automatically check and validate the HPKE key to ensure it is generated inside an attested secure enclave. Your proxy server becomes an intermediary that can be used add your Tinfoil API keys, authenticate and authorize your users with custom logic, track usage metrics and implement rate limiting, add custom routing headers based on your business logic, and log metadata for debugging and monitoring—all while the actual inference data remains encrypted throughout the entire journey.

The Challenge

When you’re building a production application with Tinfoil, you typically face a dilemma. On one hand, you need to keep your TINFOIL_API_KEY secret—exposing it to client browsers would allow anyone to use your API quota. On the other hand, you want your sensitive prompts and completions to be encrypted end-to-end to the attested enclave, not just to your backend server.

The Solution using EHBP

Your proxy server sits between your client and the Tinfoil enclave, handling authentication and metadata, while the actual inference data flows through it encrypted. The proxy receives encrypted requests from your client application, adds your TINFOIL_API_KEY as the Authorization header, preserves the encryption headers required by the protocol, forwards the encrypted payload to Tinfoil’s enclave, and returns the encrypted response to your client. This architecture gives you control over what the proxy can and cannot access. The proxy has full visibility into HTTP headers, allowing you to read custom request headers (like X-User-ID or X-Request-ID) for authentication or routing decisions, add custom response headers (like X-Rate-Limit-Remaining or X-Request-Cost) for rate limiting or cost tracking, and log metadata for debugging and monitoring. But the request and response bodies—the actual prompts, completions, and inference data—remain encrypted from client to enclave using keys the proxy never possesses.

How It Works

The data flow shows how encryption is maintained throughout the request lifecycle:

Client Setup

Setting up the client is straightforward. First, install the Tinfoil SDK:
npm install tinfoil
Proxy support with SecureClient is currently only available in the Node.js SDK.
The key detail is that you configure the Tinfoil client with two different URLs. The baseURL points to your proxy server where requests are sent, while the enclaveURL points to the Tinfoil enclave whose public key is used for encryption. This configuration ensures that even though your proxy handles the HTTP routing, only the enclave can decrypt the request bodies.
import { SecureClient } from "tinfoil";

const client = new SecureClient({
  baseURL: "https://your-proxy-server.com/",
  enclaveURL: "https://ehbp.inf6.tinfoil.sh/v1/",
  configRepo: "tinfoilsh/confidential-inference-proxy-hpke",
});

// Wait for the client to fetch encryption keys and perform verification
await client.ready();

// Make encrypted requests
const response = await client.fetch("/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "text/event-stream",
    // Optional: Add custom headers for your proxy to read
    "Your-Custom-Request-Header": "custom-value",
  },
  body: JSON.stringify({
    model: "gpt-oss-120b",
    messages: [{ role: "user", content: "Hello!" }],
    stream: true,
  }),
});
When you call await client.ready(), the client fetches the enclave’s public key from the enclaveURL and performs verification. From that point on, all request bodies are encrypted using that key before being sent to your proxy, and all response bodies are decrypted after being received.

Proxy Server Setup

One of the important aspects of EHBP is that the proxy requires no special cryptographic libraries or dependencies. Since the proxy doesn’t participate in encryption or decryption, it only needs basic HTTP handling capabilities available in any web framework. Your proxy’s job is simple: preserve the EHBP protocol headers that coordinate encryption between client and enclave, add your authentication credentials, and forward the encrypted payload. While we provide example implementations in Go and Node.js below, the proxy pattern works with any language that can handle HTTP requests. The example repository provides a complete reference implementation that you can adapt to Python, Rust, or any other language.

Required Headers to Preserve

The EHBP protocol uses specific headers to coordinate encryption keys between the client and enclave. Your proxy must forward these headers unchanged in both directions. For requests, you need to preserve Ehbp-Client-Public-Key (the client’s public key for encrypting responses) and Ehbp-Encapsulated-Key (the encrypted symmetric key for the request body). For responses, you need to preserve Ehbp-Encapsulated-Key (the encrypted symmetric key for the response body), Ehbp-Client-Public-Key (echoed back from the request), and Ehbp-Fallback (indicates if the server responded in plaintext when fallback is enabled). Understanding the body framing format isn’t necessary for proxy implementation, but it helps explain why these headers are critical—they contain the ephemeral keys needed to decrypt each chunk of the encrypted stream.

CORS Configuration

If your proxy will be called from browser-based applications, you’ll need to configure CORS headers to allow the browser to send and read the encryption headers. The key requirement is exposing the EHBP headers in both directions:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Accept, Authorization, Content-Type, Ehbp-Client-Public-Key, Ehbp-Encapsulated-Key
Access-Control-Expose-Headers: Ehbp-Encapsulated-Key, Ehbp-Client-Public-Key, Ehbp-Fallback

Implementation Examples

package main

import (
	"io"
	"log"
	"net/http"
	"os"
)

const upstreamURL = "https://ehbp.inf6.tinfoil.sh/v1/chat/completions"

var (
	encryptionRequestHeaders  = []string{"Ehbp-Client-Public-Key", "Ehbp-Encapsulated-Key"}
	encryptionResponseHeaders = []string{"Ehbp-Encapsulated-Key", "Ehbp-Client-Public-Key", "Ehbp-Fallback"}
)

func proxyHandler(w http.ResponseWriter, r *http.Request) {
	// Set CORS headers
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, Ehbp-Client-Public-Key, Ehbp-Encapsulated-Key")
	w.Header().Set("Access-Control-Expose-Headers", "Ehbp-Encapsulated-Key, Ehbp-Client-Public-Key, Ehbp-Fallback")

	if r.Method == http.MethodOptions {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	// Create upstream request
	req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, upstreamURL, r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	req.Header.Set("Content-Type", "application/json")
	if accept := r.Header.Get("Accept"); accept != "" {
		req.Header.Set("Accept", accept)
	}

	// Add your API key
	apiKey := os.Getenv("TINFOIL_API_KEY")
	if apiKey == "" {
		log.Println("Error: TINFOIL_API_KEY not set")
		http.Error(w, "TINFOIL_API_KEY not set", http.StatusInternalServerError)
		return
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)

	// Copy encryption headers
	copyHeaders(req.Header, r.Header, encryptionRequestHeaders...)

	// Forward request
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()

	// Copy encryption headers from response
	copyHeaders(w.Header(), resp.Header, encryptionResponseHeaders...)

	if ct := resp.Header.Get("Content-Type"); ct != "" {
		w.Header().Set("Content-Type", ct)
	}

	w.WriteHeader(resp.StatusCode)
	io.Copy(w, resp.Body)
}

func copyHeaders(dst, src http.Header, keys ...string) {
	for _, key := range keys {
		if value := src.Get(key); value != "" {
			dst.Set(key, value)
		}
	}
}

func main() {
	http.HandleFunc("/v1/chat/completions", proxyHandler)
	log.Println("Proxy listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Working with Custom Headers

Beyond the required EHBP headers, you can use custom headers to implement your own application-level protocols between the client and proxy. This is where you build your authentication, rate limiting, user tracking, and other business logic.

Request Headers

Your client can include custom headers that the proxy reads but the enclave never sees. This is perfect for user authentication tokens, request IDs, feature flags, or any other metadata:
const response = await client.fetch("/v1/chat/completions", {
  headers: {
    "X-User-ID": "user-123",
    "X-Request-ID": crypto.randomUUID(),
    "Your-Custom-Request-Header": "custom-value",
  },
  // ...
});
Your proxy can then read and strip these headers for logging, routing decisions, or authentication checks before forwarding the encrypted request to the enclave:
// In your proxy server
if customHeader := r.Header.Get("Your-Custom-Request-Header"); customHeader != "" {
    log.Printf("Custom request header received: %s", customHeader)
    // These headers are not forwarded to the enclave
}
Just remember to add any custom headers you use to your CORS Access-Control-Allow-Headers configuration so browsers can send them.

Response Headers

Similarly, your proxy can add custom headers to responses that the client can read. This is useful for communicating rate limit information, cost tracking, request IDs for debugging, or any other metadata:
// In your proxy server
w.Header().Set("Your-Custom-Response-Header", "response-value")
w.Header().Set("X-Rate-Limit-Remaining", "100")
The client can then access these headers:
// Optional: Read custom headers from the response
const customHeader = response.headers.get("Your-Custom-Response-Header");
const remaining = response.headers.get("X-Rate-Limit-Remaining");
As with request headers, you’ll need to expose custom response headers in your CORS Access-Control-Expose-Headers configuration for browsers to access them.

Security Considerations

While EHBP encrypts the request and response bodies, the HTTP headers remain visible throughout the proxy chain. This design is intentional—it’s what allows proxies to route and manage requests—but it has important security implications.
Always keep your TINFOIL_API_KEY secret. Never expose it to client applications. The entire purpose of the proxy architecture is to keep this key on your backend.
First and foremost, use HTTPS in production. While the request and response bodies are encrypted at the application layer, HTTP headers are transmitted in the clear. Using HTTPS provides transport-layer encryption for headers, protecting metadata like user IDs, request IDs, and other information you might include in custom headers. Second, validate and sanitize custom headers from clients. Since your proxy reads these headers for authentication and routing decisions, treat them as untrusted input. Validate formats, check for injection attacks, and implement proper authentication before trusting any client-provided header values. Finally, always preserve and forward the EHBP headers (Ehbp-Client-Public-Key, Ehbp-Encapsulated-Key, and Ehbp-Fallback). These headers are critical for the encryption protocol to work. Dropping or modifying them will break the end-to-end encryption between client and enclave.

Next Steps

For a complete working example, check out the encrypted-request-proxy-example repository. It includes a Go proxy implementation, TypeScript client with streaming support, custom header handling for authentication and rate limiting, and comprehensive error handling.

Usage Tracking and Monitoring

Your proxy server can use the Admin API to programmatically track usage and monitor your infrastructure. This allows you to retrieve detailed usage statistics by time period, get per-key token usage and costs, monitor usage patterns with time-series data, and track model-specific usage across your API keys. This enables you to build custom dashboards, implement alerting based on usage thresholds, or analyze cost patterns across your infrastructure.