Offline license validation is the ability to confirm a license is authentic and still valid without any network call — and it works because of one asymmetry in public-key cryptography: your server holds a private key that can create signatures, while your app holds only the matching public key, which can verify a signature but never forge one. That single property lets a client prove a license was issued by you, and hasn't been tampered with, entirely on its own machine. This post explains how that works end to end — signatures, node-locking, expiry, revocation, and grace windows — and why the whole thing has to fail closed to be worth anything.
Why a signature is enough to trust a license offline
Imagine your app just checked whether a license file said "tier": "pro". Trivially defeated — anyone can edit a text file. The signature is what makes the file trustworthy.
When you issue a license, the server takes the license payload (licensee, product, tier, seats, expiry, entitlements) and computes an RSA signature over it with a private key. That signature is attached to the license. Your app embeds the matching public key and, at validation time, recomputes the check: does this signature correspond to this exact payload under this public key? If yes, two things are simultaneously proven — the license was signed by your private key (authenticity), and not a single byte has changed since (integrity). Flip one character in the tier field and the signature no longer matches; validation fails.
The reason this needs no server is that verification is pure math on data the app already has. The public key isn't a secret and can ship right inside your binary — decompile the app, read the key, publish it, and an attacker still can't mint a license, because minting requires the private key that never left your server. This is the same asymmetry behind TLS and code signing, applied to licensing.
The checks that run after the signature
A valid signature only proves the license is genuine and unaltered. Offline validation then reads the now-trusted fields and enforces them locally.
Node-locking to a machine fingerprint
To stop one license from being copied to a thousand machines, a license can be node-locked: bound to a fingerprint the app derives from stable hardware and OS attributes. At validation the app recomputes the fingerprint and checks it against the one baked into the license. Mismatch, and the license doesn't apply here.
The subtlety is tolerance. Fingerprint too strictly and a customer who swaps a network card or upgrades a disk gets locked out. So a good implementation allows a small amount of drift — Keyright's SDK uses a configurable NodeLockTolerance (default 1) so an ordinary hardware change doesn't trip the lock — and lets you control which components identify a machine for unusual environments like VMs and CI runners.
Expiry and clock tampering
A subscription or trial carries an expiryUtc, and offline validation simply compares it to the current time. Which raises the obvious attack: roll the system clock backward and a trial never ends. Offline validation has to defend against that without a time server. The standard approach is to remember the latest time the app has legitimately seen and treat a clock that jumps meaningfully backward as tampering. Keyright's SDK does exactly this — moving the clock back beyond a ClockTamperToleranceHours window (default 24h) on a time-limited license yields a ClockTampered status and refuses to validate until the time is corrected. Perpetual licenses, having no expiry, aren't subject to the check.
Revocation, offline
Here is where offline-only shows its one real limitation, and how to close it. A license signed last year knows nothing about a refund or leak that happened last week — the signed payload is fixed at issue time. Online, revocation is a server flag picked up on the next activation refresh. Offline, you distribute a signed revocation list: a list of revoked license ids, itself signed by your private key so the client can trust it, shipped with an app update. A purely offline client checks incoming licenses against that list and drops revoked ones. It's not instant — it moves at the speed of your releases — but it means a disconnected app isn't defenceless against a known-bad key. Keyright can build and sign a distributable revocation list you ship with RevocationListJson / RevocationListPath.
Offline-first, with an optional online grace window
Pure offline validation is perfect for air-gapped installs and fast local gating, but it can't count seats — a client has no idea how many other machines run the same key — and it can't reflect a revocation newer than the last update. The common, honest answer is offline-first with an optional online check.
Keyright's model works like this: your app can validate a shipped offline license file with zero network, or it can activate online once — exchanging the license key for a short-lived signed lease bound to this machine. That lease is itself verified offline against your embedded public key and cached locally, so after a single activation the app keeps validating offline for the life of the lease. Only when the lease nears expiry does it need to reach the server again, at which point seat counts and revocations are reconciled. If the server is briefly unreachable, a still-valid cached lease covers the gap — a grace window — so an outage doesn't lock out an honest paying user. You get offline resilience and server-enforced seats and revocation from the same system, instead of choosing one.
Why "fail closed" is the whole game
Every check above shares one design rule, and it is the rule that makes offline validation actually protective rather than decorative: on any problem, resolve to the unlicensed state — not an exception, and never an unlocked one.
Consider the alternative. If a license check failed open — treating an error as "allow" — the easiest crack in the world would be to make the check error on purpose: delete the license file, corrupt a byte, block a call, and enjoy the software. Failing closed inverts that: a bad signature, a wrong product, an expired or revoked key, a tampered clock, a missing or unreadable file — every one of them lands in the free/unlicensed edition. The safe outcome is the default outcome, so breaking the check gets the attacker nothing.
Keyright's SDK is built around this. Validate() never throws; on any failure it returns a LicenseInfo in the Free edition carrying a Status (NoLicense, SignatureInvalid, Expired, MachineMismatch, Revoked, ClockTampered, …) and a human-readable message you can show in a "Register" dialog. Online ActivateAsync() follows the same rule for its ordinary failure paths — bad key, seats exhausted, offline, revoked — returning a fail-closed result rather than throwing. Your if (info.IsPaid) branch is the only thing that unlocks anything, and it only runs when everything genuinely checked out.
Being honest about the limits
Offline validation proves authenticity and integrity beautifully, but it is not magic. Because the check ultimately runs on the user's machine, a determined attacker with a decompiler and a debugger can patch it out — the same truth that applies to any client-side protection. That's not a reason to skip it; it's a reason to be precise about its job. Offline validation is the right, strong tool for air-gapped and enterprise deployments and a fast, resilient local gate everywhere else. When you need to enforce — count seats, kill a leaked key promptly — you pair it with online activation so those decisions live on a server the attacker doesn't control, and you make the client hard to patch with obfuscation. Offline validation and online activation aren't rivals; they're two halves of a licensing system that's both resilient and enforceable.
Where Keyright fits
Keyright is offline-first by design: every tenant gets its own RSA signing key (private half stored encrypted server-side, never exposed), issues signed offline license files and online activation leases, node-locks with tolerance, defends against clock tampering, ships signed revocation lists, and fails closed throughout its .NET, Node, Python, and Java SDKs. If you want the full mechanics with real SDK code, read the Keyright documentation — the getting-started and .NET integration guides walk the exact Validate() and ActivateAsync() paths described here.
Top comments (0)