Skip to content

Offline Licensing

Offline licensing enables license validation without network connectivity using cryptographically signed .lic files. This is designed for air-gapped environments, industrial systems, defense installations, and edge deployments where reliable internet access is not available.

The license client supports a three-tier fallback chain for validation:

  1. Online Validation — normal operation, validates against the server.
  2. Offline License File.lic file for long-term offline operation.
  3. Grace Period Token — JWT, typically 72 hours, for short-term network outages.
  4. If all three fail, the license is considered invalid.
Grace Period TokenOffline License File
Use caseTemporary network outagePermanent air-gapped operation
DurationTypically 72 hoursUp to 365 days (configurable)
Requires server contactYes (token issued during online validation)No (file created once via admin API)
Hardware bindingNoOptional (mandatory when decryption key is included)
EntitlementsCached from last online validationEmbedded in the .lic file

The license server automatically generates an Ed25519 key pair on first startup. These keys are used to sign offline license files.

  • Storage: data/keys/server/signing.key (private) and signing.pub (public)
  • Separate from the vendor server license keys (those live under data/keys/v1.0.0/)

The public key is needed by clients to verify offline license signatures. Retrieve it via the admin API:

Terminal window
# Get the PEM-encoded public signing key
curl -H "Authorization: Bearer $TOKEN" \
https://license.example.com/api/signing-key

Response:

{
"public_key_pem": "<PEM-encoded Ed25519 public key>"
}

Embed this key in your client application at build time (recommended) or distribute it alongside the .lic file. See the Public Key Embedding guide for detailed instructions.


Administrators create offline license files using the checkout endpoint.

POST /api/licenses/{id}/checkout

Request Body:

FieldTypeRequiredDescription
ttl_daysintegerYesValidity in days (1—365)
hardware_idstringNoBind to specific machine. Required if the license has a decryption key.

Example:

Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ttl_days": 90, "hardware_id": "hw-abc123"}' \
https://license.example.com/api/licenses/550e8400-e29b-41d4-a716-446655440000/checkout \
--output license.lic

Response:

FieldTypeDescription
license_filebytesThe signed .lic file content
filenamestringSuggested filename for download
valid_untiltimestampWhen the offline license expires
  • The offline license can never be valid longer than the original license. TTL is capped: valid_until = min(now + ttl_days, license.valid_until).
  • If the license contains a decryption key (for model/data encryption), a hardware_id is mandatory. The server returns HARDWARE_ID_REQUIRED if omitted.
  • The license and customer must both be active at checkout time.
  • Each checkout is recorded in the audit log.

import (
"encoding/pem"
client "git.prd.embidio.de/hive/license-server/client"
)
// Decode the embedded public key
block, _ := pem.Decode([]byte(embeddedPublicKeyPEM))
publicKeyBytes := block.Bytes
// Create client with offline support
omc, err := client.NewOfflineModeClient(
client.Config{
ServerAddr: "license.example.com:9090",
LicenseKey: "XXXX-XXXX-XXXX-XXXX",
Version: "1.0.0",
},
client.OfflineConfig{
Enabled: true,
CacheDir: "/var/lib/myapp/cache",
RetryInterval: 30 * time.Second,
LicenseFile: "/path/to/license.lic",
PublicKey: publicKeyBytes,
},
)
if err != nil {
log.Fatal(err)
}
defer omc.Close()
resp, err := omc.ValidateWithOfflineSupport(ctx)
if err != nil {
log.Fatalf("validation failed: %v", err)
}
if resp.Valid {
fmt.Printf("License valid (offline: %v)\n", resp.IsOffline)
// Check specific entitlements
if resp.HasEntitlement("DRONE_DETECTION") {
// Feature is licensed
}
// Get decryption key (if hardware-bound)
if key := resp.DecryptionKey; key != nil {
// Use key for model/data decryption
}
}
omc.OnOfflineModeEnter(func(remaining time.Duration) {
log.Printf("Entered offline mode, grace period: %v", remaining)
})
omc.OnOfflineModeExit(func() {
log.Println("Back online")
})
omc.OnGracePeriodExpired(func() {
log.Println("Grace period expired, shutting down gracefully")
})

FieldTypeDefaultDescription
EnabledboolfalseEnable offline mode support
CacheDirstringOS config dirDirectory for offline cache data
LicenseFilestringPath to the .lic file
PublicKey[]byteEd25519 public key for signature verification
RetryIntervalDuration30sInitial reconnection retry interval
MaxRetryIntervalDuration5mMaximum retry interval (exponential backoff)
MaxRetryAttemptsint0 (unlimited)Max reconnection attempts
RestrictedEntitlements[]stringEntitlement codes disabled during offline mode
AllowOfflineUsageReportingboolfalseQueue usage reports while offline

The .lic file is a Base64-encoded JSON payload with an Ed25519 signature.

{
"version": 1,
"license_id": "550e8400-e29b-41d4-a716-446655440000",
"license_key_prefix": "ABCD1234",
"customer_name": "ACME Corp",
"license_type": "time_based",
"valid_from": "2026-02-06T00:00:00Z",
"valid_until": "2026-05-06T00:00:00Z",
"hardware_id": "hw-abc123...",
"entitlements": [
{"code": "DRONE_DETECTION", "name": "Drone Detection", "usage_limit": null},
{"code": "ANALYTICS", "name": "Analytics", "usage_limit": 1000}
],
"encrypted_key": "base64...",
"issued_at": "2026-02-06T12:00:00Z",
"issuer": "ACME License Server",
"signature": "base64..."
}
FieldDescription
versionFormat version for forward compatibility
license_idUUID reference to the server-side license
license_key_prefixFirst 8 characters of the license key (identification only)
customer_nameName of the licensed customer
license_typetime_based, usage_based, or hybrid
valid_from / valid_untilValidity window (RFC 3339)
hardware_idMachine binding (empty if not hardware-bound)
entitlementsLicensed features with optional usage limits
encrypted_keyAES-256-GCM encrypted decryption key, present only when the license carries a decryption key and hardware binding is active
issued_atWhen the offline license was generated
issuerIdentifier of the issuing server
signatureEd25519 signature over all fields except signature itself
  1. Remove the signature field from the JSON payload.
  2. Marshal the remaining fields to canonical JSON.
  3. Verify using ed25519.Verify(publicKey, payloadBytes, signatureBytes).

Hardware binding ties an offline license to a specific machine, preventing file copying.

  • With decryption key (encrypted_key present): Hardware binding is mandatory. The server refuses checkout without a hardware_id.
  • Without decryption key (feature gating only): Hardware binding is optional.
  1. At checkout: The server encrypts the decryption key using a key derived from the hardware ID:

    • derivedKey = HKDF-SHA256(hardware_id, salt=license_id)
    • encrypted_key = AES-256-GCM(derivedKey, decryption_key)
  2. At validation: The client derives the same key from its own hardware ID and decrypts:

    • If the hardware ID matches, decryption succeeds.
    • If mismatched, decryption fails with HARDWARE_MISMATCH.

The Go client automatically generates a hardware ID from machine-specific attributes (CPU ID, MAC addresses, disk serial numbers). You can also provide a custom hardware ID:

client.Config{
HardwareID: "custom-hw-id-from-your-system",
}

ErrorCauseSolution
signature verification failed.lic file was tampered with, or wrong public keyEnsure the embedded public key matches the server’s signing key (GET /api/signing-key)
license expiredThe .lic file’s valid_until has passedGenerate a new offline license via the checkout endpoint
license not yet validThe .lic file’s valid_from is in the futureCheck system clock; the machine’s time may be wrong
hardware ID mismatchThe .lic was generated for a different machineRe-checkout with the correct hardware_id for this machine
failed to decrypt license keyHardware ID does not match the one used at checkoutSame as hardware ID mismatch — the decryption key is bound to the original hardware
HARDWARE_ID_REQUIREDCheckout attempted without hardware_id for a license with a decryption keyProvide a hardware_id in the checkout request
license file path is requiredLicenseFile not set in OfflineConfigSet OfflineConfig.LicenseFile to the path of the .lic file
public key is requiredPublicKey not set in OfflineConfigSet OfflineConfig.PublicKey with the server’s Ed25519 public key bytes

To inspect a .lic file manually:

Terminal window
# Decode and pretty-print the license payload
base64 -d license.lic | jq .

This shows all fields including valid_until, hardware_id, and entitlements for debugging.