Guides

Protect a WASM module

The verifier is no_std and needs nothing from its host, so it fits in a wasm32-unknown-unknown module that imports nothing. The host passes in what the module can't read itself (the clock, the platform and the lease), and the module decides.

The SDK's examples/wasm-module/ is a complete, buildable version of everything on this page.

The module's shape

Follow the same plain ABI conFIEL's kernel uses, with no JavaScript glue, so the same file loads in an extension page, a Worker and Node:

Export
alloc(len) → ptra buffer for the host to write a request (JSON) into
fn(ptr, len) → outeach call takes the request back, frees it, and returns out, which points at [len: u32 LE][response JSON]
release(out)frees a response

Responses are {"ok":true,…} or {"ok":false,"err":n}. Here n is the error number, or 0 for bad input. Return numbers rather than SIGIL_… strings, so the binary carries none and the loader maps them back. Never panic on bad input: a trap loses the error.

Two ways to check

Per request (verify)Loaded once (Gate)
Lease arriveswith every protected requestonce, via a load export
State in the modulenonethe claims and the latest clock reading seen
Clock rollback detectionnoyes (SIGIL_CLOCK_ROLLBACK)
Fitsa backend that mints a short lease per use (conFIEL's Guardian)key-based installs driven by @sigil/sdk's unlock()

Per request

src/lib.rsrust
use sigil_lease::{Error, Keyset, verify};

static KEYS: &[u8] = include_bytes!("../sigil-keys.json");

#[derive(serde::Deserialize)]
struct SignReq<'a> { lease: &'a str, plt: &'a str, now: i64, n: &'a str, /* … */ }

fn licensed(r: &SignReq, raw_id: &str, feature: &str) -> Result<(), u8> {
    let keys = Keyset::parse(KEYS).map_err(|_| Error::Malformed as u8)?;
    let v = verify(&keys, r.lease, raw_id, r.plt, r.now).map_err(|e| e as u8)?;
    if v.claims.has(feature) { Ok(()) } else { Err(Error::NotEntitled as u8) }
}

raw_id is whatever your licenses are bound to. For an install it's the machine id the host passes. You can also bind to something the module itself holds: conFIEL binds a lease to the RSA modulus it is about to sign with, "rsa-n:" + base64url(SHA-256(N)), so a lease minted for one key can't drive another.

With a Gate

src/lib.rs (from examples/wasm-module)rust
#[derive(Deserialize)]
struct LoadReq { lease: String, id: String, plt: String, now: i64 }

pub fn load(req: &[u8]) -> Vec<u8> {
    respond((|| {
        let r: LoadReq = parse(req)?;
        with_gate(|g| {
            let v = g.load(&r.lease, &r.id, &r.plt, r.now)?;
            json(&Unlocked { ok: true, status: v.status, claims: &v.claims })
        })
    })())
}

/// Entitlement `deal`: split an e.firma-style RSA key into RED + BLUE.
pub fn split(req: &[u8]) -> Vec<u8> {
    respond((|| {
        require("deal", parse::<Now>(req)?.now)?;   // before the rest of the request is parsed
        let r: SplitReq = parse(req)?;
        json(&Value { ok: true, value: cosign::split(&r.key, &unb64(&r.rand)?)? })
    })())
}

Build and check the binary

examples/wasm-moduleconsole
$ rustup target add wasm32-unknown-unknown
$ curl -fsS https://sigil.enigmahq.sh/v1/client/apps/app_YOUR_APP_ID/keys -o sigil-keys.json
$ bash build.sh
built dist/confiel-core.wasm (185152 bytes) for app_YOUR_APP_ID

The release profile strips symbols, aborts on panic without a message, and remaps source paths. check.mjs then fails the build if the module has any import, or if its strings name a crate it was built from or a source path. The protocol's own strings (sigil+jwt, sigil-fp:v1:, claim names) and your keyset stay readable. They show that Sigil is in use, not what your module does.

Drive it from the host

The host owns the network. In a Chrome extension or Node, @sigil/sdk (in the SDK's js/) activates the key, caches the lease and renews it. A small loader adapts the raw exports to the SigilModule interface that unlock() drives, and adds now and the platform to each request:

popup.js (Chrome extension)js
import { SigilClient, chromeStorage, installId } from "./vendor/sdk/index.js";
import { loadCore } from "./vendor/confiel-core.mjs";            // examples/wasm-module/loader.mjs

const sigil = new SigilClient({
  api: "https://sigil.enigmahq.sh",
  app: "app_YOUR_APP_ID",
  platform: "chrome-extension",
  storage: chromeStorage(),
  machineId: installId(chromeStorage()),
});

const core = await loadCore(new URL("vendor/confiel-core.wasm", location.href));
await sigil.activate(licenseKey);   // once: takes a seat, caches the lease
await sigil.unlock(core);           // every start: renews at half-life, loads the lease into the module
core.split(jwk);                    // { ok: false, code: "SIGIL_NOT_LOADED" } until unlocked

The extension needs 'wasm-unsafe-eval' in its CSP and the Sigil origin in host_permissions. The .wasm ships inside the package: fetching code at run time is not allowed in the Chrome Web Store. The lease is data, so fetching it is fine.

The loader detects the platform as chrome-extension when chrome.runtime.id is set, server under Node, and browser otherwise.