Skip to content

Token format

An offline license is a single string you can verify in any language. You do not need the SDK.

token = base64url(payloadJSON) + "." + base64url(ed25519Signature)

Both halves use unpadded base64url. The signature is a standard Ed25519 (RFC 8032) signature, 64 bytes, over the exact payload bytes as transmitted.

type LicensePayload = {
v: 1; // schema version
alg: "ed25519";
lid: string; // license ID
mid: string; // merchant ID
kid?: string; // signing-key ID (see below)
pid?: string; // PRODUCT ID this token is for
cid?: string; // customer ID
kind: "full" | "demo";
feat: string[]; // feature flags
fp: string; // the device fingerprint this token is bound to
iat: number; // issued at, unix seconds
exp: number; // end of the LEASE, unix seconds
lexp?: number; // the license's own end date; ABSENT means perpetual
grace: number; // offline grace in days, past exp
maxd: number; // maximum devices
hb?: number; // informational: suggested check-in cadence, seconds
};
  • exp is the end of the current lease. It is renewed by every heartbeat.
  • lexp is when the license itself ends (a dated license, a subscription, a demo). A lease can never outlive the license behind it, so the effective expiry is min(exp, lexp).
  • hb never affects the verdict.
  • pid is optional only for tokens minted before it existed. Always pin it.

Apply them in this order. A verifier that is more permissive than this one is the crack surface, so implement every step.

  1. Shape. Exactly one . in the token. Each half must be canonical unpadded base64url (alphabet A-Z a-z 0-9 - _, and re-encoding the decoded bytes must reproduce the input exactly). Accepting padding, the +/ alphabet, or trailing whitespace would give one signed payload many valid spellings. The signature must decode to exactly 64 bytes.
  2. Choose the key. If the payload has a kid, try only the embedded key(s) whose derived kid matches. If there is no kid, try every embedded key. A token whose kid matches none of your keys is rejected as unknown-key. (kid = first 16 characters of base64url(SHA-256(raw public key bytes)). It is a routing hint, never a trust decision.)
  3. Verify the signature over the raw decoded payload bytes before trusting any field.
  4. Parse the JSON, then assert alg === "ed25519" and v === 1. Reject anything else; this stops downgrade attacks.
  5. Validate every claim’s type and range. lid, mid, fp are strings; kind is "full" or "demo"; feat is an array of strings; iat and exp are integers; lexp, if present, is an integer; grace is an integer from 0 to 3650; maxd is a non-negative integer. A good signature proves origin, not well-formedness: an exp that arrives as a string would turn arithmetic into concatenation and produce a license that never expires.
  6. Product binding. If you know which product you are, require pid === yourProductId.
  7. Machine binding. Require fp to equal this machine’s fingerprint, compared in constant time. If you do not have a fingerprint, fail closed rather than skip the check.
  8. Not yet valid. Reject if now + 300 < iat (five minutes of clock skew allowed).
  9. Expiry. With effectiveExp = lexp === undefined ? exp : min(exp, lexp), and graceDays = kind === "demo" ? 0 : grace: reject if now > effectiveExp + graceDays * 86400. If now > effectiveExp but inside grace, the license is valid in grace: run, but nag.

Demo tokens get no offline grace regardless of the grace claim: a demo stops the moment its lease ends.

Evaluate time against a monotonic clock, not the raw system clock, and apply your own offline deadline. See Offline integration and Hardening.

  • TypeScript / Node: the SDK’s evaluateOffline and verifyLicenseToken.
  • C++ (JUCE): the JUCE activation kit, which carries a vendored public-domain Ed25519 verifier and an HMAC-SHA256 fingerprint, byte-compatible with the server.
  • .NET: the base library has no Ed25519 type. Use the NuGet package BouncyCastle.Cryptography (the package ID has no Org. prefix, though the namespace does): Ed25519Signer, initialised for verification with new Ed25519PublicKeyParameters(rawKey, 0). Then JsonDocument for the payload and HMACSHA256 for the fingerprint.
  • Swift: swift-crypto Curve25519.Signing.PublicKey(rawRepresentation:) and .isValidSignature(_:for:); HMAC<SHA256> for the fingerprint.
  • Python: cryptography’s Ed25519PublicKey.from_public_bytes(...).verify(...), with hmac and hashlib for the fingerprint. Add = padding before base64.urlsafe_b64decode.
  • Go, Rust, PHP, Kotlin: the standard library or a mainstream crate/extension (crypto/ed25519, ed25519-dalek, libsodium, java.security) all verify RFC 8032 signatures directly.