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.
How It Works
Section titled “How It Works”The license client supports a three-tier fallback chain for validation:
- Online Validation — normal operation, validates against the server.
- Offline License File —
.licfile for long-term offline operation. - Grace Period Token — JWT, typically 72 hours, for short-term network outages.
- If all three fail, the license is considered invalid.
Grace Period vs Offline License
Section titled “Grace Period vs Offline License”| Grace Period Token | Offline License File | |
|---|---|---|
| Use case | Temporary network outage | Permanent air-gapped operation |
| Duration | Typically 72 hours | Up to 365 days (configurable) |
| Requires server contact | Yes (token issued during online validation) | No (file created once via admin API) |
| Hardware binding | No | Optional (mandatory when decryption key is included) |
| Entitlements | Cached from last online validation | Embedded in the .lic file |
Server Setup
Section titled “Server Setup”Signing Keys
Section titled “Signing Keys”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) andsigning.pub(public) - Separate from the vendor server license keys (those live under
data/keys/v1.0.0/)
Retrieving the Public Key
Section titled “Retrieving the Public Key”The public key is needed by clients to verify offline license signatures. Retrieve it via the admin API:
# Get the PEM-encoded public signing keycurl -H "Authorization: Bearer $TOKEN" \ https://license.example.com/api/signing-keyResponse:
{ "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.
Creating Offline Licenses
Section titled “Creating Offline Licenses”Administrators create offline license files using the checkout endpoint.
API Endpoint
Section titled “API Endpoint”POST /api/licenses/{id}/checkoutRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
ttl_days | integer | Yes | Validity in days (1—365) |
hardware_id | string | No | Bind to specific machine. Required if the license has a decryption key. |
Example:
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.licResponse:
| Field | Type | Description |
|---|---|---|
license_file | bytes | The signed .lic file content |
filename | string | Suggested filename for download |
valid_until | timestamp | When the offline license expires |
Security Rules
Section titled “Security Rules”- 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_idis mandatory. The server returnsHARDWARE_ID_REQUIREDif omitted. - The license and customer must both be active at checkout time.
- Each checkout is recorded in the audit log.
Client Integration (Go)
Section titled “Client Integration (Go)”Basic Setup
Section titled “Basic Setup”import ( "encoding/pem"
client "git.prd.embidio.de/hive/license-server/client")
// Decode the embedded public keyblock, _ := pem.Decode([]byte(embeddedPublicKeyPEM))publicKeyBytes := block.Bytes
// Create client with offline supportomc, 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()Validation with Automatic Fallback
Section titled “Validation with Automatic Fallback”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 }}Offline Event Callbacks
Section titled “Offline Event Callbacks”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")})Configuration Reference
Section titled “Configuration Reference”| Field | Type | Default | Description |
|---|---|---|---|
Enabled | bool | false | Enable offline mode support |
CacheDir | string | OS config dir | Directory for offline cache data |
LicenseFile | string | — | Path to the .lic file |
PublicKey | []byte | — | Ed25519 public key for signature verification |
RetryInterval | Duration | 30s | Initial reconnection retry interval |
MaxRetryInterval | Duration | 5m | Maximum retry interval (exponential backoff) |
MaxRetryAttempts | int | 0 (unlimited) | Max reconnection attempts |
RestrictedEntitlements | []string | — | Entitlement codes disabled during offline mode |
AllowOfflineUsageReporting | bool | false | Queue usage reports while offline |
.lic File Format
Section titled “.lic File Format”The .lic file is a Base64-encoded JSON payload with an Ed25519 signature.
Payload Structure
Section titled “Payload Structure”{ "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..."}Field Reference
Section titled “Field Reference”| Field | Description |
|---|---|
version | Format version for forward compatibility |
license_id | UUID reference to the server-side license |
license_key_prefix | First 8 characters of the license key (identification only) |
customer_name | Name of the licensed customer |
license_type | time_based, usage_based, or hybrid |
valid_from / valid_until | Validity window (RFC 3339) |
hardware_id | Machine binding (empty if not hardware-bound) |
entitlements | Licensed features with optional usage limits |
encrypted_key | AES-256-GCM encrypted decryption key, present only when the license carries a decryption key and hardware binding is active |
issued_at | When the offline license was generated |
issuer | Identifier of the issuing server |
signature | Ed25519 signature over all fields except signature itself |
Signature Verification
Section titled “Signature Verification”- Remove the
signaturefield from the JSON payload. - Marshal the remaining fields to canonical JSON.
- Verify using
ed25519.Verify(publicKey, payloadBytes, signatureBytes).
Hardware Binding
Section titled “Hardware Binding”Hardware binding ties an offline license to a specific machine, preventing file copying.
When Is It Required?
Section titled “When Is It Required?”- With decryption key (
encrypted_keypresent): Hardware binding is mandatory. The server refuses checkout without ahardware_id. - Without decryption key (feature gating only): Hardware binding is optional.
How It Works
Section titled “How It Works”-
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)
-
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.
Hardware ID Generation
Section titled “Hardware ID Generation”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",}Troubleshooting
Section titled “Troubleshooting”| Error | Cause | Solution |
|---|---|---|
signature verification failed | .lic file was tampered with, or wrong public key | Ensure the embedded public key matches the server’s signing key (GET /api/signing-key) |
license expired | The .lic file’s valid_until has passed | Generate a new offline license via the checkout endpoint |
license not yet valid | The .lic file’s valid_from is in the future | Check system clock; the machine’s time may be wrong |
hardware ID mismatch | The .lic was generated for a different machine | Re-checkout with the correct hardware_id for this machine |
failed to decrypt license key | Hardware ID does not match the one used at checkout | Same as hardware ID mismatch — the decryption key is bound to the original hardware |
HARDWARE_ID_REQUIRED | Checkout attempted without hardware_id for a license with a decryption key | Provide a hardware_id in the checkout request |
license file path is required | LicenseFile not set in OfflineConfig | Set OfflineConfig.LicenseFile to the path of the .lic file |
public key is required | PublicKey not set in OfflineConfig | Set OfflineConfig.PublicKey with the server’s Ed25519 public key bytes |
Inspecting a .lic File
Section titled “Inspecting a .lic File”To inspect a .lic file manually:
# Decode and pretty-print the license payloadbase64 -d license.lic | jq .This shows all fields including valid_until, hardware_id, and entitlements for debugging.