w3hc/wulong

Privacy-preserving API

★ 1Forks 0TypeScriptGitHub ↗Compare
mlkemnestjssiwetee

README

Wulong

NestJS Test TypeScript pnpm Node.js License: GPL v3

A NestJS API designed to run inside a Trusted Execution Environment (TEE) with quantum-resistant ML-KEM-1024 encryption and Web3 authentication (SIWE), giving users cryptographic guarantees that the operator cannot access their data during processing. Optimized for Phala Network deployment.

Features

  • TEE Attestation - Cryptographic proof of code integrity
  • Web3 Authentication - SIWE (Sign-In with Ethereum)
  • Quantum-Resistant Encryption - ML-KEM-1024 (NIST FIPS 203) with multi-recipient support
    • Client-side encryption with w3pk
    • Privacy-first: clients can decrypt locally without server
    • Server-side decryption for operations (with SIWE auth)
    • See ML-KEM guide and client guide

Quick Start

Local Development (without Docker)

Mock TEE attestation - no real hardware security.

# Install dependencies
pnpm install

# Setup environment
cp .env.template .env

# Generate TLS certificates
mkdir -p secrets
openssl req -x509 -newkey rsa:4096 -keyout secrets/tls.key -out secrets/tls.cert -days 365 -nodes -subj "/CN=localhost"

# Run the dstack simulator (keys are derived from it, never set in .env)
# git clone https://github.com/Dstack-TEE/dstack && cd dstack/sdk/simulator
# ./build.sh && ./dstack-simulator
export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090

# Start development server
pnpm start:dev

# Test ML-KEM encryption (in another terminal)
pnpm test:mlkem              # Basic encryption test
pnpm test:store-access       # Full store+access flow with SIWE

Access at https://localhost:3000 (accept self-signed certificate warning)

Docker Development

Mock TEE attestation - no real hardware security.

docker compose -f docker-compose.dev.yml up

Access at https://localhost:3000

Phala Cloud (Production TEE)

# Tag a release: CI builds, pushes and attests the image, and publishes its digest
# in the release notes. Pin that digest in docker-compose.yml (see docs/DOCKER.md#releases)
git tag v0.2.0 && git push origin v0.2.0

# Deploy to Phala Cloud
phala deploy --interactive

# Verify the attestation, including that TLS terminates in the enclave
pnpm verify:attestation https://your-app-id-3000s.phala.network/chest/attestation

# Test against Phala deployment. The certificate comes from the dstack KMS CA,
# not a public CA: skip the trust store only once verify:attestation passes
NODE_TLS_REJECT_UNAUTHORIZED=0 WULONG_URL=https://your-app-id-3000s.phala.network pnpm test:store-access

Rate Limiting

Every route is rate-limited per client IP, and each route has its own counter:

Limit Default Env var
Requests per route per IP 10 THROTTLE_LIMIT
Window 60 s THROTTLE_TTL (ms)
Pending SIWE nonces 10,000 —

Past a limit, the API answers 429 Too Many Requests. Once 10,000 nonces are pending, POST /auth/nonce is rejected until some are used or expire (5 minutes); live nonces are never evicted. With TLS terminating in the enclave, the gateway forwards encrypted bytes and cannot add X-Forwarded-For, so the client IP is the gateway's and every client shares one counter. X-Forwarded-For is trusted only under the ALLOW_TLS_OUTSIDE_ENCLAVE opt-out.

Browser Clients

Authentication travels in the X-SIWE-Message and X-SIWE-Signature headers, never in cookies, so CORS runs without credentials. Browsers may call the API only from the origins listed in CORS_ORIGINS, comma-separated as scheme://host[:port], e.g. https://app.example.com,http://localhost:5173. Unset, no origin is allowed. Non-browser clients (scripts, servers, mobile apps) are unaffected by CORS.

GET /chest/access/:slot returns plaintext. Every response sends Cache-Control: no-store and Pragma: no-cache, takes at least 100 ms, and carries no fingerprinting headers. A slot the caller does not own answers 404 like a missing one. See docs/SIDE_CHANNEL_ATTACKS.md.

Storage

Secrets are stored, encrypted, in a single JSON file, read once and kept in memory. Writes are serialized within the process and saved atomically (temp file, then rename) before memory is updated. With WULONG_ANCHOR_ADDRESS set, each write is anchored on chain by the enclave's relayer wallet before it replaces the chest, and startup fails on a chest that does not match the anchor, so it cannot be rolled back. See docs/KEY_DERIVATION.md.

Setting Default Env var
Chest file <cwd>/chest.json CHEST_PATH
Maximum chest size 50 MB CHEST_MAX_BYTES (bytes)
Storage per address 1 MiB CHEST_ADDRESS_QUOTA_BYTES (bytes)
Chest anchor contract unset (no rollback protection) WULONG_ANCHOR_ADDRESS
Base RPC endpoint required with an anchor BASE_RPC_URL
Relayer balance cap 0.01 ETH RELAYER_MAX_BALANCE_WEI (wei)

docker-compose.yml keeps the chest at /app/data/chest.json on the wulong-data named volume, so it survives redeploys. A store that would push the chest past the cap is rejected with 507 Insufficient Storage, and one that would push the caller's own entries past their quota with 413 Payload Too Large. DELETE /chest/:slot removes an entry the caller stored and frees their quota. The quota is per SIWE address, and addresses are free, so the cap remains the ceiling. The lock is per process: run a single replica.

Docs

Setup & Deployment

API & Usage

Architecture & Security

  • Overview - Project overview, architecture, and security model
  • TEE Setup - dstack attestation, fail-closed startup, and how to reproduce the measurements
  • Governance - Who can allow new builds to derive the keys: Safe, timelock, releases and emergency removal
  • Side Channel Attacks - Security considerations and mitigations
  • Implementation Plan - ML-KEM development roadmap

License

GPL-3.0

Contact

Julien Béranger (GitHub)

Contributors

julienbrg

Issues