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) → ptr | a buffer for the host to write a request (JSON) into |
fn(ptr, len) → out | each 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 arrives | with every protected request | once, via a load export |
| State in the module | none | the claims and the latest clock reading seen |
| Clock rollback detection | no | yes (SIGIL_CLOCK_ROLLBACK) |
| Fits | a backend that mints a short lease per use (conFIEL's Guardian) | key-based installs driven by @sigil/sdk's unlock() |
Per request
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
#[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
$ 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_IDThe 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:
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 unlockedThe 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.