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.
| Type | Signer | Lifetime | Distribution |
|---|---|---|---|
| license | issuer (offline private key) | perpetual or expires | emailed to the buyer |
| entitlement | activation server (entitlement key) | short (TTL, default 30d) + grace window | returned on activation, cached |
| crl | issuer | re-signed on each change | static file |
| unblock | issuer | one-shot | emailed for false-positive recovery |
| manifest | issuer | newest 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:
| Crate | Used for |
|---|---|
| ed25519-dalek | Ed25519 sign / verify (re-exported so downstream crates don't pin their own version). |
| getrandom | Key material + random license ids from the OS CSPRNG. |
| sha2 | SHA-256 for fingerprints, binary self-hash, and blacklist hashing. |
| serde / serde_json | Token payload (de)serialization. |
| base64 | base64url (no-pad) envelope + key encoding. |
| gethostname | Machine-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_lot | Synchronous activation server + seat-store locking (no tokio). |
See the documentation for the full CLI, server, and embedding reference.