Lease format
A lease is a standard JWT, signed with EdDSA over Ed25519 by the product's private key. KeyGrant returns one on every activation and renewal, and your app verifies it offline with the product's public keys.
The header names the key that signed it:
{"alg":"EdDSA","typ":"JWT","kid":"<kid>"} -
kidis that key's RFC 7638 thumbprint, the samekidas its entry inkeysfrom /pubkey. -
A lease signed before leases named their key has the header
{"alg":"EdDSA","typ":"JWT"}and names no key.
Claims
{ "iss": "keygrant", "sub": "MYPROD-9K4T-2M8P-XQ7V-H3DW", "product": "my-product", "aid": "act_0f3c2a9d8e7b4c1a9f0e2d3c4b5a6978", "fp": "1f5daedc3c0b49dd8c701362c6b664233946ec82967af77ff3e1aef86c38791d", "fpk": "machine", "model": "perpetual", "lim": 3, "major": 2, "ent": { "edition": "pro" }, "iat": 1791257861, "exp": 1793849861} Always "keygrant".
The license key, as issued.
The product's API ID.
The activation ID of the seat it was signed for.
The device, hashed: lowercase hex SHA-256 of keygrant-fp| followed by the fingerprint the seat is keyed by. That is the fingerprint sent on activation, or the machine ID's fingerprint once a renewal has moved the seat to it. Never the fingerprint itself.
What the fingerprint is: machine or hostname. Absent when the seat's kind is unknown. Only machine holds the lease to its device.
perpetual, subscription or trial. A timed key's lease says subscription: it ends, renews online and has a grace period as a subscription key's does, and every SDK already reads that (see Timed models).
The key's device limit.
The highest major the key covers, or 1000000 when it has no upper bound or its upgrade protection is running.
The lowest major it covers. Present only when above 1.
When upgrade protection ends, in Unix seconds. Present only while protection is running on a key with a highest version.
The key's entitlements, {} for none. See Editions & entitlements.
The key's meters, by code: what is left of each, the uses it holds, the offline allowance each device may spend, and whether it comes back each billing period. Present only on a key that has a meter. See Metered uses.
When it was signed, and when it runs out, in Unix seconds.
true while the key is past its expiry but inside its grace period. Absent otherwise.
true on a test license. Absent otherwise.
- Times in a lease are in Unix seconds. Timestamps in KeyGrant's JSON responses and webhook bodies are in Unix milliseconds.
- A lease has no
nbf,audorjti, and never the customer's email.
A trial's lease
The lease returned by POST /trial differs from a key's in these claims:
trial: followed by the device's hashed fingerprint.
That same hash.
Absent.
Present when your app sent fingerprintKind. Only machine holds the trial to its device.
trial.
1.
1000000.
The product's Trial entitlements.
The trial's end: the lease runs to it.
How long a lease lasts
At a glance
- Offline days
- 1 to 90, the license model's setting
- When the model sets none
- 30 days for a perpetual key, 7 for a subscription, trial or timed key
- A lease never runs past
- The key's expiry plus its grace period
- The shortest lease
- 5 minutes
- A key with no expiry
- Its leases are not capped
- A revocation takes at most
- Offline days, on a device that stays offline
- A key keeps the Offline days it was sold or issued with, like its other terms.
- The 5-minute floor wins over the key's end: a lease signed in the key's last 5 minutes still lasts 5 minutes, and can outlast the key by up to 5 minutes.
- A revoked or refunded key keeps working offline until its device's lease runs out.
Verifying without an SDK
Any JWT library that supports EdDSA with Ed25519 can verify a lease against the keys from
/pubkey. With jose in Node:
import { createHash } from "node:crypto";import { decodeProtectedHeader, importJWK, jwtVerify } from "jose";
// KEYS: the keys array from /pubkey, shipped with your app.// A lease names the key that signed it; one signed before leases named their key names none.const { kid } = decodeProtectedHeader(lease);const candidates = kid === undefined ? KEYS : KEYS.filter((jwk) => jwk.kid === kid);let payload;for (const jwk of candidates) { try { ({ payload } = await jwtVerify(lease, await importJWK(jwk, "EdDSA"), { algorithms: ["EdDSA"], issuer: "keygrant", requiredClaims: ["exp"], })); break; } catch (error) { if (error.code !== "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") throw error; }}if (payload === undefined) throw new Error("No key this app carries signed this lease");
const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
// The checks a JWT library cannot make for you:const ok = payload.product === "my-product" && payload.sub.toUpperCase() === storedKey.toUpperCase() && payload.aid === storedActivationId && (payload.fpk !== "machine" || payload.fp === undefined || payload.fp === sha256(`keygrant-fp|${fingerprint}`)) && MAJOR >= (payload.minMajor ?? 1) && MAJOR <= (payload.major ?? 0);
Verify with the key whose kid the header names, or, for a lease that names none, with
each key in turn. The library checks the signature and the expiry. To accept the same leases an SDK
accepts, also check:
- Verify with your own public keys and allow only EdDSA; never trust the algorithm named in the header.
- A lease that names a key you do not carry is refused. The SDKs report it as
invalidwith reasonunknown-key. productis your product's API ID, exactly.subis the key you stored, ignoring case, andaidthe activation ID you stored, exactly.- When
fpkismachineandfpis present,fpis the hash of this device's fingerprint. Otherwise, skip the device check. - The running major is at least
minMajor(1 when absent) and at mostmajor. test, if your release builds refuse test licenses.
-
fingerprintin the code is the fingerprint your app sends to KeyGrant for this device: the one it activated with, or the machine ID's once a renewal has moved the seat to it. -
The
.pemon the dashboard's Signing key tab is only the key that signs now. - To renew a lease, call POST /validate.
Rotating the signing key
Each build of your app carries the product's key set. Rotating its signing key takes two steps on the Signing key tab, with your builds shipped between them, so that they carry the next key before it signs anything:
- Stage next key (on the tab). KeyGrant makes the product's next keypair and publishes
its public key beside the current one:
/pubkeylists both inkeys, and Copy key set copies them. Every lease is still signed with the current key, so nothing changes for any app. - Ship builds carrying the key set (your release). Each activation and renewal from such a build reports the ids of the keys it carries. The tab counts the devices on valid licenses whose builds have not reported carrying the staged key.
- Switch to staged key (on the tab), which asks first and shows that count. Every lease signed from then on, for activations, renewals and trials, is signed with the staged key.
A build on an SDK from before key sets reports no key ids: @keygrant/electron before
0.4.0, and the .NET, Rust, Swift and C++ SDKs before 0.3.0. The Dart SDK reports them from its first
version.
Warning
A switch cannot be undone. A build that carries only the old key rejects every lease signed after the switch.
After the switch
- On a build that carries only the old key, an activation fails, though the device still takes a seat on the license key, and a device trial started on it ends at once.
-
An installed copy keeps the lease it holds, and stops being licensed when that lease runs out,
reporting
expiredwith reasonofflinefrom then on. - Updating it to a build that carries the new key licenses it again at its next renewal.
Until the old key retires
The key that signed until the switch stays in keys until its retire date, when the last
lease it signed has run out:
- Retire date: the switch, plus the longest a lease of the product can last (the longest Offline days among its keys, or its trial length when that is longer), plus a day.
- Until then: a build you make should still carry every key in
keys. A customer who updates while holding a lease the old key signed keeps verifying it offline until it is renewed. - After it: the old key leaves
keys, and your builds can drop it. - A key that has signed a license file or a key file never retires: it stays in
keys, and in your builds, for good. Such a file never expires and names the key that signed it. A build without that key reads the file asinvalidwith reasonunknown-key(see License files).
If the private key leaked
Warning
Stage and switch at once, then ship builds that carry only the key that signs (publicJwk
from /pubkey), not the whole set. A build already shipped with the leaked key accepts a
lease forged with it until it is updated.
The whole set would carry the leaked key: it stays in keys until it retires (for good, if
it has signed a license file or a key file), and any build that carries it accepts a lease forged with
it.