Reference

Rust SDK

Two crates. sigil-lease is the enforcement: it decides whether protected code may run. sigil-client is convenience for native hosts: it gets and renews the lease that sigil-lease checks.

sigil-lease

no_std + alloc, #![forbid(unsafe_code)]. Dependencies: ed25519-compact (no default features, no dependencies of its own), sha2, base64, serde, serde_json. Feature std adds the system clock.

Keyset

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

let keyset = Keyset::parse(KEYS)?;    // Result<Keyset, KeysetError>: a build error, in practice
keyset.app();                         // "app_…": every accepted lease must name it in `aud`

verify: stateless

pub fn verify(keyset: &Keyset, token: &str, raw_id: &str, platform: &str, now: i64)
    -> Result<Verified, Error>;

pub struct Verified { pub claims: Claims, pub status: Status }   // Status::Valid | Status::Grace

Runs the protocol's ten checks in order: shape and header, key, signature (nothing in the payload is used before this), spec and issuer, application, machine (fp == fingerprint(app, raw_id)), platform, not-before (300 s skew), license term, then validity or grace. It doesn't check entitlements. Use claims.has(feature), which is true for an exact match or "*". verify_with_skew takes a different skew.

Gate: a loaded lease

let mut gate = Gate::new(keyset);                       // .with_skew(secs) to change the 300 s default
gate.load(token, raw_id, platform, now)?;               // verify and hold; a failed load keeps the previous lease
gate.require("cosign", now)?;                           // per protected call: window + entitlement ("" = any valid lease)
let (claims, status) = gate.status(now)?;               // re-evaluate without a feature
gate.unload();

// with feature "std":
gate.load_now(token, raw_id, platform)?;
gate.require_now("cosign")?;
let now: i64 = sigil_lease::now();

The gate remembers the latest now it has seen. A reading more than the skew earlier fails with SIGIL_CLOCK_ROLLBACK, so winding the clock back doesn't revive an expired lease.

Claims

FieldTypeMeaning
audStringapplication id
sub / jtiStringlicense id / activation id
iat, nbf, expi64issued, not before, expires (Unix seconds)
gri64grace after exp, seconds
lexi64license term end, 0 = perpetual
ltyStringlease (online) or offline (air-gapped)
fp, pltStringmachine fingerprint, platform
entVec<String>entitlements
licStringlicensee display name

fingerprint

sigil_lease::fingerprint("app_demo", "install-123")
// "3oQyFQrW_5f_3eBFIiP4w0CRrhY6SOIEraP_iScxxPI"  (the shared test vector)

Errors

Error is a fieldless enum with a stable number (e as u8, e.number(), Error::from_number(n)), a code (e.code()) and a message (e.message()). A module that wants no readable strings returns only the number; unused code()/message() are dropped by LTO.

#CodeWhen
1SIGIL_MALFORMEDnot three segments, bad header, alg ≠ EdDSA, bad payload, wrong sv/iss, no exp
2SIGIL_UNKNOWN_KEYkid not in the embedded keyset (rotated and not rebuilt?)
3SIGIL_BAD_SIGNATUREthe lease was edited, or signed by another application
4SIGIL_WRONG_APPaud is another application
5SIGIL_WRONG_MACHINEthe lease is for a different raw_id
6SIGIL_WRONG_PLATFORMplt ≠ the platform the host reports
7SIGIL_NOT_YET_VALIDnow + skew before nbf/iat
8SIGIL_EXPIREDpast exp + gr, or past lex
9SIGIL_NOT_ENTITLEDthe feature isn't in ent
10SIGIL_CLOCK_ROLLBACKthe clock went back while a Gate held a lease
11SIGIL_NOT_LOADEDrequire/status on an empty Gate

sigil-client

For native programs. It does HTTPS through ureq + rustls, and keeps its state in one file per application, mode 0600.

use sigil_client::{Client, Config};

let cfg = Config::new("https://sigil.enigmahq.sh", keyset.app(), "/var/lib/my-engine")
    .label("engine-host-01")          // default: hostname
    .app_version("1.4.0")
    .platform("server");              // default
let client = Client::new(cfg);

client.activate("SGL-…")?;            // POST /v1/client/activate; takes or renews this machine's seat
client.unlock(&mut gate)?;            // cached lease (renewed past half-life if online) → gate.load
client.current();                     // Option<State>, no network
client.fingerprint()?;                // for air-gapped activation
client.import_offline_lease(token)?;  // a lease issued from the portal
client.deactivate(Some(&mut gate))?;  // release the seat, forget the license

Machine id: Config::machine_id if set, else SIGIL_MACHINE_ID, else the OS's id (/etc/machine-id, macOS IOPlatformUUID, Windows MachineGuid), else a random id stored in the state directory. Only its fingerprint is sent.

Renewal: unlock renews once now ≥ refresh_after (half the lease). If the network is down it carries on with the cached lease, and the gate decides whether that's still inside exp + grace. A refusal of LICENSE_REVOKED, LICENSE_SUSPENDED, LICENSE_EXPIRED, LICENSE_NOT_FOUND or PLATFORM_NOT_ALLOWED deletes the cache.

Errors: sigil_client::Error { code, message, status }. The code is an API code (see API errors), a gate code (SIGIL_…), or local: NOT_ACTIVATED, NETWORK, STATE.

sigil-verify

Support: why does this install refuse?console
$ cargo run -q -p sigil-verify -- sigil-keys.json lease.txt <machine-id> --platform server
{"ok":false,"code":"SIGIL_WRONG_MACHINE","message":"lease was issued for a different machine"}