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.
Payload
Section titled “Payload”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};expis the end of the current lease. It is renewed by every heartbeat.lexpis 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 ismin(exp, lexp).hbnever affects the verdict.pidis optional only for tokens minted before it existed. Always pin it.
Verification rules
Section titled “Verification rules”Apply them in this order. A verifier that is more permissive than this one is the crack surface, so implement every step.
- Shape. Exactly one
.in the token. Each half must be canonical unpadded base64url (alphabetA-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. - Choose the key. If the payload has a
kid, try only the embedded key(s) whose derivedkidmatches. If there is nokid, try every embedded key. A token whosekidmatches none of your keys is rejected asunknown-key. (kid= first 16 characters of base64url(SHA-256(raw public key bytes)). It is a routing hint, never a trust decision.) - Verify the signature over the raw decoded payload bytes before trusting any field.
- Parse the JSON, then assert
alg === "ed25519"andv === 1. Reject anything else; this stops downgrade attacks. - Validate every claim’s type and range.
lid,mid,fpare strings;kindis"full"or"demo";featis an array of strings;iatandexpare integers;lexp, if present, is an integer;graceis an integer from 0 to 3650;maxdis a non-negative integer. A good signature proves origin, not well-formedness: anexpthat arrives as a string would turn arithmetic into concatenation and produce a license that never expires. - Product binding. If you know which product you are, require
pid === yourProductId. - Machine binding. Require
fpto equal this machine’s fingerprint, compared in constant time. If you do not have a fingerprint, fail closed rather than skip the check. - Not yet valid. Reject if
now + 300 < iat(five minutes of clock skew allowed). - Expiry. With
effectiveExp = lexp === undefined ? exp : min(exp, lexp), andgraceDays = kind === "demo" ? 0 : grace: reject ifnow > effectiveExp + graceDays * 86400. Ifnow > effectiveExpbut 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.
Reference verifiers
Section titled “Reference verifiers”- TypeScript / Node: the SDK’s
evaluateOfflineandverifyLicenseToken. - 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 noOrg.prefix, though the namespace does):Ed25519Signer, initialised for verification withnew Ed25519PublicKeyParameters(rawKey, 0). ThenJsonDocumentfor the payload andHMACSHA256for the fingerprint. - Swift: swift-crypto
Curve25519.Signing.PublicKey(rawRepresentation:)and.isValidSignature(_:for:);HMAC<SHA256>for the fingerprint. - Python:
cryptography’sEd25519PublicKey.from_public_bytes(...).verify(...), withhmacandhashlibfor the fingerprint. Add=padding beforebase64.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.