Offline License Validation in C#: Verifying Signed Tokens Without a Server
An app that needs to keep working on an air-gapped machine — a factory floor terminal, a secure government workstation, a laptop that's simply offline more often than not — can't call validate() every time it starts. PermitCore's offline activation solves this with a signed token the SDK can check entirely on its own, no network required. Here's the actual code, and honestly, its real limits.
Offline behavior is configured per license — Offline Grace Days controls how long a cached "valid" result is trusted between real activations.
The Token Format
An offline token is a plain string with three dot-separated parts:
pc_offline_v1.<base64url(payload_json)>.<base64url(signature)>
The prefix identifies the format so the SDK can reject anything malformed immediately. The payload is a small JSON object — license ID, device ID, expiry, a key ID (kid) identifying which signing key produced it — and the signature is an ECDSA P-256 (SHA-256) signature over the raw bytes of the base64url-encoded payload, produced with a private key that never leaves PermitCore's servers.
The Real Verification Code
This is the actual method from PermitCore's .NET SDK — not a simplified version. It's a static method with zero dependencies, so it's easy to audit or even copy into your own project if you'd rather not take the SDK as a dependency:
public static OfflineTokenResult VerifyOfflineToken(string token, string publicKeyBase64) { var parts = token?.Split('.') ?? []; if (parts.Length != 3 || parts[0] != "pc_offline_v1") return new OfflineTokenResult { IsValid = false, Message = "Malformed token." }; try { var payloadBytes = Encoding.UTF8.GetBytes(parts[1]); var sig = FromBase64Url(parts[2]); using var ecdsa = ECDsa.Create(); ecdsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKeyBase64), out _); if (!ecdsa.VerifyData(payloadBytes, sig, HashAlgorithmName.SHA256)) return new OfflineTokenResult { IsValid = false, Message = "Invalid signature." }; var json = Encoding.UTF8.GetString(FromBase64Url(parts[1])); var payload = JsonSerializer.Deserialize<OfflineTokenPayload>(json); if (payload.ExpiresAt < DateTime.UtcNow) return new OfflineTokenResult { IsValid = false, Message = "Token expired.", Payload = payload }; return new OfflineTokenResult { IsValid = true, Message = "Valid.", Payload = payload }; } catch { return new OfflineTokenResult { IsValid = false, Message = "Invalid or corrupt token." }; } }
Notice the signature is checked before the payload's expiry date is even trusted — you never want to make a security decision based on data you haven't verified yet. This exact method is documented, byte-for-byte, to match what PermitCore's own server does when it signs a token, so there's no drift between what gets issued and what gets accepted.
The Full Offline Flow
Verifying a token is the primitive — most apps use two higher-level calls built on top of it:
// First run, while still online — fetch a token, verify it, cache it to disk var result = PermitCoreClient.ActivateOffline(token, publicKey, deviceId); // Every later launch — no network call at all var cached = PermitCoreClient.ValidateOffline(deviceId); if (!cached.IsValid) { /* show activation screen */ }
ActivateOffline also checks the token's DeviceId matches the machine calling it — a valid, correctly-signed token issued for one device is rejected if you copy it onto another. ValidateOffline never touches the network at all; it reads whatever was cached by the last successful ActivateOffline call and re-checks its expiry.
What This Actually Protects Against — and What It Doesn't
A signature makes the token's contents tamper-proof: nobody can edit the cached JSON on disk to push out the expiry date or swap the device ID, because doing so breaks the signature. That's a real, meaningful guarantee.
What it can't stop is a hostile local clock. The whole expiry check depends on DateTime.UtcNow — and on a machine the license holder fully controls, nothing stops them from setting the system clock backward before it runs. This is an open, honestly-documented gap in offline verification generally, not just in PermitCore's implementation — see Offline Grace Mode for the fuller discussion of grace periods and their real limits.
In practice, offline validation is meant to survive a legitimately disconnected machine, not a hostile one actively trying to defeat it — the same tradeoff every offline-capable licensing system makes.