Originally published on the Keylight blog.
A license check that needs a server is a license check that fails on a plane. For desktop and CLI apps that is unacceptable, so Keylight makes the license itself verifiable offline: it is an Ed25519-signed document your app checks locally with only a bundled public key. This post walks through how that works in Rust with the keylight crate — the synchronous verifier, the trusted keys, and the cached lease that keeps an app licensed without a connection.
The shape of offline validation
There are two distinct jobs in any licensing system, and the mistake most DIY implementations make is fusing them. The first job is verification: is this license genuine and unexpired? The second is revalidation: has anything changed server-side — a refund, a downgrade, a revocation — since the last time we checked?
Verification can and should be local. A Keylight lease is a small JSON document signed with Keylight's Ed25519 private key. Your app ships with the matching public key, so it can confirm the lease is authentic and untampered using only math, in microseconds, with no connection. Revalidation is the only part that needs the network, and it is periodic and can happen in the background — never on the critical path of launching the app.
Splitting them is what makes offline-first possible. The app verifies the cached lease at startup and unlocks immediately; it reaches out to revalidate only when a check is actually due. For the broader model, see how offline license validation works; for the anatomy of the signed document itself, what is inside a Keylight lease.
Setting up the keylight crate
The crate is synchronous — it uses blocking HTTP via ureq, so there is no tokio runtime to set up. Add it:
cargo add keylight
Build a config with your tenant and product IDs, pull the tenant's public keyset so leases verify offline, and create the client:
use keylight::{Keylight, KeylightConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut cfg = KeylightConfig::builder("your-tenant", "your-product", "sdk_live_…")
.max_offline_days(7) // bound how long a cached lease is accepted offline
.build();
// Fetch the tenant's trusted Ed25519 keyset. You can also pin keys at build
// time with .trusted_key(kid, pub_b64) if you'd rather not fetch them.
if let Some((_, keys)) = keylight::keyset::fetch_keyset(
&keylight::http::ureq_transport::UreqTransport::default(),
&cfg.base_url,
&cfg.tenant_id,
) {
cfg.trusted_keys.extend(keys);
}
let kl = Keylight::new(cfg)?;
// The one online step: exchange a key for a signed lease. The lease is
// Ed25519-verified *before* it is written to disk.
let res = kl.activate("USER-LICENSE-KEY")?;
println!("activated: {}", res.activated);
// From here on, gating works offline from the cached lease.
if kl.has_entitlement("pro") {
println!("Pro features unlocked");
}
Ok(())
}
activate is the only call here that touches the network. Once it succeeds, the verified lease is persisted, and everything after that — entitlement checks, state resolution — reads from the cache.
Verifying a lease with no network call
The crate exposes the verifier as a standalone function, so you can check a lease yourself — in a CLI, in your own gating logic, or server-side — without constructing a client:
use keylight::verifier::{verify_lease, SKEW_SECONDS};
let result = verify_lease(
&lease, // the Lease you hold
&trusted_keys, // HashMap<kid, base64 ed25519 public key>
now_seconds, // current unix time
SKEW_SECONDS, // 300s tolerance for clock drift
);
if result.is_trusted() && !result.expired {
// Genuine, unexpired lease — unlock.
}
verify_lease returns a VerifyResult with three independent booleans: kid_known (the lease was signed by a key your app trusts), signature_valid (the Ed25519 signature checks out), and expired (the lease is past its expiry window, accounting for clock skew). is_trusted() is the conjunction of the first two — a lease can be authentic but expired, and the struct lets you tell those cases apart.
This is the whole offline story in one function call: no HTTP, no I/O beyond reading the lease you already have. The trust anchor is the tenant's private key staying on Keylight's servers; an attacker who extracts the public key from your binary still cannot mint a lease that signature_valid accepts.
How the client stays valid offline
In normal use you do not call verify_lease directly — the client does it for you. state() resolves the current license state with no network call, reading the cached lease and re-verifying it each time:
use keylight::LicenseState;
match kl.state() {
LicenseState::Licensed => { /* full access */ }
LicenseState::Trial { days_left } => println!("{days_left} days left"),
LicenseState::Limited => { /* on the offline grace window */ }
LicenseState::FreeTier => { /* free features only */ }
LicenseState::Expired => { /* renew */ }
LicenseState::Invalid => { /* prompt for a key */ }
}
The cached lease lives in a device-bound encrypted file store (ChaCha20-Poly1305), in the per-OS application data directory, so it survives restarts and is the same code path on macOS, Windows, and Linux. The encryption is a hardening measure, not the trust boundary — the Ed25519 signature is what makes the lease unforgeable. Even decrypted, a lease edited to grant more entitlements fails signature_valid and is rejected.
The max_offline_days setting is what stops a device from drifting offline forever. When set, the client refuses a cached lease whose last successful online validation is older than that window, even if the lease itself has not expired. It is the honest middle ground between fail-closed (a server call on every launch, which breaks offline use) and fail-open (a cached lease accepted indefinitely): legitimate offline use keeps working for the window you choose, and the app revalidates when it can. At launch, kl.check_on_launch() triggers that background revalidation only when one is due.
That is offline license validation in Rust end to end: one online activation, then local Ed25519 verification on every check, bounded by a grace window you control. The same crate also backs the Tauri plugin, so a Tauri app and a plain Rust binary verify leases the same way. If your Rust setup has an angle this post does not cover, send us your feedback and we'll extend it.
FAQ
Can I validate a license in Rust without a network connection?
Yes. A Keylight lease is an Ed25519-signed document. The keylight crate verifies the signature locally against the tenant's public keys with verify_lease — no HTTP call. The network is only needed for the initial activation and periodic revalidation.
Is the keylight Rust crate async?
No. It is synchronous and uses blocking HTTP via ureq, so it works in a plain Rust binary or CLI without a tokio runtime. The offline verification path makes no network call at all.
How long can a Rust app stay offline before the license stops working?
Until the lease itself expires, unless you set max_offline_days. That bounds how long the cached lease is accepted since the last successful online validation, so a device cannot stay offline indefinitely.