A set of building blocks for end-to-end verifiable TEE applications.
Note: Under active development, has not been audited.
Trusted execution environments make it possible to build private, verifiable web services, but web pages in a browser cannot verify that they're connected to a TEE. Browsers don't expose X.509 certificate information that proves a connection terminates inside the secure environment, so proxies like Cloudflare can trivially see and modify traffic to TEEs forwarded through them. Anyone hosting a TEE app can use a TLS proxy to extract session data and/or impersonate users.
To work around this, some TEE application hosts implement their own proxy in front of the TEE, but this reintroduces trust assumptions around their proxy. We can also use certificate log monitoring to improve security, but this happens out-of-band and doesn't directly protect the connection between the user and the TEE.
@teekit/tunnel implements protocols for remotely-attested HTTPS and WSS channels, which web pages can use to establish secure connections that verifiably terminate inside trusted execution environments (currently Intel TDX/SGX). This makes it possible to create applications that interact with a TEE trustlessly, especially if applications are pinned on IPFS or other immutable hosting services. It also allows TEE developers to use public certificate authorities like Let's Encrypt without custom configuration.
- @teekit/tunnel:
- Establishes tunneled connections to a TEE through an encrypted WebSocket, with key exchange, quote validation, and CRL/TCB validation
 - Supports encrypted HTTP requests via a 
fetch-compatible API - Supports encrypted WebSockets via a 
WebSocket-compatible API - Includes a ServiceWorker for upgrading all HTTP requests from a browser page to use the encrypted channel
 
 - @teekit/qvl:
- WebCrypto-based SGX/TDX quote verification library
 - Validates the full chain of trust from the root CA, down to binding
the public key of the encrypted channel in 
report_data - Includes optional CRL/TCB validation inside the browser. (TCB info cannot be fetched from Intel in the browser without a CORS proxy.)
 
 - @teekit/demo:
- A demo application that supports HTTPS and WSS requests over the encrypted channel, both with and without the embedded ServiceWorker.
 
 
The encrypted channel adds ~3x overhead for concurrent requests, and ~6.5x for large payloads. Some of this increase is because we use @noble/ciphers for encryption; Wasm-based cryptography has been tested to give us a speedup of ~50-100%.
| Test | Average | Median | 90th % | 99th % | Max | 
|---|---|---|---|---|---|
| 100 concurrent requests | 109.9ms | 109.9ms | 110.47ms | 112.32ms | 112.32ms | 
| 50 serial requests | 0.96ms | 0.38ms | 0.55ms | 28.72ms | 28.72ms | 
| 50 requests with 1MB up/down | 33.64ms | 33.11ms | 34.49ms | 60.4ms | 60.4ms | 
| Test | Average | Median | 90th % | 99th % | Max | 
|---|---|---|---|---|---|
| 100 concurrent requests | 32.83ms | 33.28ms | 39.13ms | 45.75ms | 45.75ms | 
| 50 serial requests | 0.37ms | 0.2ms | 0.31ms | 7.67ms | 7.67ms | 
| 50 requests with 1MB up/down | 5.13ms | 4.95ms | 5.56ms | 8.88ms | 8.88ms | 
On the client, create a TunnelClient() object. You should switch out
unencrypted Node.js fetch and WebSocket instances for our fetch and
WebSocket wrappers, exposed on the TunnelClient().
It is your responsibility to configure TunnelClient with the expected
mrtd and report_data measurements, certificate revocation lists,
and manually verify the TCB inside any custom quote validator.
Your client will validate all measurements, quote signatures, and additional CRL/TCB info before opening a connection.
import { TunnelClient } from "@teekit/tunnel"
import { hex, parseTdxQuote } from "@teekit/qvl"
async function main() {
  const origin = "http://127.0.0.1:3000"
  // You can validate against expected mrtd/report_data or provide a custom matcher.
  // Below shows fixed values; compute these from an expected quote if you have one.
  const expectedMrtd = '...' /* hex string */
  const expectedReportData = '...' /* hex string */
  const client = await TunnelClient.initialize(origin, {
    mrtd: expectedMrtd,
    report_data: expectedReportData,
    crl: [], // certificate revocation list
    verifyTcb: ({ fmspc, cpuSvn, pceSvn, quote }) => {
      // Check for TCB freshness and return true if valid
      return true
    },
    // sgx: true // defaults to TDX otherwise
  })
  // HTTP over tunnel
  const res = await client.fetch("/hello")
  console.log(await res.text()) // server replies "world"
  // WebSocket over tunnel
  const ws = new client.WebSocket(origin.replace(/^http/, "ws"))
  ws.addEventListener("open", () => ws.send("ping"))
  ws.addEventListener("message", (evt) => console.log(evt.data))
}
main()On the server, add a TunnelServer middleware to your Node.js/Express
server. We only support Node.js now, but future versions will support
arbitrary backends through Nginx.
import express from "express"
import { TunnelServer } from "@teekit/tunnel"
async function main() {
  const app = express()
  app.get("/hello", (_req, res) => res.status(200).send("world"))
  async function getQuote(x25519PublicKey: Uint8Array): Promise<Uint8Array> {
    // Return a Uint8Array bound to x25519PublicKey. See packages/demo/server
    // for an example that uses `trustauthority-cli` and --user-data binding.
    return new Uint8Array(Buffer.from('...', 'hex'))
  }
  const tunnelServer = await TunnelServer.initialize(app, getQuote)
  // Optional: WebSocket support via the built-in mock server
  tunnelServer.wss.on("connection", (ws) => {
    ws.on("message", (data: any) => ws.send(data))
  })
  tunnelServer.server.listen(3000, () => {
    console.log("teekit service listening on :3000")
  })
}
main()You may also use the included ServiceWorker to transparently upgrade
HTTP GET/POST requests to go over the encrypted channel to your
TunnelServer.
To do this, first add the ServiceWorker plugin to your bundler. You
can use an included Vite plugin to handle this, or manually serve
__ra-serviceworker__.js at your web root from
node_modules/@teekit/tunnel/lib/sw.build.js:
// vite.config.js
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import { includeRaServiceWorker } from "@teekit/tunnel/sw"
export default defineConfig({
  plugins: [react(), includeRaServiceWorker()],
})Then, register the ServiceWorker at app startup, pointed at your tunnel origin:
// src/main.tsx (or similar)
import { registerServiceWorker } from "@teekit/tunnel/register"
const baseUrl = "http://127.0.0.1:3000" // your TunnelServer origin
registerServiceWorker(baseUrl)Note that different browsers vary in their support of ServiceWorkers, and some browsers may block ServiceWorkers from being installed, in which case your application will be silently downgraded to lose privacy. For this reason we recommend using the default @teekit/tunnel APIs whenever possible.
By default, ServiceWorkers intercept link clicks, location.assign() calls, subresource requests, and fetch() / XMLHttpRequest requests (but not WebSockets).
The packages/demo directory contains a demo of a chat app that relays WebSocket messages and fetch requests over an encrypted channel.
Node v22 is expected.
Run the client using tsx:
npm run dev
Run the server using Node.js:
npm run server
The tunnel performs a key exchange and attestation check before allowing any traffic. After the handshake, all payloads are CBOR encoded and encrypted with the XSalsa20‑Poly1305 stream cipher.
- Client opens a control WebSocket to the server at
ws(s)://<host>:<port>/__ra__. - Server immediately sends 
server_kxwith an X25519 public key and a TDX/SGX attestation quote. - Client verifies the quote (using 
@teekit/qvl), optionally enforcesmrtd/report_dataor a custom matcher, generates a symmetric key, and sends it sealed to the server viaclient_kx. - All subsequent messages are encrypted envelopes
{ type: "enc", nonce, ciphertext }carrying tunneled HTTP and WebSocket messages. 
- One fixed keypair per server. No key rotation (yet).
 - HTTP request/response bodies are buffered end-to-end, not streamed.
 - HTTP request bodies supported: 
string,Uint8Array,ArrayBuffer,ReadableStream(noFormData). - WebSocket bodies: 
Blobis not supported, convert toArrayBuffer. - The client request timeout is 30 seconds, and this is not configurable at this time.
 - WebSocket messages queued before 
openare flushed once the socket opens. 
@teekit/tunnel, @teekit/qvl, and @teekit/demo packages are made available under the MIT License.
@teekit/kettle is made available under the AGPL V3 License.
(C) 2025 Canvas Technologies, Inc.