// ZPWR-LICENSE — ENGINEERING REPORT

Signed envelopes

Every token is a typed, Ed25519-signed envelope: <typ>.<base64url(payload)>.<base64url(signature)>. The signature is computed over "<typ>.<payload>", so the type tag is part of the signed bytes. A license token therefore cannot be replayed where an entitlement is expected even though both verify under related keys — type confusion is rejected by construction, and pinned by a unit test.

Why keygens are impossible (and cracks are not)

Verification uses an asymmetric public key embedded in the product. A keygen would need the private key, which never leaves the issuer machine — there is no symmetric secret in the binary to recover, so no valid generator can exist. That is a cryptographic guarantee. Patching the binary to bypass verification is a different attack the crypto cannot address: whoever controls the binary can NOP the check. The report does not pretend otherwise. The mitigation is to make the valuable payload live server-side, delivered on activation, so a patched client runs hollow.

Two-key activation

The issuer key signs licenses and stays offline. The activation server holds a separate entitlement signing key and the license public key. On POST /activate it verifies the license with the public key, enforces revocation and the seat count, binds the machine fingerprint, and returns a short-lived entitlement signed with the entitlement key. A full server compromise leaks only that key — capable of minting expiring, machine-bound entitlements, never licenses. The flow is pinned end to end by an integration test.

Offline-first runtime

The client verifies the cached entitlement offline on every launch and honors a grace window past expiry, so a server outage degrades gracefully instead of locking out paying customers. Revocation rides a signed CRL that can be served as a static file — no live server required. The whole system runs at $0 in pure-offline mode (issuer keys + static CRL); the activation server is an opt-in layer that adds seat-locking and kill-switch without changing the token format.

Envelope construction in detail

Signing serializes the payload to JSON, base64url-encodes it (no padding), and forms signed = "<typ>.<payload_b64>". Ed25519 signs the UTF-8 bytes of signed; the signature is base64url-encoded and appended: <typ>.<payload_b64>.<sig_b64>. Verification splits on . into exactly three parts, rejects a typ mismatch before touching crypto, decodes the 64-byte signature, re-derives signed, and runs VerifyingKey::verify over it. Only after the signature passes is the payload base64-decoded and deserialized — so malformed or forged input never reaches the JSON parser as trusted data.

Type tags and the five token kinds

Each token carries one of five reserved typ tags — license, entitlement, crl, unblock, manifest — and the tag is part of the signed message. This blocks cross-type replay for every pair, not just license/entitlement: an unblock token, for instance, cannot be presented where a license is expected even under the same key, and the unit tests pin both the type-confusion rejection and the unblock-is-issuer-signed property.

TypeSignerLifetimeDistribution
licenseissuer (offline private key)perpetual or expiresemailed to the buyer
entitlementactivation server (entitlement key)short (TTL, default 30d) + grace windowreturned on activation, cached
crlissuerre-signed on each changestatic file
unblockissuerone-shotemailed for false-positive recovery
manifestissuernewest wins (anti-replay)static file

Offline node-locking & fingerprints

The fingerprint stored in a license is base64url(SHA-256(salt ":" raw_id)), salted by the product name. The raw hardware id therefore never travels in the license and is not recoverable from it; the same raw id under a different product salt produces a different hash, so a fingerprint from one product cannot be reused against another. The raw id itself is the real hardware identity per platform — macOS IOPlatformUUID read via direct IOKit/CoreFoundation FFI (no ioreg subprocess that could be shadowed on PATH), Windows MachineGuid from the registry, Linux /etc/machine-id — falling back to the hostname only if the platform source is unavailable.

Anti-tamper as a cost layer

The report does not overstate this. Every anti-tamper check runs inside the binary and is therefore patchable. The design choices that make it a meaningful cost rather than theater: environmental signals (debugger_present, injection_detected, env_suspicious, timing_anomaly) are combined, not decisive; the telltale string literals are XOR-obfuscated and the C-ABI symbols are renamed (zpwr_rt_*) so static triage does not flag the layer; and the strongest primitives turn detection into corruption instead of a catchable failure. binding_seed derives a value from the token's signature that only a valid license reproduces; guarded_seed returns a deterministically-wrong value under analysis. Woven into real computation, NOP-ing a boolean gate is no longer enough — the attacker must forge an Ed25519 signature. tamper_tripwire escalates to a durable, hashed, multi-location blacklist, lifted only by a vendor-signed unblock.

Activation, seats, and floating licenses

On POST /activate the server verifies the license with the public key, rejects expired or revoked licenses, then enforces seats by mapping one license id to a bounded set of machine fingerprints (a new fingerprint past the seats limit is refused). It returns an entitlement signed with the separate entitlement key. The seat store is a JSON file persisted on each change. The free-tier Cloudflare Worker extends this with floating/concurrent licensing: /checkout grabs a seat as a KV lease with a TTL, /heartbeat renews it, /checkin releases it, and a crashed client's seat frees automatically when its lease expires — so "N users at once" works without zombie seats and without leaving the free tier.

Test coverage that pins the design

The invariants above are not aspirational; they are asserted by the unit and integration tests in the workspace. Among them: license sign/verify roundtrip, tampered-payload rejection, wrong-key rejection, type-confusion rejection, key encode/decode roundtrip, salted-fingerprint determinism and non-recoverability, node-lock binding to the right machine, signed metadata claims roundtrip, keyring rotation (old + new keys both verify, a stranger's does not), expiry/validity windows, the entitlement grace window, CRL revocation + signing, the issuer-signed/typed unblock token, dotted-version comparison, and manifest force-upgrade + anti-replay freshness. The anti-tamper module separately pins the clock-rollback check, deterministic token-specific binding seeds, the recheck cadence, the hashed persistent blacklist (record / idempotency / clear), the key-substitution self-test, and the self-signature roundtrip + tamper detection.

Dependencies

A small, vendorable, durable set — no licensing SaaS client, and the embedded core has no async runtime:

CrateUsed for
ed25519-dalekEd25519 sign / verify (re-exported so downstream crates don't pin their own version).
getrandomKey material + random license ids from the OS CSPRNG.
sha2SHA-256 for fingerprints, binary self-hash, and blacklist hashing.
serde / serde_jsonToken payload (de)serialization.
base64base64url (no-pad) envelope + key encoding.
gethostnameMachine-id fallback when the platform hardware id is unavailable.
libc (macOS/Linux) · winreg (Windows)Hardware id + anti-debug primitives (sysctl, ptrace, registry).
ureq (feature online)Blocking pull of a signed CRL / manifest from a static host.
tiny_http · parking_lotSynchronous activation server + seat-store locking (no tokio).

See the documentation for the full CLI, server, and embedding reference.