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::GraceRuns 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
| Field | Type | Meaning |
|---|---|---|
aud | String | application id |
sub / jti | String | license id / activation id |
iat, nbf, exp | i64 | issued, not before, expires (Unix seconds) |
gr | i64 | grace after exp, seconds |
lex | i64 | license term end, 0 = perpetual |
lty | String | lease (online) or offline (air-gapped) |
fp, plt | String | machine fingerprint, platform |
ent | Vec<String> | entitlements |
lic | String | licensee 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.
| # | Code | When |
|---|---|---|
| 1 | SIGIL_MALFORMED | not three segments, bad header, alg ≠ EdDSA, bad payload, wrong sv/iss, no exp |
| 2 | SIGIL_UNKNOWN_KEY | kid not in the embedded keyset (rotated and not rebuilt?) |
| 3 | SIGIL_BAD_SIGNATURE | the lease was edited, or signed by another application |
| 4 | SIGIL_WRONG_APP | aud is another application |
| 5 | SIGIL_WRONG_MACHINE | the lease is for a different raw_id |
| 6 | SIGIL_WRONG_PLATFORM | plt ≠ the platform the host reports |
| 7 | SIGIL_NOT_YET_VALID | now + skew before nbf/iat |
| 8 | SIGIL_EXPIRED | past exp + gr, or past lex |
| 9 | SIGIL_NOT_ENTITLED | the feature isn't in ent |
| 10 | SIGIL_CLOCK_ROLLBACK | the clock went back while a Gate held a lease |
| 11 | SIGIL_NOT_LOADED | require/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 licenseMachine 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
$ 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"}