Caution
Disclaimer: RATSnest is a proof-of-concept tech demo that illustrates end-to-end confidential compute with remote attestation. It is not production-ready, so do not use it in production.
- It uses TeeKit (read this first) to establish an encrypted, hardware-attested encryption tunnel between the browser and TDX VM on a Google Cloud Confidential VM.
- It uses a fork of flashbot-images to build a custom GCP Confidential VM friendly disk image with MRTD measurements, and IMA logging.
Remote Attestation Tunnel with TDX - A minimal proof-of-concept demonstrating end-to-end confidential computing with Intel TDX remote attestation.
Browser clients verify they're talking to a specific version of code running in a TDX VM using cryptographic attestation, with app-layer encryption that cloud providers cannot intercept.
RATSnest demonstrates how to build a verifiable confidential computing application where:
- Browser clients cryptographically verify they're talking to specific code running in a hardware-attested TDX VM
- All communication is encrypted end-to-end using X25519 keys bound to TDX quotes
- Runtime integrity is verified using IMA measurements of executed binaries
- Cloud providers (even with root access) cannot decrypt or tamper with the communication
| Term | What It Means |
|---|---|
| TDX | Intel Trust Domain Extensions - hardware-based confidential computing technology |
| MRTD | Measurement of Trust Domain - cryptographic hash of VM infrastructure (firmware + config) |
| RTMR | Runtime Measurement Register - boot-time measurements (kernel + initrd + cmdline) |
| IMA | Integrity Measurement Architecture - runtime file integrity measurements |
| ConfigFS-TSM | Linux kernel interface for generating TDX attestation quotes |
| TEEKit | Library for building attested encrypted tunnels (@teekit/tunnel) https://github.com/canvasxyz/teekit |
| Quote | Hardware-signed attestation report containing measurements + custom data |
| X25519 | Elliptic curve Diffie-Hellman for key exchange (used by TEEKit) |
| XSalsa20-Poly1305 | Authenticated encryption algorithm (encrypts tunnel messages) |
- Deno 2.0+ (install)
- Node.js 20+ + npm
- Nix with flakes enabled (install)
- Google Cloud SDK for deployment (install)
# 1. Install frontend dependencies
cd frontend && npm install
# 2. Run backend (in one terminal)
cd backend && deno task dev
# 3. Run frontend (in another terminal)
cd frontend && npm run dev
# 4. Open http://localhost:5173# Run all tests
make test
# Or manually:
cd backend && deno test --allow-all
cd frontend && npm run testβββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β Browser ββββββββ HTTPS βββββββββΊβ TDX Confidential VM (GCP) β
β Client β β β
β β 1. WebSocket β ββββββββββββββββββββββββββ β
β @teekit/tunnel β Handshake β β TunnelServer β β
β @teekit/qvl ββββββββββββββββββββββΊ β β (Express + WS) β β
β β 2. TDX Quote + β β β β
β - Verify Quote β X25519 Pubkey β β - Generate Quote β β
β - Verify MRTD ββββββββββββββββββββββ β β - Bind X25519 Key β β
β - Verify RTMRs β 3. Encrypted Key β β - Proxy Requests β β
β - Verify IMA ββββββββββββββββββββββΊ β ββββββββββββ¬ββββββββββββββ β
β - X25519 ECDH β 4. XSalsa20-Poly1305 β β β
β ββββββββββββββββββββββΊ β β β
βββββββββββββββββββ Encrypted β ββββββββββββββββββββββββββ β
Messages β β Hono API Server β β
β β (localhost:4000) β β
β β β β
β β - GET /api/hello β β
β β - GET /api/ima/log β β
β β - POST /debug/* β β
β β - Static files (SPA) β β
β ββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββ β
β β TDX Quote Generation β β
β β (ConfigFS-TSM) β β
β β β β
β β /sys/kernel/config/ β β
β β tsm/report/ β β
β ββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββ β
β β IMA Runtime β β
β β Measurements β β
β β β β
β β /sys/kernel/security/ β β
β β ima/ascii_runtime_ β β
β β measurements β β
β ββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββ
Frontend (React + TypeScript):
- @teekit/tunnel - TunnelClient for WebSocket handshake & encryption
- @teekit/qvl - Quote verification library (TDX quote parsing)
- shared/policy.ts - MRTD/RTMR/IMA policy verification
Backend (Deno + TypeScript):
- TunnelServer (Express, port 3000) - WebSocket server for attestation handshake
- Hono API (port 4000) - HTTP API for application logic
- tdx.ts - TDX quote generation via ConfigFS-TSM
- ima.ts - IMA runtime measurement verification
Attestation Stack:
- ConfigFS-TSM - Linux kernel interface for TDX quote generation
- IMA (Integrity Measurement Architecture) - Runtime file integrity measurements
- TEEKit - Client/server libraries for attested encrypted tunnels
RATSnest uses TEEKit (@teekit/tunnel) to establish an encrypted, hardware-attested tunnel between the browser and TDX VM. Here's the complete handshake flow:
1. Client Opens WebSocket
// Client initiates WebSocket connection
const client = await TunnelClient.initialize('https://your-vm.com')
// This opens: wss://your-vm.com/__ra__2. Server Sends Key Exchange (server_kx message)
The server sends a message containing:
{
type: "server_kx",
x25519PublicKey: Uint8Array(32), // Server's ephemeral X25519 public key
quote: Uint8Array, // TDX quote (with report_data embedded)
verifier_data: { // CBOR-encoded freshness proof
val: Uint8Array(32), // nonce (32 random bytes)
iat: Uint8Array(8) // timestamp (8-byte big-endian)
},
runtime_data: Uint8Array // Optional: IMA log bytes
}3. Quote Generation (Server-Side)
The quote is pre-generated when TunnelServer.initialize() is called:
// backend/tunnel.ts
async function getQuote(x25519PublicKey: Uint8Array): Promise<QuoteData> {
// 1. Generate ephemeral X25519 keypair (once per boot)
// 2. Generate nonce + timestamp
const nonce = crypto.getRandomValues(new Uint8Array(32)) // 32 random bytes
const iat = new Uint8Array(8) // 8-byte timestamp
const now = BigInt(Date.now())
new DataView(iat.buffer).setBigUint64(0, now, false) // Big-endian
// 3. Compute report_data = SHA-512(nonce || iat || x25519_pubkey)
const combined = new Uint8Array(32 + 8 + 32) // 72 bytes total
combined.set(nonce, 0)
combined.set(iat, 32)
combined.set(x25519PublicKey, 40)
const reportData = await crypto.subtle.digest("SHA-512", combined) // 64 bytes
// 4. Get TDX quote with embedded report_data
const quote = await getTdxQuote(reportData) // Calls ConfigFS-TSM
// 5. Return quote + verifier_data
return {
quote,
verifier_data: { val: nonce, iat },
runtime_data: await getIMALogBytes() // Optional IMA measurements
}
}4. Client Verification
The client verifies the server's identity:
// frontend/src/App.tsx - customVerifyQuote
async function verifyTdxQuote(quote: ParsedQuote): Promise<boolean> {
// Extract measurements from quote
const mrtd = quote.body.mr_td // 48 bytes - infrastructure measurement
const rtmr1 = quote.body.rtmr1 // 48 bytes - boot measurements
const reportData = quote.body.report_data // 64 bytes - binding data
// Verify MRTD against policy
if (!policy.allowed_mrtd.includes(mrtd)) {
return false // Wrong VM image!
}
// Verify RTMRs (optional, varies with code changes)
if (policy.allowed_rtmr1 && !policy.allowed_rtmr1.includes(rtmr1)) {
return false // Wrong code version!
}
// Verify IMA measurements (runtime integrity)
const imaLog = await fetch('/api/ima/log').then(r => r.text())
const imaResult = verifyIMAMeasurements(imaLog)
if (!imaResult.allowed) {
return false // Binary has been tampered with!
}
return true
}5. X25519 Key Binding Verification
TEEKit automatically verifies the X25519 public key is bound to the quote:
// Performed by @teekit/tunnel internally
function verifyX25519Binding(serverKx): boolean {
const { x25519PublicKey, quote, verifier_data } = serverKx
// 1. Extract report_data from quote
const reportData = quote.body.report_data // 64 bytes
// 2. Compute expected report_data
const combined = new Uint8Array(72)
combined.set(verifier_data.val, 0) // nonce (32 bytes)
combined.set(verifier_data.iat, 32) // iat (8 bytes)
combined.set(x25519PublicKey, 40) // pubkey (32 bytes)
const expected = await crypto.subtle.digest("SHA-512", combined)
// 3. Verify they match
if (!bytesEqual(reportData, expected)) {
throw new Error("X25519 public key is not bound to quote!")
}
return true
}This proves:
- The server owns the X25519 private key (report_data contains hash of pubkey)
- The quote is fresh (nonce prevents replay attacks)
- The TDX hardware signed the binding (quote signature validates report_data)
6. Client Sends Encrypted Symmetric Key (client_kx)
// Client generates symmetric key and seals it
const symmetricKey = crypto.getRandomValues(new Uint8Array(32))
const sealed = crypto_box_seal(symmetricKey, serverX25519PublicKey)
client.send({
type: "client_kx",
sealed_key: sealed // Encrypted with server's X25519 public key
})7. Server Decrypts Symmetric Key
// Server unseals using X25519 private key
const symmetricKey = crypto_box_seal_open(
sealed_key,
serverX25519PrivateKey,
serverX25519PublicKey
)8. Encrypted Communication
All future messages are encrypted with XSalsa20-Poly1305 using the shared symmetric key:
// Client sends encrypted request
const plaintext = JSON.stringify({ method: 'GET', url: '/api/hello' })
const encrypted = xsalsa20_poly1305_encrypt(plaintext, symmetricKey, nonce)
client.send({ type: 'message', data: encrypted })
// Server decrypts, processes, encrypts response
const decrypted = xsalsa20_poly1305_decrypt(encrypted, symmetricKey, nonce)
const response = await fetch('/api/hello')
const encryptedResponse = xsalsa20_poly1305_encrypt(response, symmetricKey, nonce)
client.send({ type: 'response', data: encryptedResponse })BROWSER CLIENT TDX VM (GCP)
============== ============
[1] WebSocket Handshake
|
| GET wss://vm.example.com/__ra__
|---------------------------------->
| [TunnelServer receives connection]
|
| [2] Server generates quote
| - Generate X25519 keypair (cached)
| - Generate nonce (32 bytes)
| - Generate iat (8 bytes timestamp)
| - SHA-512(nonce || iat || x25519_pubkey)
| - Request quote from TDX hardware
| - Read IMA log
|
| server_kx message
| <----------------------------------
| {
| x25519PublicKey,
| quote (TDX-signed),
| verifier_data: {nonce, iat},
| runtime_data (IMA log)
| }
|
[3] Client verifies quote
- Parse quote with @teekit/qvl
- Extract MRTD from quote.body.mr_td
- Verify: MRTD β policy.allowed_mrtd β
- Extract RTMR1 from quote.body.rtmr1
- Verify: RTMR1 β policy.allowed_rtmr1 β
- Extract report_data (64 bytes)
- Compute: SHA-512(nonce || iat || x25519PublicKey)
- Verify: report_data == computed hash β
- Parse IMA log from runtime_data
- Verify: /usr/bin/ratsnest hash matches β
|
[4] Client generates session key
- Generate 32-byte symmetric key
- Seal with server X25519 pubkey
|
| client_kx message
|---------------------------------->
| { sealed_key }
| [5] Server unseals session key
| - Use X25519 private key
| - Both sides now have shared key
|
[6] Encrypted communication
|
| Encrypted: GET /api/hello
|---------------------------------->
| - Decrypt with XSalsa20-Poly1305
| - Proxy to Hono API (localhost:4000)
| - Encrypt response
| Encrypted: {"message":"world"}
| <----------------------------------
- Decrypt response
- Return to application
|
β
End-to-end encrypted tunnel established
Cloud provider cannot decrypt traffic
Client verified exact code version
β Confidentiality: All messages encrypted with XSalsa20-Poly1305 β Authenticity: TDX hardware signature proves VM identity β Integrity: MRTD/RTMR/IMA verify exact code version β Freshness: Nonce + timestamp prevent replay attacks β Key Binding: X25519 pubkey cryptographically bound to TDX quote
β Does NOT protect against: Supply chain attacks on the build process (use reproducible builds + code transparency for this)
RATSnest uses three layers of verification to ensure code integrity:
What it measures: TDX infrastructure (TDVF firmware + machine configuration) When it's set: At VM launch by TDX hardware What it validates: Running on legitimate GCP TDX infrastructure
// shared/policy.ts
allowed_mrtd: [
"0xc5bf87009d9aaeb2a40633710b2edab43c0b0b8cbe5a036fa45b1057e7086b0726711d0c78ed5859f12b0d76978df03c"
]MRTD is fixed per GCP machine type and does NOT vary with your application code.
What they measure: Boot-time measurements (kernel, initrd, command line) When they're set: During boot by systemd-stub What they validate: Exact code version running in the VM
// shared/policy.ts
allowed_rtmr1: [
"0x4484eea1a5ad776567a76d381d0e4233b28adab4d94e0f4c426f8761d98a6463b9dadb8ad4db878611a09ab5e0a999d2"
]RTMRs change with every code modification, providing strong code identity guarantees.
TDX provides 4 RTMRs:
- RTMR0: Usually empty
- RTMR1: Kernel + initrd + cmdline (use this for code identity)
- RTMR2: Additional boot measurements
- RTMR3: Additional boot measurements
What it measures: Runtime file integrity (SHA256 hashes of executed binaries) When it's measured: At file access/execution time What it validates: No runtime tampering of binaries
// shared/policy.ts
expected_ima_measurements: {
"/usr/bin/ratsnest": "12c7226a0a41dfd2456b4fc8eb7e547f87c6ced1a9cc18c7657d4bce550997a4",
"/usr/lib/systemd/systemd-executor": "a0e08eb8f3e086b6d28b66369db05b45915e9bb8584859a282168b1cc44ef78d",
...
}IMA provides runtime integrity independent of boot measurements. Even if an attacker modifies a binary after boot, the IMA hash won't match.
- Build image:
make image - Deploy to VM:
make deploy - Extract measurements from VM logs:
make console-output | grep "TDX ATTESTATION - MEASUREMENTS"
- Update
shared/policy.tswith new values - Rebuild frontend:
make build
# Full deployment (build image + deploy to GCP)
make deploy
# Replace existing VM with new image
REPLACE_VM=true make deploy
# Or step-by-step:
make image # Build TDX disk image
make deploy-gcp # Upload and deploy to GCP# GCP Configuration
export GCP_PROJECT=your-project-id
export GCP_ZONE=us-west1-a
export INSTANCE_NAME=ratsnest-vm
# Deployment Options
export REPLACE_VM=true # Delete and recreate VM during deploy# Get VM IP
gcloud compute instances describe ratsnest-vm \
--zone=us-west1-a \
--format='get(networkInterfaces[0].accessConfigs[0].natIP)'
# View logs
gcloud compute ssh ratsnest-vm \
--zone=us-west1-a \
-- journalctl -u ratsnest -f
# Test from local frontend
cd frontend && npm run dev
# Then open: http://localhost:5173/?backend=http://VM_IP:3000Test local frontend against deployed TDX backend:
# Terminal 1: Run local frontend
cd frontend && npm run dev
# Terminal 2: Set up Cloudflare Tunnel (for HTTPS)
cloudflared tunnel --url http://VM_IP:3000
# Browser: Open with query params
http://localhost:5173/?backend=https://your-tunnel.trycloudflare.com&mrtd=0xACTUAL_MRTDThe /debug/handshake-bytes endpoint shows cryptographic details:
curl -X POST http://localhost:3000/debug/handshake-bytes \
-H "Content-Type: application/json" \
-d '{"pubkey":"0000000000000000000000000000000000000000000000000000000000000000"}'Or use the frontend debug panel (click "π Test Handshake Computation").
ratsnest/
βββ backend/ # Deno server (TypeScript)
β βββ tunnel.ts # β TunnelServer + Express proxy (port 3000)
β β # - Initializes TunnelServer with getQuote()
β β # - Proxies HTTP requests to Hono API
β β # - Handles WebSocket /__ra__ endpoint
β βββ main.ts # β Hono API server (port 4000)
β β # - GET /api/hello - Example API endpoint
β β # - GET /api/ima/* - IMA log endpoints
β β # - Serves static frontend (SPA)
β βββ tdx.ts # β TDX quote generation
β β # - getQuote(reportData) β TDX quote bytes
β β # - Uses ConfigFS-TSM: /sys/kernel/config/tsm/report
β β # - Extracts MRTD + RTMRs for policy updates
β βββ ima.ts # β IMA runtime measurements
β β # - Reads /sys/kernel/security/ima/ascii_runtime_measurements
β β # - Parses IMA log entries
β β # - Returns binary hashes for verification
β βββ debug.ts # Debug endpoints (for development only)
β β # - POST /debug/handshake-bytes - Verify SHA-512 computation
β β # - GET /debug/ima-summary - View IMA log summary
β βββ deno.json # Deno config + build tasks
β βββ *.test.ts # Unit tests
β
βββ frontend/ # React SPA (TypeScript + Vite)
β βββ src/
β β βββ App.tsx # β Main application + TunnelClient
β β # - TunnelClient.initialize(baseUrl, { customVerifyQuote })
β β # - verifyTdxQuote() - Checks MRTD/RTMRs/IMA against policy
β β # - logAndVerifyX25519Binding() - Logs handshake details
β β # - Encrypted API calls: enc.fetch('/api/hello')
β βββ package.json # Dependencies: @teekit/tunnel, @teekit/qvl, react
β βββ vite.config.ts # Vite build configuration
β βββ dist/ # Built static files (generated by 'npm run build')
β
βββ shared/ # Shared TypeScript code (used by both frontend & backend)
β βββ policy.ts # β MRTD/RTMR/IMA policy + verification logic
β # - policy.allowed_mrtd - List of trusted MRTD values
β # - policy.allowed_rtmr1 - List of trusted RTMR1 values
β # - policy.expected_ima_measurements - Binary hash expectations
β # - verifyMeasurements() - Multi-layer verification function
β
βββ image/ # TDX image build system
β βββ build.sh # β Build script
β β # 1. Compile backend: deno task build β dist/ratsnest
β β # 2. Build UKI: mkosi β ratsnest-tdx.efi
β β # 3. Extract measurements: measured-boot β measurements.json
β β # 4. Display MRTD for policy updates
β βββ deploy-gcp.sh # β GCP deployment script
β β # 1. Upload image to GCS
β β # 2. Create GCE image
β β # 3. Create/update TDX-enabled VM instance
β βββ ratsnest/ # mkosi configuration
β β βββ mkosi.conf # Image build settings (Debian, kernel, etc.)
β β βββ mkosi.extra/ # Files to include in image
β β βββ usr/bin/ratsnest - Compiled binary (copied during build)
β βββ ratsnest.conf # Main mkosi config (references flashbots-images)
β
βββ build/ # Build artifacts (generated)
β βββ ratsnest-tdx.efi # Unified Kernel Image (UKI)
β βββ ratsnest-tdx.tar.gz # GCP-compatible disk image
β βββ measurements.json # Boot measurements (MRTD/RTMRs)
β
βββ Makefile # Build automation
# - make build: Build frontend + backend
# - make image: Build TDX disk image
# - make deploy: Build + deploy to GCP
# - make test: Run tests
1. Policy Flow (shared/policy.ts):
Build β Extract Measurements β Update policy.ts β Rebuild Frontend β Deploy
- Modified by: Developer (after extracting measurements from VM logs)
- Used by:
frontend/src/App.tsx(client verification) - Contains: MRTD, RTMRs, IMA hashes
2. Quote Generation Flow (backend/tdx.ts β backend/tunnel.ts):
TunnelServer.initialize()
β getQuote(x25519PublicKey)
β SHA-512(nonce || iat || pubkey) = report_data
β ConfigFS-TSM: /sys/kernel/config/tsm/report
β TDX hardware signs quote
β Returns quote bytes + verifier_data
3. Client Verification Flow (frontend/src/App.tsx):
WebSocket Connect
β Receive server_kx (quote + verifier_data + runtime_data)
β Parse quote with @teekit/qvl
β Extract MRTD, RTMRs, report_data
β Verify against shared/policy.ts
β Verify X25519 binding (SHA-512 computation)
β Fetch IMA log from /api/ima/log
β Verify IMA hashes against policy
β β
Tunnel established
4. Request Proxying Flow (backend/tunnel.ts β backend/main.ts):
Client: enc.fetch('/api/hello')
β Encrypt with XSalsa20-Poly1305
β Send via WebSocket
β TunnelServer (port 3000) decrypts
β Proxy to Hono API (localhost:4000)
β Hono processes /api/hello
β Response β TunnelServer encrypts
β Client decrypts β Returns JSON
5. Build & Deploy Flow:
make build
β npm run build (frontend β dist/)
β deno task build (backend β dist/ratsnest)
make image
β build.sh:
β Copy ratsnest binary to mkosi.extra/
β mkosi --profile=gcp β UKI + disk image
β measured-boot β Extract MRTD/RTMRs
β Display measurements for policy update
make deploy
β make image (if needed)
β deploy-gcp.sh:
β Upload to GCS
β Create GCE image
β Launch TDX VM
β VM boots β ratsnest service starts
β Browser connects β Verifies quote β β
Encrypted tunnel
β Malicious Cloud Provider
- Even with root access, GCP cannot decrypt your traffic (end-to-end encryption)
- Cannot modify your code without detection (MRTD/RTMR verification)
- Cannot impersonate your VM (TDX hardware signature)
β Code Tampering
- Boot-time: MRTD/RTMRs detect modified kernel/initrd
- Runtime: IMA detects modified binaries after boot
- Network: X25519 binding prevents quote reuse
β Man-in-the-Middle Attacks
- TDX quote proves VM identity
- X25519 ECDH establishes authenticated tunnel
- XSalsa20-Poly1305 provides authenticated encryption
β Rollback Attacks
- Nonce + timestamp in quote binding
- Client can enforce minimum RTMR versions
β Supply Chain Attacks
- If your build toolchain is compromised, the attacker can generate valid measurements
- Mitigation: Use reproducible builds + code transparency logs
β Side-Channel Attacks
- TDX provides memory encryption but some side channels may exist
- Mitigation: Follow TDX best practices for sensitive data handling
β Kernel Vulnerabilities
- A kernel exploit could compromise the VM
- Mitigation: Keep kernel updated, use minimal attack surface
β Physical Attacks
- TDX assumes physical security of the CPU package
- Mitigation: This is a hardware assumption (trust Intel/AMD)
RATSnest uses multiple layers of security:
- Hardware Root of Trust: Intel TDX provides memory encryption + attestation
- Boot Integrity: MRTD/RTMRs verify boot-time measurements
- Runtime Integrity: IMA verifies binary hashes at execution time
- Network Security: TLS (clientβserver) + attested tunnel (end-to-end)
- Cryptographic Binding: X25519 keys bound to TDX quotes via SHA-512
Each layer is independent - compromising one doesn't compromise the others.
This is a proof-of-concept. Do NOT use this for production...
- TEEKit: https://github.com/canvasxyz/teekit
- Flashbots images: https://github.com/flashbots/flashbots-images
- Intel TDX: https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html
- ConfigFS-TSM: https://docs.kernel.org/ABI/testing/configfs-tsm
- IMA: https://sourceforge.net/p/linux-ima/wiki/Home/
- go-configfs-tsm: https://github.com/google/go-configfs-tsm