A complete specification for implementing License Kit clients in any programming language or architecture — a reference for developers (or AI assistants) building compatible client libraries.
Activation → Token Receipt → Offline Validation → Periodic Refresh → Expiration/Renewal
- Generate stable device fingerprint
- Store tokens securely
- Validate tokens offline
- Refresh tokens before expiration
- Handle grace periods
- Enforce license restrictions
Purpose: Create a unique, stable identifier for the device/installation
Requirements:
- MUST be deterministic (same device = same fingerprint)
- MUST NOT contain PII (personally identifiable information)
- MUST be reproducible after app restart
- SHOULD survive minor system changes
Implementation:
fingerprint = SHA256(
hardware_id +
mac_address +
cpu_id +
installation_path +
app_identifier
)
Platform-specific sources:
- Windows: Registry MachineGuid, Volume Serial, CPU ID
- macOS: IOPlatformUUID, Hardware UUID
- Linux: /etc/machine-id, DMI product UUID
- Docker: Container ID + Host fingerprint
- Web: LocalStorage ID + Canvas fingerprint
- Mobile: Device ID + App Installation ID
Purpose: Securely store offline tokens and metadata
Storage Requirements:
{
"token": "v2.public.eyJ...",
"public_key_bundle": {
"root": {
"kid": "kid_xxx",
"public_key": "-----BEGIN PUBLIC KEY-----..."
}
},
"cached_at": "2024-01-01T00:00:00Z",
"last_verified": "2024-01-01T00:00:00Z",
"last_online_check": "2024-01-01T00:00:00Z"
}Security Requirements:
- MUST encrypt token at rest (using OS keychain/credential manager)
- MUST protect against tampering
- SHOULD use platform-specific secure storage:
- Windows: Credential Manager / DPAPI
- macOS: Keychain
- Linux: libsecret / gnome-keyring
- Mobile: iOS Keychain / Android Keystore
- Web: IndexedDB with Web Crypto API
Purpose: Validate tokens offline without server contact
PASETO v2.public Token Structure:
v2.public.<payload>.<footer>
Verification Steps:
- Parse token into parts
- Extract footer (contains kid and certificate chain)
- Verify certificate chain against root public key
- Confirm the certificate binds the signing public key — constant-time compare the key vouched for by the certificate against the signing key in the footer, and reject on mismatch (prevents a genuine certificate being paired with an attacker key to forge a token)
- Verify token signature using the signing public key
- Check token claims
Required Claim Validations:
{
"iat": "2024-01-01T00:00:00Z", // Issued at - check not future
"nbf": "2024-01-01T00:00:00Z", // Not before - check if valid
"exp": "2024-01-08T00:00:00Z", // Expiration - check not expired
"sub": "123", // License ID
"iss": "laravel-licensing", // Issuer - must match config
"license_id": 123,
"license_key_hash": "sha256...",
"usage_fingerprint": "sha256...", // Must match current device
"status": "active", // Must be active or grace
"max_usages": 5,
"force_online_after": "2024-01-14T00:00:00Z",
"licensable_type": "App\\Models\\Team",
"licensable_id": 456,
"license_expires_at": "2024-12-31T23:59:59Z", // Optional
"grace_until": "2024-01-14T23:59:59Z" // Optional
}Validation Rules:
- Token not expired (
exp> now) - Token is valid (
nbf<= now) - Fingerprint matches current device
- Status is usable (active or grace)
- Not past force_online_after date
- Clock skew tolerance: ±60 seconds
Purpose: Exchange activation key for offline token
API Request:
POST /api/licensing/v1/activate
Content-Type: application/json
{
"license_key": "LIC-A3F2B9K1-C4D8E5H7-9D2EK8F3-L6A9M1B4",
"fingerprint": "sha256_hash",
"metadata": {
"client_type": "desktop",
"name": "John's MacBook",
"app_version": "1.0.0",
"os": "macOS 14.0"
}
}API Response (Success):
{
"success": true,
"data": {
"token": "v2.public.eyJ...",
"public_key_bundle": {...},
"refresh_after": "2024-01-07T00:00:00Z",
"force_online_after": "2024-01-14T00:00:00Z"
}
}API Response (Error):
{
"success": false,
"error": {
"code": "USAGE_LIMIT_REACHED",
"message": "License has reached maximum usages",
"hint": "Deactivate another device first"
}
}Error Codes:
INVALID_KEY: License key not found or invalidEXPIRED_LICENSE: License has expiredSUSPENDED_LICENSE: License is suspendedUSAGE_LIMIT_REACHED: Max usages exceededFINGERPRINT_MISMATCH: Fingerprint validation failedRATE_LIMITED: Too many requests
Purpose: Get new token before expiration
Refresh Strategy:
if (token_expires_in < 24_hours || force_online_soon) {
try {
new_token = refresh_online()
store(new_token)
} catch (NetworkError) {
if (token_still_valid) {
continue_offline()
} else {
block_access()
}
}
}
API Request:
POST /api/licensing/v1/refresh
Authorization: Bearer <current_token>
{
"fingerprint": "sha256_hash"
}Purpose: Update last_seen_at to maintain active status
When to Send:
- On application startup
- Every 24 hours while running
- Before extended offline period
API Request:
POST /api/licensing/v1/heartbeat
Authorization: Bearer <current_token>
{
"fingerprint": "sha256_hash",
"metadata": {
"app_version": "1.0.1",
"last_used_features": ["feature_a", "feature_b"]
}
}Purpose: Block access to protected features
Pseudocode:
def require_license(feature=None):
license = load_cached_license()
# Step 1: Verify token offline
if not verify_token_offline(license.token):
return block_access("Invalid or expired license")
# Step 2: Check force online
if past_force_online_date(license):
if not refresh_license_online():
return block_access("Online verification required")
# Step 3: Check feature entitlements (if applicable)
if feature and feature not in license.entitlements:
return block_access(f"Feature {feature} not licensed")
# Step 4: Check grace period
if license.status == "grace":
show_warning(f"License expires in {license.grace_days_remaining} days")
return allow_access()Application Start
↓
Load Cached Token
↓
Verify Offline → [Valid] → Check Force Online
↓ ↓
[Invalid] [Needed]
↓ ↓
Prompt for Key Refresh Online
↓ ↓
Activate Online Update Cache
↓ ↓
Store Token Continue
Every Hour/Day
↓
Check Token Expiry
↓
[Expires Soon?]
↓
Yes → Try Refresh → [Success] → Update Cache
↓
[Fail] → Continue if Valid
Validation Failed
↓
[Network Available?]
↓ ↓
Yes No
↓ ↓
Refresh Online Check Grace Period
↓ ↓
[Success?] [In Grace?]
↓ ↓
Update Allow with Warning
↓ ↓
Continue Block after Grace
- Store tokens in OS credential manager
- Generate fingerprint from hardware IDs
- Show system tray notifications for expiry
- Handle sleep/wake for heartbeat
- Use IndexedDB for token storage
- Implement service worker for offline validation
- Handle tab visibility for heartbeat
- Use Web Crypto API for verification
- Use platform keychain/keystore
- Handle app backgrounding
- Request network permission for refresh
- Implement push notifications for expiry
- Use filesystem with permissions 600
- Generate fingerprint from container/VM ID
- Implement systemd timer for heartbeat
- Log validation failures to syslog
- Mount license as secret
- Use pod/container ID in fingerprint
- Implement init container for activation
- Use liveness probe for validation
- Never log full tokens
- Encrypt tokens at rest
- Validate token integrity
- Implement rate limiting on validation
- Clear tokens on deactivation
- Always use HTTPS for API calls
- Implement certificate pinning (optional)
- Validate server certificates
- Handle man-in-the-middle attacks
- Implement exponential backoff
- Hash all hardware IDs
- Never send raw hardware info
- Allow user to view fingerprint
- Document what data is collected
- Comply with GDPR/privacy laws
- Fingerprint generation consistency
- Token parsing and validation
- Clock skew handling
- Grace period calculation
- Error response handling
- Full activation flow
- Token refresh flow
- Offline validation
- Force online handling
- Network failure recovery
- Secure storage access
- Hardware ID availability
- Network permission handling
- Background task execution
- UI notification display
A minimal client MUST implement:
- Fingerprint generation (stable device ID)
- Token storage (encrypted)
- Offline PASETO verification (with RSA)
- License activation (exchange key for token)
- Basic enforcement (block if invalid)
Optional features for enhanced functionality:
- Token refresh
- Heartbeat reporting
- Grace period handling
- Feature-level enforcement
- Audit logging
- Telemetry collection
import hashlib
import json
from datetime import datetime
from paseto import PasetoV2
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
class LicenseClient:
def __init__(self, server_url, public_key_bundle):
self.server_url = server_url
self.public_key = self.load_public_key(public_key_bundle)
self.token = None
def generate_fingerprint(self):
# Platform-specific implementation
hardware_id = self.get_hardware_id()
return hashlib.sha256(hardware_id.encode()).hexdigest()
def activate(self, license_key):
fingerprint = self.generate_fingerprint()
response = requests.post(
f"{self.server_url}/activate",
json={
"license_key": license_key,
"fingerprint": fingerprint
}
)
if response.ok:
self.token = response.json()["data"]["token"]
self.save_token(self.token)
return True
return False
def verify(self):
if not self.token:
self.token = self.load_token()
try:
# Verify PASETO token
payload = PasetoV2.verify(
self.token,
self.public_key,
options={"verify_exp": True}
)
# Check fingerprint
if payload["usage_fingerprint"] != self.generate_fingerprint():
return False
# Check force online
force_online = datetime.fromisoformat(payload["force_online_after"])
if datetime.now() > force_online:
return self.refresh()
return payload["status"] in ["active", "grace"]
except Exception:
return False
def require_license(self, func):
def wrapper(*args, **kwargs):
if not self.verify():
raise Exception("Invalid license")
return func(*args, **kwargs)
return wrapperClients MUST:
- Document all data collected for fingerprinting
- Provide privacy policy compliance
- Allow users to view collected data
- Implement data deletion on request
- Comply with GDPR Article 17 (Right to Erasure)
- Implement region restrictions if required
- Check license against embargo lists
- Log access attempts from restricted regions
- Log all license validations
- Record activation/deactivation events
- Track feature usage if required
- Implement tamper-evident logging
- Clock Skew: System time off by >60 seconds
- Fingerprint Drift: Hardware changes causing mismatch
- Token Corruption: Storage failure or tampering
- Network Isolation: Can't refresh when required
- Permission Issues: Can't access secure storage
Clients SHOULD provide debug command/UI showing:
- Current fingerprint
- Token expiration
- Last online check
- License status
- Error logs
- Public key bundle
- Manual token refresh command
- License reset option
- Fingerprint regeneration
- Offline activation via file
- Emergency grace period extension
Current: v1
Token Format: PASETO v2.public
Clients MUST handle:
- New required claims in tokens
- Changed API endpoints
- Updated crypto algorithms
- Modified fingerprint requirements
- 6 months notice for breaking changes
- 12 months support for old protocol
- Clear migration guides
- Backward compatibility when possible