Guide · By Buğra Ömer Aydemir · Updated 15 October 2026

Offline license verification with signed leases (JWS and Ed25519)

A desktop app cannot ask a server every time it starts: customers work on planes, behind firewalls and on machines that never go online. The usual answer is a signed statement the app can check by itself. This page explains how Carcha Seal builds that statement, a lease, how an app verifies it without a network call, and what offline verification can and cannot guarantee.

Why a signature, and why a short-lived one

A plain license key proves nothing on its own: the app must either ask a server or contain the logic that accepts keys, and that logic can be copied into a key generator. A digital signature changes this. The server signs with a private key that never leaves it; the app holds only the public key, which can check a signature but cannot make one. Anyone can read a lease, nobody without the private key can write one.

A signed file that is valid forever cannot be taken back. Carcha Seal therefore signs a lease that expires: it carries a check-in deadline (72 hours on the default policy) and an offline grace period (7 days). The app renews it whenever it can reach the server, so revocations, seat changes and new features arrive within one check-in interval, and an app that stays offline keeps working for a known, bounded time.

The lease format

A lease is a compact JSON Web Signature (RFC 7515): three base64url parts, header.claims.signature. The algorithm is Ed25519 (RFC 8032), named Ed25519 as RFC 9864 specifies for JOSE. The header carries typ: licensekit-lease+jwt and kid, the RFC 7638 thumbprint of the signing key. The claims say who the lease is for and until when:

ClaimMeaning
audProduct code
fpDevice fingerprint hash
nbf, expNot before; license expiry (absent for perpetual licenses)
rev, grcCheck-in deadline; offline grace in seconds
featLicensed features

The full list is in the documentation. Ed25519 signatures are 64 bytes and public keys 32 bytes, so a lease stays small.

Verification, step by step

The verifier checks in a fixed order and stops at the first failure, so every refusal has exactly one reason:

  1. Three parts, valid base64url and JSON, else malformed.
  2. The algorithm is exactly Ed25519, else unsupported_algorithm. Accepting only one algorithm closes the algorithm-confusion attacks RFC 8725 warns about, such as none or a public key used as an HMAC secret.
  3. The type is licensekit-lease+jwt, else wrong_type; a header that lists critical extensions (crit) is rejected, as RFC 7515 requires for extensions the verifier does not understand.
  4. The kid names a key the app trusts, else unknown_key.
  5. The signature verifies, else bad_signature. Changing one character of the claims breaks it.
  6. Issuer, product and device match, else wrong_issuer, wrong_product or wrong_device.
  7. Time: not_yet_valid, expired, then revalidation_required once the check-in deadline plus grace has passed. Five minutes of clock skew are allowed, as RFC 7519 permits.

You can run the same check yourself with the open-source core package:

const { verifyLease } = require('@carchalabs/seal-core');

const result = verifyLease(lease, {
  keys: [currentKeyPem, nextKeyPem],
  product: 'example-studio',
  fingerprintHash,
  graceSeconds: 7 * 86400,
});
if (!result.ok) console.log(result.reason);   // e.g. 'wrong_device'

The browser sandbox on the home page verifies each lease it receives in the page itself.

Key rotation without an app update

Apps ship two public keys: the current one and the next one. The server publishes both as a JSON Web Key Set (RFC 7517) at /v1/keys, and each lease names its key with kid. To rotate, the server starts signing with the next key; apps already in the field accept those leases because they already trust that key. Ship a new next key with a later app version before the following rotation.

Device binding

The fp claim is a SHA-256 hash of the operating system's machine ID (IOPlatformUUID on macOS, MachineGuid on Windows, /etc/machine-id on Linux) together with the product code. The raw ID never leaves the computer, and the same machine gives different hashes for different products. A lease copied to another computer fails with wrong_device. Activating again on the same computer reuses its seat.

Time and the clock

An offline check trusts the computer's clock, and a user can turn the clock back to stretch a lease. The SDK remembers the latest time it has seen; if the clock moves back more than a day, or a lease appears to come from the future, it reports clock_suspect and paid features stop until the next successful online check. This does not stop someone who keeps the clock frozen from the first day, which is one reason the grace period is bounded.

What offline verification cannot do

  • Instant revocation. A revoked license works on an offline computer until its check-in deadline plus grace: up to 72 hours plus 7 days on the default policy. Shorter intervals shrink the window.
  • Stop someone who edits the app. The check runs on the customer's computer. A signature stops forged licenses, not a patched binary. Encrypting feature code with a key the lease carries makes a patched app useless for that feature; see Encrypting data and code.
  • Protect storage everywhere. On Linux without a keyring, Electron's safeStorage uses a fixed password; the SDK reports storageProtected: false.

Add license keys to an Electron app · Documentation · Try it without signing up

Sources