Software that works without an internet connection is not optional — it is a requirement. Medical devices in operating rooms, point-of-sale terminals on a cruise ship, industrial controllers on a factory floor, avionics systems at 35,000 feet, workstations inside air-gapped government networks. In every case, the license validation system must function offline with zero degradation in security. Ed25519 cryptographic signatures make this possible.
Why Offline Validation Is Non-Negotiable
Online license validation is the default: your application calls an API, the API responds with a verdict. But a significant percentage of enterprise deployments operate in environments where that network call is impossible, unreliable, or explicitly forbidden:
- Healthcare — Medical imaging workstations, patient monitoring systems, and diagnostic equipment operating in compliance-restricted hospital networks where outbound API calls violate data policies
- Industrial & Manufacturing — SCADA systems, CNC controllers, and factory floor automation running on isolated OT networks with no internet bridge
- Government & Defense — Classified systems on air-gapped networks where connectivity is physically impossible by design
- Retail Point-of-Sale — POS terminals that must continue processing transactions during network outages lasting hours or days
- Aviation & Maritime — Onboard systems operating during flights or ocean crossings with intermittent or zero satellite connectivity
- Field Equipment — Geological survey instruments, agricultural monitoring systems, and remote energy infrastructure deployed beyond cell coverage
If your licensing system requires a network call to validate, you have excluded these markets entirely. Offline validation through cryptographic signatures solves this without compromising security.
The Cryptographic Foundation: Why Ed25519
Offline validation relies on asymmetric cryptography. You sign license data with a private key (kept exclusively on your server), and client software verifies that signature using a public key (embedded in the application binary). The public key can only verify — it cannot forge. This is the fundamental property that makes offline validation secure.
The algorithm you choose matters. Three options dominate the landscape:
| Property | Ed25519 | RSA-2048 | ECDSA P-256 |
|---|---|---|---|
| Signature Size | 64 bytes | 256 bytes | 64 bytes |
| Public Key Size | 32 bytes | 256 bytes | 64 bytes |
| Security Level | ~128-bit | ~112-bit | ~128-bit |
| Verification Speed | < 0.1ms | ~0.2ms | ~0.3ms |
| Deterministic Signing | Yes | No (padding) | No (RNG required) |
| RNG Dependency | None | PKCS#1 v1.5 padding | Critical — weak RNG leaks private key |
| Padding Oracle Attacks | Immune | Vulnerable without OAEP | N/A |
| Side-Channel Resistance | Designed-in | Implementation-dependent | Implementation-dependent |
Ed25519 wins on every axis that matters for software licensing:
- Deterministic signatures — The same input always produces the same signature. No randomness dependency means no risk of a weak RNG leaking your signing key (a real vulnerability that has compromised ECDSA deployments in production)
- Compact payload — 64-byte signatures keep license files small. RSA signatures are 4× larger for weaker security
- Sub-millisecond verification — Verification completes in microseconds, adding zero perceptible latency to application startup
- No padding attacks — Ed25519 has no padding scheme, eliminating an entire class of attacks that plague RSA implementations
- Constant-time operations — The algorithm is designed from the ground up to resist timing side-channel attacks, critical when your verification code runs on untrusted client hardware
Architecture: How Offline Validation Works
The offline validation architecture follows a cache-first pattern with cryptographic verification:
┌─────────────────────────────────────────────────────────────┐
│ ONLINE PHASE (one-time) │
│ │
│ App Startup ──▶ API Call ──▶ Server Signs License Payload │
│ with Ed25519 Private Key │
│ │ │
│ ▼ │
│ Signed Payload Returned │
│ (license data + signature) │
│ │ │
│ ▼ │
│ Cache Signed Payload Locally │
│ (encrypted at rest on disk) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ OFFLINE PHASE (every startup) │
│ │
│ App Startup ──▶ Load Cached Payload from Disk │
│ │ │
│ ▼ │
│ Ed25519 Verify Signature │
│ (using embedded public key) │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ Valid Invalid │
│ │ │ │
│ ▼ ▼ │
│ Check Expiry Reject & │
│ │ Require Online │
│ ┌─────┴─────┐ Revalidation │
│ │ │ │
│ Active Expired │
│ │ │ │
│ ▼ ▼ │
│ Grant Access Grace Period │
│ or Degraded Mode │
└─────────────────────────────────────────────────────────────┘
The key insight: the signature proves the license data originated from your server and has not been tampered with. No network call is needed to verify authenticity — only the embedded public key.
Step 1: Generate the Ed25519 Key Pair
Your licensing server holds the private key. Your application binary embeds the public key. Generate the pair once and rotate on a defined schedule (annually is typical for licensing keys).
// Key generation (run once, store securely)
import { generateKeyPair } from '@traffic-orchestrator/client'
const { publicKey, privateKey } = await generateKeyPair()
// publicKey: 32 bytes, safe to embed in your application
// privateKey: 64 bytes, store in your secrets manager — NEVER ship this
console.log('Public Key (embed in app):', publicKey)
console.log('Private Key (server only):', privateKey)
The public key is safe to distribute. It can verify signatures but cannot create them. Even if an attacker extracts it from your binary, they cannot forge license files.
Step 2: Online Validation Returns a Signed Payload
The first time your application validates online, the server returns both the license verdict and a cryptographically signed payload for offline use:
// First validation — online, returns signed offline payload
import TrafficOrchestrator from '@traffic-orchestrator/client'
const to = new TrafficOrchestrator({
apiKey: process.env.TO_API_KEY,
publicKey: 'your-ed25519-public-key-base64'
})
const result = await to.validate({
licenseKey: 'LK-ACME-2026-PRO-XXXX',
domain: 'app.acmecorp.com'
})
if (result.valid) {
// result.offlineToken contains the signed payload
// This is the data you cache for offline verification
console.log('License valid, offline token received')
console.log('Token expires:', result.offlineToken.expiresAt)
}
Step 3: Cache the Signed Payload Locally
Store the signed payload on disk, encrypted at rest. The payload contains the license metadata (plan, features, expiration, domains) and the Ed25519 signature that proves its authenticity.
import { writeFileSync, readFileSync } from 'fs'
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
// Encrypt before writing to disk
const encryptAndStore = (offlineToken: string, encryptionKey: Buffer) => {
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-gcm', encryptionKey, iv)
const encrypted = Buffer.concat([
cipher.update(offlineToken, 'utf8'),
cipher.final()
])
const authTag = cipher.getAuthTag()
writeFileSync('.license', Buffer.concat([iv, authTag, encrypted]))
}
// Decrypt when loading from disk
const loadFromDisk = (encryptionKey: Buffer): string => {
const data = readFileSync('.license')
const iv = data.subarray(0, 16)
const authTag = data.subarray(16, 32)
const encrypted = data.subarray(32)
const decipher = createDecipheriv('aes-256-gcm', encryptionKey, iv)
decipher.setAuthTag(authTag)
return Buffer.concat([
decipher.update(encrypted),
decipher.final()
]).toString('utf8')
}
Encrypting at rest prevents casual inspection and modification of the cached license file. Even though the Ed25519 signature already protects against tampering, encryption adds defense-in-depth.
Step 4: Offline Verification with Ed25519
On subsequent launches, your application loads the cached payload and verifies the signature locally — no network required:
// Offline verification — zero network calls
const offlineResult = await to.verifyOffline({
cachedToken: loadFromDisk(encryptionKey)
})
if (offlineResult.valid) {
// Signature verified, license data is authentic
// Check feature entitlements
const features = offlineResult.features
console.log('Plan:', offlineResult.plan)
console.log('Features:', features)
console.log('Expires:', offlineResult.expiresAt)
} else {
// Signature invalid or token expired
// Attempt online revalidation
console.log('Offline token invalid, reason:', offlineResult.reason)
attemptOnlineRevalidation()
}
The verifyOffline() method performs three checks:
- Signature verification — Confirms the payload was signed by your server’s private key
- Expiration check — Validates the offline token has not expired
- Integrity check — Ensures no field in the payload has been modified since signing
Step 5: Handle Expiration Gracefully
Offline tokens have a finite lifetime (typically 30–90 days). When they expire, your application needs a strategy beyond hard-locking the user:
const handleOfflineExpiry = async (offlineResult) => {
const daysUntilExpiry = offlineResult.daysRemaining
if (daysUntilExpiry > 7) {
// Normal operation — silently attempt background refresh
tryBackgroundRefresh()
} else if (daysUntilExpiry > 0) {
// Grace period — show renewal prompt
showRenewalBanner(daysUntilExpiry)
} else if (daysUntilExpiry > -14) {
// Expired but within grace window — degraded mode
enableDegradedMode()
showReconnectPrompt()
} else {
// Hard expired — require online revalidation
requireOnlineRevalidation()
}
}
// Degraded mode: core functionality works, premium features disabled
const enableDegradedMode = () => {
disableFeature('advanced-analytics')
disableFeature('bulk-export')
disableFeature('api-access')
// Core editing, viewing, and local saves remain functional
}
The key principle: never hard-lock a user without warning. A medical device that stops functioning because a license token expired during a procedure is a liability, not a feature. Degraded mode preserves core functionality while clearly communicating the need to reconnect.
Multi-Language Verification Examples
Ed25519 verification is supported natively in every major language. Here are production-ready examples using Traffic Orchestrator SDKs:
Node.js / TypeScript
import TrafficOrchestrator from '@traffic-orchestrator/client'
const to = new TrafficOrchestrator({
apiKey: process.env.TO_API_KEY,
publicKey: process.env.TO_PUBLIC_KEY
})
// Online validation with offline token caching
const result = await to.validate({
licenseKey: 'LK-XXXX-XXXX-XXXX',
domain: 'app.example.com',
enableOffline: true // Server returns a signed offline token
})
// Later, offline verification (no network)
const offline = await to.verifyOffline({
cachedToken: loadCachedToken()
})
if (offline.valid) {
console.log(`License active until ${offline.expiresAt}`)
}
Python
from traffic_orchestrator import TrafficOrchestrator
to = TrafficOrchestrator(
api_key="your-api-key",
public_key="your-ed25519-public-key"
)
# Online validation with offline caching
result = to.validate(
license_key="LK-XXXX-XXXX-XXXX",
domain="app.example.com",
enable_offline=True
)
# Offline verification
offline = to.verify_offline(cached_token=load_cached_token())
if offline.valid:
print(f"License active, plan: {offline.plan}")
print(f"Features: {', '.join(offline.features)}")
else:
print(f"Offline verification failed: {offline.reason}")
Go
package main
import (
"fmt"
to "github.com/TrafficOrchestrator/go-sdk"
)
func main() {
client := to.NewClient(
to.WithAPIKey("your-api-key"),
to.WithPublicKey("your-ed25519-public-key"),
)
// Online validation with offline token
result, err := client.Validate(to.ValidateRequest{
LicenseKey: "LK-XXXX-XXXX-XXXX",
Domain: "app.example.com",
EnableOffline: true,
})
if err != nil {
fmt.Printf("Validation error: %v\n", err)
return
}
// Offline verification
offline, err := client.VerifyOffline(loadCachedToken())
if err != nil {
fmt.Printf("Offline verification error: %v\n", err)
return
}
if offline.Valid {
fmt.Printf("License active, plan: %s\n", offline.Plan)
}
}
Security Considerations
Offline validation introduces attack surfaces that online validation avoids. Address each one:
Key Pinning
Embed the public key directly in your compiled binary, not in a configuration file. Attackers should not be able to swap the public key for one they control. For compiled languages (Go, Rust, C++), the key becomes part of the binary. For interpreted languages (Python, Node.js), use integrity checks on the key file.
Anti-Tampering
Sign the entire license payload — plan tier, features, expiration date, domain restrictions, device fingerprint. If any field can be modified without invalidating the signature, it will be modified. The Ed25519 signature covers the full payload, so any byte-level change causes verification failure.
Clock Manipulation Detection
Clients can set their system clock backward to extend expired licenses. Defend against this with monotonic timestamp tracking:
const detectClockManipulation = (lastKnownTimestamp: number): boolean => {
const currentTime = Date.now()
// If clock moved backward by more than 5 minutes, flag it
if (currentTime < lastKnownTimestamp - 300000) {
return true // Clock manipulation detected
}
// Store current time for next check
persistTimestamp(currentTime)
return false
}
Key Versioning
When you rotate signing keys, include a key version identifier in the signed payload. The verification code checks which public key to use based on the version field. This allows seamless key rotation without invalidating existing offline tokens.
Revocation for Intermittently Connected Systems
Maintain a signed revocation list. When connectivity is available, your application downloads the latest revocation list and checks it during offline verification. The revocation list itself is Ed25519-signed to prevent tampering.
Algorithm Comparison: When to Use What
While Ed25519 is the recommended choice for new implementations, here is a detailed comparison for teams evaluating alternatives:
| Criterion | Ed25519 | RSA-2048 | ECDSA P-256 |
|---|---|---|---|
| Best For | New projects, modern stacks | Legacy system compatibility | Existing PKI infrastructure |
| Signature + Key Overhead | 96 bytes total | 512 bytes total | 128 bytes total |
| Signing Speed | < 0.1ms | ~2ms | ~0.3ms |
| Verification Speed | < 0.1ms | ~0.2ms | ~0.3ms |
| RNG Failure Risk | Zero | Low (padding-dependent) | Critical (key exposure) |
| Library Availability | Excellent (2015+) | Universal | Excellent |
| FIPS 140-2 Certified | Yes (via Ed25519ph) | Yes | Yes |
| Quantum Resistance | No | No | No |
Use RSA only if you must support legacy environments (pre-2015 embedded systems, older Java runtimes) that lack Ed25519 support. For everything else, Ed25519 is the correct choice.
Implementation Checklist
Before shipping offline validation to production:
- Key management — Private key in a secrets manager (not in source code, not in environment variables on developer machines)
- Key rotation plan — Document the key rotation procedure, including how to re-sign active offline tokens
- Grace period configured — Define degraded mode behavior and the grace window (7–14 days is standard)
- Clock manipulation detection — Monotonic timestamp tracking implemented and tested
- Encrypted local storage — Cached tokens encrypted at rest with AES-256-GCM
- Revocation list sync — For intermittently connected environments, periodic sync of signed revocation lists
- Telemetry on reconnect — When the application regains connectivity, report offline usage metrics back to your licensing server
- Multi-device testing — Verify offline behavior on every target platform (Windows, macOS, Linux, embedded)
Ship Offline Validation in Your Next Release
Traffic Orchestrator provides Ed25519 offline verification, signed offline tokens, and automatic cache management across 12 SDKs. Offline activation is available on Professional plans and above.
View Plans & PricingShip licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.