// ZPWR-LICENSE — OFFLINE-FIRST SOFTWARE LICENSING

What it is

zpwr-license is self-hosted, vendor-agnostic licensing for paid native apps and plugins. Ed25519-signed license keys are verified offline inside the binary; an issuer CLI mints and manages them; an optional activation server node-locks keys to machines and enforces seats. No third-party licensing SaaS, no mandatory phone-home, and it runs at $0 in pure-offline mode.

The positioning is offline-first, not "biggest feature list." The SaaS incumbents are online-first, so the wedge here is a fully self-hosted, embeddable, zero-runtime-dependency licenser: arbitrary signed claims/metadata, key rotation, per-product / per-major-version keys, and both Rust and C/C++ embedding. The honest limits below are part of the design, not hidden by it.

  • Version 0.1.0 · MIT · workspace of three Rust crates.
  • Crypto: Ed25519 (ed25519-dalek), SHA-256 fingerprints, base64url envelopes.
  • Offline core has no async runtime and no network; the online layer is opt-in behind a feature flag.

Threat model (honest)

ThreatStopped?By what
KeygenYes, cryptographicallyEd25519 signatures. The binary embeds only the public key; forging a token needs the private key, which never ships. No symmetric secret to extract, so no valid generator can exist.
Key sharingYes, even offlineA license can be node-locked to a machine fingerprint at issue time (no server), or the activation server can bind + seat-limit it online.
Binary crackNo — not on client-side native codeWhoever controls the binary can NOP any check. Defeated only by server-gated value (content not in the binary) plus OS code-signing / notarization. The anti-tamper layer raises cost; it is not a guarantee.

Components

CrateArtifactsRole
license-corerlib + cdylib + staticlibEd25519 sign/verify of typed tokens, key codec, node-lock, anti-tamper, optional online pull client. Usable from Rust and from C/C++ via include/zpwr_license.h / .hpp. Offline, no async, unit-tested.
license-clizpwr-licenseIssuer CLI — keygen, issue, verify, revoke, manifest, blacklist/unblock, anti-tamper helpers. Holds the private key; runs offline.
license-serverzpwr-license-serverActivation — node-lock, seat enforcement, signed entitlements, CRL. Synchronous tiny_http, no tokio.

Token model

A token is <typ>.<base64url(payload)>.<base64url(signature)>; the signature covers "<typ>.<payload>", so the type tag is part of the signed bytes and a license can never be replayed where an entitlement is expected. There are five signed token types:

typIssued byPayload
licenseissuer (offline)id, product, major, customer, issued, optional expires/features/seats/fingerprint, arbitrary metadata. Verified inside the product binary.
entitlementserverlicense_id, product, major, machine fingerprint, features, issued, expires. Machine-bound and short-lived (default 30 days); the client caches it, verifies offline each launch, and honors a grace window past expiry.
crlissuerissued, revoked[] — a signed revocation list publishable as a static file.
unblockissuerlicense_id, issued — the vendor's authorization to clear a tamper blacklist. Only a vendor-signed unblock can lift a blacklist.
manifestissuerissued, optional min_version, revoked[], disabled_features[], message — the $0 remote-control channel.

Two-key design: the issuer key signs licenses and stays offline; the server holds a separate entitlement key plus the license public key. A server breach leaks only the entitlement key — which mints short-lived, machine-bound entitlements, never licenses.

Mint a license (offline, no server)

cargo build --release

./target/release/zpwr-license keygen --out issuer
./target/release/zpwr-license issue \
  --product audio-haxor --major 2 \
  --customer buyer@example.com \
  --features pro,vst3 --seats 3 \
  --key issuer.key

./target/release/zpwr-license verify --token "<token>" --pubkey issuer.pub

That alone kills keygens and works air-gapped. The activation server is opt-in.

Issuer CLI reference

zpwr-license <command>. The signing (private) key never leaves the machine the CLI runs on. Every command runs offline; the fetch-* commands pull a signed file over HTTP and verify the signature before trusting a byte.

CommandKey flagsDoes
keygen--out (default zpwr-license)Generate an Ed25519 keypair → <name>.key (private, 0600 on Unix) + <name>.pub (embed in products).
pubkey--keyDerive and print the public key from a private key file.
issue--product --major --customer [--expires-days --features --seats --id --fingerprint --meta KEY=VALUE] --keyMint a signed license token to stdout. --meta is repeatable for arbitrary signed claims; --fingerprint node-locks the license offline.
verify--token --pubkey [--fingerprint]Verify a license, print its JSON, exit non-zero on failure. With --fingerprint, also enforce the node-lock against that raw machine id.
machine-idPrint this machine's raw hardware id for offline node-locking.
sign-binary--file --keySign a release binary's SHA-256 so the product can self-verify integrity (antitamper::verify_self). Prints the base64url signature to embed.
tamper-scanRun the local anti-tamper scan and print the JSON report.
blacklist--idPermanently blacklist a license id on this machine (tamper response).
blacklisted--idCheck whether a license id is blacklisted on this machine; exits 0 if blacklisted, 1 if clean.
unblock--id --keyMint a signed unblock token (issuer side) to email a customer for false-positive recovery.
clear--token --pubkeyApply a signed unblock token on this machine: verify it and clear that id from the local blacklist.
fetch-crl--url --pubkeyPull + verify a signed CRL from a free static host and print the revoked ids.
manifest[--min-version --revoke --disable --message --out] --keyBuild + sign the $0 remote-control manifest (default out manifest.sig).
fetch-manifest--url --pubkey [--current-version]Pull + verify a signed manifest; with --current-version, report whether an upgrade is forced.
revoke[--store --add --out] --keyAdd ids to the revocation list and emit a freshly signed CRL token. Plaintext --store (default crl.json) is the editable source of truth; signed --out (default crl.sig) is what you publish.

Offline node-locking

Bind a key to a machine with no server — the classic machine-id → keyfile activation:

  1. The customer runs zpwr-license machine-id (or your app shows it) and sends you the value.
  2. You mint a license locked to it: issue --fingerprint <machine-id>.
  3. The binary verifies the signature and that the bound fingerprint matches the current machine (verify_license_locked / zpwr_license_verify_license_locked).

The machine id is the real hardware identity — macOS IOPlatformUUID (direct IOKit FFI, no ioreg subprocess to shadow), Windows MachineGuid, Linux /etc/machine-id — not a mutable hostname. It is stored hashed (salted SHA-256, salt = product name), so the customer's raw id never travels in cleartext or is recoverable from the license.

Tamper detection (cost layer)

A cost layer, embedded via license-core::antitamper. Every check runs inside the binary, so a determined attacker can patch any single one out. Use many, from several code paths, and fail late and subtly. This raises the cost and filters casual piracy; it is not a guarantee. The hard defense remains server-gated value.

CheckDetects
debugger_present()An attached debugger (Linux TracerPid, macOS P_TRACED via sysctl).
injection_detected()LD_PRELOAD / LD_AUDIT / DYLD_INSERT_LIBRARIES library injection.
env_suspicious()Loader/allocator debug env (LD_DEBUG, DYLD_PRINT_*, MallocStackLogging, …).
timing_anomaly()A tight loop running far too slow — single-stepping.
verify_self(sig, vk)On-disk patching of the shipped binary (Ed25519 over its hash; sign with sign-binary).
self_test(vk, token)Public-key substitution — a baked-in known-good token stops verifying if the embedded key was swapped.
clock_rolled_back(last, now)A clock rollback used to revive an expired entitlement.
arm_anti_debug()Actively blocks debugger attach (macOS PT_DENY_ATTACH, Linux PTRACE_TRACEME). Call once, early.
binding_seed(token)Woven check — a value only a valid license reproduces. Weave it into real computation so NOP-ing the gate isn't enough.
guarded_seed(token)Tamper-coupled woven check — returns a wrong value under debugger/injection, so analysis silently corrupts results instead of raising a catchable failure.
should_recheck(last, now, interval)Whether a periodic re-verification is due, so there is no single launch-time gate to neutralize.
tamper_tripwire(id)Detect-and-brick — on any tamper signal, blacklists the license id permanently (hashed, to several durable locations) and returns true.
scan()Runs the environmental checks and returns a TamperReport.

Obfuscation: the telltale string literals are XOR-encoded and decoded at runtime, so strings on the stripped release binary shows nothing that flags a license check. The C-ABI exports are named innocuously — zpwr_rt_state (debugger), zpwr_rt_metrics (scan), zpwr_rt_check (key-substitution self-test) — so nm doesn't advertise the anti-tamper layer either. This is source-level obfuscation: it defeats casual static triage, not a disassembler that reads the syscalls.

False-positive risk is real: a legitimate debugger session can brick the license via tamper_tripwire, so reserve it for high-value products and ship a support unblock path (unblockclear).

Activation server

zpwr-license-server is the opt-in stateful layer. It holds the license public key and a separate entitlement signing key — never the license private key — so a full server compromise leaks only short-lived, machine-bound entitlements.

./target/release/zpwr-license keygen --out entitlement

ZPWR_LICENSE_LICENSE_PUBKEY="$(cat issuer.pub)" \
ZPWR_LICENSE_ENTITLEMENT_KEY="$(cat entitlement.key)" \
./target/release/zpwr-license-server          # 127.0.0.1:8787

curl -X POST localhost:8787/activate \
  -d '{"token":"<license token>","fingerprint":"<machine hash>"}'
# -> { "entitlement": "<entitlement token>", "expires": <unix> }
EndpointDoes
GET /health{"status":"ok"}
GET /crlServe the configured CRL (issued + revoked); 404 if none configured.
POST /activateVerify the license, enforce expiry + revocation + the seat count, bind the fingerprint, return a signed entitlement.
POST /validateVerify an entitlement and report revocation status.
Env varDefaultMeaning
ZPWR_LICENSE_ADDR127.0.0.1:8787Listen address.
ZPWR_LICENSE_LICENSE_PUBKEYrequiredbase64url license public key.
ZPWR_LICENSE_ENTITLEMENT_KEYrequiredbase64url entitlement signing key.
ZPWR_LICENSE_SEATS_FILEseats.jsonSeat-store path.
ZPWR_LICENSE_CRLPath to a signed CRL token (verified with the license pubkey).
ZPWR_LICENSE_TTL_DAYS30Entitlement lifetime.

Integrating a product

The core builds as an rlib (Rust), a staticlib (libzpwr_license.a) and a cdylib (libzpwr_license.{dylib,so}), so it drops into both stacks.

Rust apps (Tauri backends)

use zpwr_license::{decode_verifying_key, verify_license_locked, machine_id, antitamper};

const PUBKEY: &str = "…base64url public key…"; // baked in, not secret

let vk = decode_verifying_key(PUBKEY).unwrap();
match verify_license_locked(&token, &vk, &machine_id()) {
    Ok(lic) => unlock(&lic.features),
    Err(_)  => run_free_tier(),
}
if antitamper::scan().any() { /* fail late + subtly */ }

Full example: crates/license-core/examples/app_gate.rs. Use verify_license_any / verify_license_any_locked with a keyring of public keys to support key rotation — the first key that validates wins, so old licenses keep verifying after you issue under a new key.

C++ plugins (JUCE)

#include "zpwr_license.hpp"
static const char* kPubKey = "…base64url public key…";   // baked in, not secret

if (!zpwr::license::ok(token, kPubKey, zpwr::license::machine_id()))
    runFreeTier();                                        // node-locked, offline
if (zpwr::license::tamper::debugger()) { /* fail late */ }
target_include_directories(MyPlugin PRIVATE ${ZPWR}/include)
target_link_libraries(MyPlugin PRIVATE ${ZPWR}/target/release/libzpwr_license.a)
# macOS: also -framework IOKit -framework CoreFoundation -framework Security
# Linux: pthread dl m

The raw C ABI (include/zpwr_license.h) exposes zpwr_license_ok, zpwr_license_verify_license[_locked], zpwr_license_machine_id, zpwr_license_verify_manifest, zpwr_license_unblock, zpwr_license_free_cstring, and the innocuously named anti-tamper calls zpwr_rt_state / zpwr_rt_metrics / zpwr_rt_check / zpwr_rt_arm / zpwr_rt_seed / zpwr_rt_gseed / zpwr_rt_tripwire / zpwr_rt_blacklisted.

Online layer at $0 (no paid hosting)

Global revocation/blacklist without paying for a server. The online layer (Cargo feature online, blocking ureq) is pull-based from a free static host — the file is Ed25519-signed, so the host is untrusted.

zpwr-license revoke --add lic-bad --key issuer.key --out crl.sig
# commit crl.sig to a public repo / gist -> served free by GitHub raw,
# jsDelivr, Cloudflare Pages, Netlify, an S3 bucket, …
use zpwr_license::online::is_revoked_online;
// fail-open (keep working) on fetch error: pass `false`
if is_revoked_online(CRL_URL, &lic.id, &vk, false) { run_free_tier(); }

Signed manifest — $0 remote control. One signed file is a vendor control channel. min_version is a force-upgrade kill-switch (Manifest::requires_upgrade): bump it and every older copy — including cracked releases — must refuse to run. Plus revoked, disabled_features, and a broadcast message, all in one pull. Anti-replay: clients keep the newest issued and reject a stale manifest (fetch_manifest_fresh), so a hostile host can't replay a pre-revocation copy.

Only live seat-counting genuinely needs a stateful endpoint — and that ships too, on the free tier: serverless/cloudflare/ is a Cloudflare Workers activation function (100k req/day free + free KV), wire-compatible with the Rust core (WebCrypto Ed25519). It serves /health, /activate, /validate, /checkout, /heartbeat, /checkin, /blacklist, and /unblock — the /checkout / /heartbeat / /checkin trio implements floating/concurrent licensing via TTL leases (a crashed client's seat frees when its lease expires).

Build & test

The repo is private and does not run GitHub Actions; CI is local only. scripts/ci.sh is the source of truth — run it before every push.

make ci        # fmt + clippy(-D warnings) + check + test + doc + build
make build     # cargo build
make test      # cargo test --workspace
make clippy    # cargo clippy --all-targets -- -D warnings

Status & honest limits

Generic and shipping today: arbitrary signed claims (metadata), key rotation via keyring, multi-product / multi-key / per-major-version, offline node-locking to the real hardware id, CRL, Rust + C/C++ embedding, obfuscated anti-tamper with active anti-debug and woven-check seeds, free-tier floating-license activation.

Roadmap (not yet built): a trial model, more SDKs over the C ABI (C#/.NET, Python, Node, Swift), token-format versioning + migration, and a semver/release/support commitment before another company depends on it.

Honest limits: true floating/concurrent licensing needs the online server — offline can't count across machines. Client-side checks are patchable; server-gated value is the only hard stop. Offline-first, not uncrackable.

Catalog

Licenses the paid app-store catalog — per product, per major version:

See the engineering report for the cryptographic design rationale.