Offline License Validation in C#: Verifying Signed Tokens Without a Server
How to check an ECDSA-signed license token in .NET with no network call
Originally published on the PermitCore blog.
A factory-floor terminal, a secure workstation with no internet, a laptop that's offline more often than not: apps like these can't call a license server on every launch. The answer is a signed offline token that your app verifies entirely on its own. Here's how that looks in C#, using the real verification method from PermitCore's .NET SDK.
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 anything malformed is rejected immediately. The payload is a small JSON object (license ID, device ID, expiry, and a key ID kid naming the signing key), and the signature is ECDSA P-256 with SHA-256 over the raw bytes of the base64url-encoded payload. The issuer holds the private signing key; your application only ever needs the trusted public key.
The verification code
This is a static method with zero dependencies, so it's easy to audit, or even copy into your own project:
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." }; }
}
The order matters: the signature is checked before the expiry date is trusted. Never make a security decision based on data you haven't verified yet. The method matches what the server does when it signs a token, so there's no drift between what gets issued and what gets accepted.
The full offline flow
Most apps use two higher-level calls built on that primitive:
// trustedTenantPublicKey comes from your application configuration.
using var client = new PermitCoreClient(new PermitCoreOptions {
BaseUrl = apiUrl, TrustedPublicKeys = new[] { trustedTenantPublicKey },
CacheDirectory = appCacheDirectory, DeviceId = deviceId
});
// First run: verify and retain the signed token.
var result = client.ActivateOffline(token, deviceId, productId);
// Every later launch, with no network call at all
var cached = client.ValidateOffline(deviceId, productId);
if (!cached.IsValid) { /* show activation screen */ }
ActivateOffline also checks that the token's device ID matches the machine calling it, so a correctly signed token issued for one device is rejected when copied onto another. ValidateOffline reads the original signed token and re-verifies its signature, expiry, device and (optionally) product against your configured trust keys.
What the signature gives you
Because the contents are signed, nobody can edit the cached JSON on disk to push out the expiry date or swap the device ID: any change breaks the signature. How long a token stays valid between online checks is configured per license with Offline Grace Days, and for store subscriptions offline access is also bounded by the paid-through date plus a configurable payment grace (14 days by default). A successful online check refreshes the signed cache after renewal.
The full guide, including the related video tutorial, is on the PermitCore blog, and the offline setup docs walk through configuring trusted keys.
How do you handle licensing for apps that spend most of their life offline? 🙂
About the author
- Software licensing with a built-in branded Store. https://permitcore.dev
Comments
Join the discussionNo comments yet. Be the first to comment.