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:

Header
{"alg":"EdDSA","typ":"JWT","kid":"<kid>"}
  • kid is that key's RFC 7638 thumbprint, the same kid as its entry in keys from /pubkey.
  • A lease signed before leases named their key has the header {"alg":"EdDSA","typ":"JWT"} and names no key.

Claims

Payload
{  "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}
iss

Always "keygrant".

sub

The license key, as issued.

product

The product's API ID.

aid

The activation ID of the seat it was signed for.

fp

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.

fpk

What the fingerprint is: machine or hostname. Absent when the seat's kind is unknown. Only machine holds the lease to its device.

model

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).

lim

The key's device limit.

major

The highest major the key covers, or 1000000 when it has no upper bound or its upgrade protection is running.

minMajor

The lowest major it covers. Present only when above 1.

upgradeUntil

When upgrade protection ends, in Unix seconds. Present only while protection is running on a key with a highest version.

ent

The key's entitlements, {} for none. See Editions & entitlements.

met

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.

iat, exp

When it was signed, and when it runs out, in Unix seconds.

grace

true while the key is past its expiry but inside its grace period. Absent otherwise.

test

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, aud or jti, and never the customer's email.

A trial's lease

The lease returned by POST /trial differs from a key's in these claims:

sub

trial: followed by the device's hashed fingerprint.

aid

That same hash.

fp

Absent.

fpk

Present when your app sent fingerprintKind. Only machine holds the trial to its device.

model

trial.

lim

1.

major

1000000.

ent

The product's Trial entitlements.

exp

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:

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 invalid with reason unknown-key.
  • product is your product's API ID, exactly.
  • sub is the key you stored, ignoring case, and aid the activation ID you stored, exactly.
  • When fpk is machine and fp is present, fp is the hash of this device's fingerprint. Otherwise, skip the device check.
  • The running major is at least minMajor (1 when absent) and at most major.
  • test, if your release builds refuse test licenses.
  • fingerprint in 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 .pem on 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:

  1. Stage next key (on the tab). KeyGrant makes the product's next keypair and publishes its public key beside the current one: /pubkey lists both in keys, and Copy key set copies them. Every lease is still signed with the current key, so nothing changes for any app.
  2. 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.
  3. 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 expired with reason offline from 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 as invalid with reason unknown-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.