All posts
Deep Dive Offline

Offline License Validation in C#: Verifying Signed Tokens Without a Server

PC
PermitCore
September 15, 2026 · 6 min read

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.

A license's Offline Grace Days setting in the PermitCore admin panel

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.

All posts Next: Software Licensing with Stripe