Skip to content

Privasys/wasm-app-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WASM App Example

A WebAssembly Component that exercises all major capabilities of the Enclave OS (Mini) WASM runtime. Designed as a reference implementation and integration test for validating new releases.

Six exported functions cover every host import available to WASM apps running inside an SGX enclave:

# Function WASI Interface What it tests
1 hello (none) Smoke test — pure guest code, no host imports
2 get-random wasi:random Hardware RNG (RDRAND inside SGX)
3 get-time wasi:clocks/wall-clock Wall clock via OCALL
4 kv-store wasi:filesystem Write to sealed KV store
5 kv-read wasi:filesystem Read from sealed KV store
6 fetch-headlines privasys:enclave-os/https HTTPS egress (TLS inside SGX)

Quick Start

Prerequisites

Tool Version Install
Rust stable 1.82+ rustup update stable
WASI target rustup target add wasm32-wasip2
cargo-component latest cargo install cargo-component

Build

cargo component build --release

Output: target/wasm32-wasip1/release/wasm_example.wasm

Pre-compile (optional)

For faster load times you can AOT-compile to .cwasm using Wasmtime:

wasmtime compile target/wasm32-wasip1/release/wasm_example.wasm -o wasm_example.cwasm

Run the integration tests

With the enclave running (see Deployment):

python tests/test_wasm_functions.py wasm_example.cwasm
================================================================
  Enclave OS (Mini) - WASM Integration Test Suite
================================================================

  Connecting to 127.0.0.1:8443 ...
  Connected: TLSv1.3, TLS_AES_256_GCM_SHA384

  Loading wasm_example.cwasm ...
  Loaded in 0.12s

  ------------------------------------------------------------
    #  Test                 Result  Details
  ------------------------------------------------------------
    1  Hello World          ✔  Hello, World!
    2  Random Number        ✔  45
    3  Wall Clock           ✔  1772364814.000000000
    4  KV Store (write)     ✔  greeting = 'Hello from WASM in SGX!'
    5  KV Store (read)      ✔  Hello from WASM in SGX!
    6  HTTPS Egress         ✔  9 headlines — 1. Guides d'achat
  ------------------------------------------------------------

  ✔ Result: ALL 6 TESTS PASSED

How it works

WASM inside SGX

Enclave OS (Mini) embeds a Wasmtime runtime inside the SGX enclave. WASM apps are:

  1. Compiled externally (this project) into a Component Model .wasm
  2. Pre-compiled to .cwasm (AOT) with Wasmtime — no Cranelift inside SGX
  3. Loaded dynamically at runtime via wasm_load over the RA-TLS wire protocol — the enclave starts empty, no apps are compiled in
  4. Attested automatically — the SHA-256 of the WASM bytecode is embedded in the config Merkle tree and in every RA-TLS certificate (OID 1.3.6.1.4.1.65230.2.3)
  5. Called over RA-TLS via JSON wasm_call envelopes
  6. Executed statelessly — each call gets a fresh instance with a 10 million instruction fuel budget
  7. Managed at runtime — apps can be listed and unloaded without restarting the enclave

Wire protocol

All communication happens over a single RA-TLS connection using length-delimited JSON frames (4-byte big-endian length prefix).

Call parameters are typed JSON values:

{"type": "string", "value": "hello"}
{"type": "u32", "value": 42}

Dynamic WASM Loading

The enclave starts empty — no WASM apps are compiled in. Apps are loaded, called, and managed entirely at runtime over the RA-TLS wire protocol.

Lifecycle

 ┌───────────────┐     RA-TLS     ┌───────────────────────────┐
 │  Client       │◄──────────────►│  SGX Enclave              │
 │  (ra-tls-cli) │                │                           │
 │               │  wasm_load ──► │  1. SHA-256 code hash     │
 │               │                │  2. Deserialize (AOT)     │
 │               │                │  3. Introspect exports    │
 │               │  ◄── loaded    │  4. Register app          │
 │               │                │  5. Embed hash in cert    │
 │               │  wasm_call ──► │                           │
 │               │  ◄── result    │  Fresh instance per call  │
 │               │                │  (stateless, fuel-limited)│
 └───────────────┘                └───────────────────────────┘

1. Load — wasm_load

Send the pre-compiled .cwasm bytecode to the enclave. The enclave:

  1. Hashes the bytecode (SHA-256) — this becomes the app's identity
  2. Deserializes the AOT artifact (no Cranelift compilation inside SGX)
  3. Introspects the component to discover all exported functions
  4. Generates an AES-256 encryption key via RDRAND for the app's KV store (or accepts a caller-supplied key — see BYOK)
  5. Registers the app under the given name
  6. Re-derives the RA-TLS certificate with the new code hash in the config Merkle tree (OID 1.3.6.1.4.1.65230.2.3)

Request:

{
  "wasm_load": {
    "name": "test-app",
    "bytes": [0, 97, 115, 109, ...],
    "hostname": "app.enclave.example.com"
  }
}
Field Required Description
name yes App identifier — used in subsequent wasm_call requests
bytes yes Raw .cwasm bytecode as a JSON integer array
hostname no SNI hostname for a dedicated per-app TLS certificate (defaults to name)
encryption_key no Hex-encoded 32-byte AES-256 key for KV store (BYOK)

Response:

{
  "status": "loaded",
  "app": {
    "name": "test-app",
    "hostname": "app.enclave.example.com",
    "code_hash": "a1b2c3d4...64hex",
    "key_source": "generated",
    "exports": [
      {"name": "hello", "param_count": 0, "result_count": 1},
      {"name": "kv-store", "param_count": 2, "result_count": 1}
    ]
  }
}

The code_hash is the SHA-256 of the loaded bytecode. Remote clients can verify this value in the RA-TLS certificate to confirm exactly what code is running inside the enclave.

2. Call — wasm_call

Invoke an exported function on a loaded app. Each call creates a fresh WASM instance (stateless execution) with a fuel budget of 10 million instructions.

Request:

{
  "wasm_call": {
    "app": "test-app",
    "function": "hello",
    "params": []
  }
}

Response:

{
  "status": "ok",
  "returns": [{"type": "string", "value": "Hello, World!"}]
}

With parameters:

{
  "wasm_call": {
    "app": "test-app",
    "function": "kv-store",
    "params": [
      {"type": "string", "value": "greeting"},
      {"type": "string", "value": "Hello from WASM in SGX!"}
    ]
  }
}

3. List — wasm_list

Query all currently loaded apps with metadata.

Request:

{"wasm_list": {}}

Response:

{
  "status": "apps",
  "apps": [
    {
      "name": "test-app",
      "hostname": "app.enclave.example.com",
      "code_hash": "a1b2c3d4...64hex",
      "key_source": "generated",
      "exports": [
        {"name": "hello", "param_count": 0, "result_count": 1},
        {"name": "get-random", "param_count": 0, "result_count": 1},
        {"name": "get-time", "param_count": 0, "result_count": 1},
        {"name": "kv-store", "param_count": 2, "result_count": 1},
        {"name": "kv-read", "param_count": 1, "result_count": 1},
        {"name": "fetch-headlines", "param_count": 0, "result_count": 1}
      ]
    }
  ]
}

4. Unload — wasm_unload

Remove an app from the enclave. The in-memory encryption key is destroyed, making any KV data written with a generated key permanently unrecoverable.

Request:

{"wasm_unload": {"name": "test-app"}}

Response:

{"status": "unloaded", "name": "test-app"}

Bring Your Own Key

By default, the enclave generates a random AES-256 encryption key per app via RDRAND. The key lives only in enclave memory — if the app is unloaded, the key and any data encrypted with it are gone forever.

For persistent data across app reloads, supply an encryption_key during wasm_load:

{
  "wasm_load": {
    "name": "my-app",
    "bytes": [0, 97, 115, 109, ...],
    "encryption_key": "a1b2c3d4e5f6...64hex"
  }
}

The key must be exactly 32 bytes (64 hex characters). The enclave will use this key for all KV store operations for this app. The key_source field in the response will report "byok" instead of "generated".

Python example (complete)

import json, socket, ssl, struct

def frame(data):
    return struct.pack(">I", len(data)) + data

# Connect via RA-TLS
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE  # or verify against RA-TLS CA
sock = ctx.wrap_socket(
    socket.create_connection(("127.0.0.1", 8443)),
    server_hostname="127.0.0.1",
)

# Load the WASM app
with open("wasm_example.cwasm", "rb") as f:
    wasm_bytes = list(f.read())

load_req = json.dumps({
    "wasm_load": {"name": "my-app", "bytes": wasm_bytes}
}).encode()
sock.sendall(frame(json.dumps({"Data": list(load_req)}).encode()))

# Read response (simplified — production code should handle partial reads)
buf = sock.recv(65536)
length = struct.unpack(">I", buf[:4])[0]
resp = json.loads(buf[4:4+length])
inner = json.loads(bytes(resp["Data"]))
print("Loaded:", json.dumps(inner, indent=2))

# Call a function
call_req = json.dumps({
    "wasm_call": {"app": "my-app", "function": "hello", "params": []}
}).encode()
sock.sendall(frame(json.dumps({"Data": list(call_req)}).encode()))

buf = sock.recv(65536)
length = struct.unpack(">I", buf[:4])[0]
resp = json.loads(buf[4:4+length])
result = json.loads(bytes(resp["Data"]))
print("Result:", json.dumps(result, indent=2))
# {"status": "ok", "returns": [{"type": "string", "value": "Hello, World!"}]}

sock.close()

Test API reference

1. hello — Smoke test

{"wasm_call": {"app": "test-app", "function": "hello", "params": []}}

Returns: "Hello, World!"

No host imports. Validates the end-to-end path: RA-TLS → wire protocol → WASM instantiation → guest code → response.

2. get-random — Hardware RNG

{"wasm_call": {"app": "test-app", "function": "get-random", "params": []}}

Returns: integer in [1, 100]

Uses wasi:random/random.get-random-bytes(4) → maps to RDRAND inside SGX. Different value on each call.

3. get-time — Wall clock

{"wasm_call": {"app": "test-app", "function": "get-time", "params": []}}

Returns: "<seconds>.<nanoseconds>" (e.g. "1772364814.000000000")

Uses wasi:clocks/wall-clock.now() → OCALL to host for current UNIX time.

4. kv-store — Sealed KV write

{
  "wasm_call": {
    "app": "test-app",
    "function": "kv-store",
    "params": [
      {"type": "string", "value": "greeting"},
      {"type": "string", "value": "Hello from WASM in SGX!"}
    ]
  }
}

Returns: "stored: greeting"

Opens a "file" via wasi:filesystem, writes the value, and calls sync-data() to flush encrypted data to the host-side sealed KV store.

5. kv-read — Sealed KV read

{
  "wasm_call": {
    "app": "test-app",
    "function": "kv-read",
    "params": [{"type": "string", "value": "greeting"}]
  }
}

Returns: "Hello from WASM in SGX!" (the value stored by kv-store)

Data persists across calls and enclave restarts (same MRENCLAVE required). Each app is namespace-isolated: app:<name>/fs:<path>.

6. fetch-headlines — HTTPS egress

{"wasm_call": {"app": "test-app", "function": "fetch-headlines", "params": []}}

Returns: numbered list of Le Monde headlines (up to 10)

Uses privasys:enclave-os/https.fetch() to make an HTTPS GET request. TLS 1.3 terminates inside the enclave using rustls + Mozilla root CAs. The host only sees encrypted TCP bytes.


Connecting with RA-TLS clients

Use ra-tls-clients to connect, verify attestation, and interact with the enclave.

Go CLI (recommended)

cd ra-tls-clients/go
go run . --host <server> --port 443

The CLI verifies the RA-TLS certificate (DCAP quote + ReportData binding) then drops into an interactive session where you can send JSON commands.

Python

from ratls_client import RaTlsClient

client = RaTlsClient("server.example.com", 443, ca_cert="path/to/ca.crt")
client.connect()

# Load the WASM app
with open("wasm_example.cwasm", "rb") as f:
    wasm_bytes = list(f.read())
client.send({"wasm_load": {"name": "test-app", "bytes": wasm_bytes}})
result = client.recv()

# Call a function
client.send({"wasm_call": {"app": "test-app", "function": "hello", "params": []}})
result = client.recv()
print(result)  # {"status": "ok", "returns": [{"type": "string", "value": "Hello, World!"}]}

Available clients: Go, Python, Rust, TypeScript, C# (.NET) — see ra-tls-clients for details.


Deployment

Building Enclave OS with WASM support

See the Enclave OS (Mini) README for full build instructions. The WASM runtime is enabled by building with the wasm-enclave composition crate:

cd enclave-os-mini
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_WASM=ON -DWASM_ENCLAVE_DIR=/path/to/wasm-app-example/enclave
make -j$(nproc)

This produces enclave-os-host and enclave-os-enclave.signed.so in build/bin/.

Running the enclave

cd build/bin
./enclave-os-host -p 8443

The enclave listens on 0.0.0.0:8443 for RA-TLS connections. WASM apps are loaded dynamically — no apps need to be present at build time.

Production: Layer 4 proxy

The enclave terminates TLS internally — a front-end load balancer must operate at Layer 4 (TCP passthrough). See the Layer 4 Proxy Guide for Caddy (with caddy-l4) and HAProxy configurations.

For cloud-specific setup instructions, see:


Project structure

wasm-example/
├── Cargo.toml                 # cdylib crate, depends on wit-bindgen-rt
├── README.md                  # This file
├── src/
│   ├── lib.rs                 # 6 exported functions (~210 lines)
│   └── bindings.rs            # Auto-generated by wit-bindgen (do not edit)
├── tests/
│   └── test_wasm_functions.py # Integration test suite (all 6 functions)
├── install/
│   └── ovh-sgx.md             # OVH bare metal SGX deployment guide
└── wit/
    ├── world.wit              # Component world (imports + exports)
    └── deps/
        ├── clocks/            # wasi:clocks@0.2.0
        ├── enclave-os/        # privasys:enclave-os@0.1.0 (HTTPS)
        ├── filesystem/        # wasi:filesystem@0.2.0
        ├── io/                # wasi:io@0.2.0
        └── random/            # wasi:random@0.2.0

WIT interfaces are copied from the Enclave OS WASM SDK. The full SDK includes additional interfaces (cli, sockets, crypto, keystore) not used by this example.


Expected results

Function Expected output Validation
hello "Hello, World!" Exact string match
get-random Integer [1, 100] Range check, varies per call
get-time "<unix_ts>.000000000" Valid recent UNIX timestamp
kv-store("k","v") "stored: k" Exact match
kv-read("k") "v" Returns previously stored value
kv-read("missing") "error: key not found: missing" Error message
fetch-headlines Numbered list (1-10 items) At least 2 headlines

Security notes

  • Stateless execution: Each call creates a fresh WASM instance. KV data persists via sync-data() → sealed KV store on the host.
  • Namespace isolation: Each app's files and keys are prefixed with app:<name>/ — apps cannot access each other's data.
  • HTTPS only: The egress interface only supports https:// URLs — http:// requests are rejected. The host never sees plaintext.
  • Attestation: The SHA-256 hash of the loaded WASM bytecode is embedded in the RA-TLS certificate, allowing remote clients to verify exactly what code is running.

License

This project is licensed under the GNU Affero General Public License v3.0.

About

No description, website, or topics provided.

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors