Offline-first, cryptographically secure licensing client for Python apps using the Icare Licensing Server
Project description
Icare Licensing Client
icare-licensing is an offline-first, cryptographically secure licensing client library for Python applications. It integrates seamlessly with the Icare Licensing Server using RSA-SHA256 signatures, OS-native stable hardware bindings, and automated offline-resilient sync policies.
Features
- OS-Native Stable Hardware Binding (HWID): Generates a stable, reproducible machine signature using OS-level identifiers — not fragile MAC addresses.
- Offline Cryptographic Validation: Validates license state offline using RSA-SHA256 without calling the server on every startup.
- Automated Sync Daemon: Runs in the background and pings the verification endpoint every 24 hours.
- 3-Day Offline Grace Period: Remains fully functional offline for up to 3 days before requiring internet.
- Instant Revocation Purge: Deletes local signatures and metadata immediately upon server-side license revocation or deletion to prevent offline bypass.
Installation
pip install icare-licensing
Hardware ID (HWID) Strategy
The library generates a stable, cross-platform HWID using the following priority chain:
| Priority | Platform | Source |
|---|---|---|
| 1st | Windows | HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid — set once at OS install |
| 2nd | Linux | /etc/machine-id — set once at first boot |
| 3rd | macOS | IOPlatformSerialNumber from ioreg |
| 4th | All (fallback) | MAC address — only if hardware-backed (not a randomly generated address) |
Why not
uuid.getnode()? Python'suuid.getnode()silently returns a random 48-bit number when no hardware network interface is found (e.g. in VMs, Docker containers, or systems with only virtual NICs). This would cause a legitimately activated license to become invalid on the next restart. This library detects and rejects randomly-generated MAC addresses automatically.
License Storage & File Location Policy
By default, the client writes license.lic and license_metadata.json relative to the Current Working Directory (CWD).
Critical Production Guidelines
For desktop or system-wide deployments, always initialize with an absolute path pointing to a user-writable directory:
import os
from icare_licensing import LicensingClient
app_folder = os.path.join(os.path.expanduser("~"), ".my_app")
os.makedirs(app_folder, exist_ok=True)
client = LicensingClient(
server_url="https://licensing.mycompany.com",
license_file_path=os.path.join(app_folder, "license.lic")
)
The metadata file (
license_metadata.json) is automatically created in the same directory aslicense.lic.
Licensing States
| Constant | Meaning |
|---|---|
STATUS_VALID |
Cryptographically valid, correct HWID, not expired, synced |
STATUS_MISSING |
No license.lic file found — device needs activation |
STATUS_EXPIRED |
License expiration date has passed |
STATUS_INVALID |
Signature corrupt, HWID mismatch, or actively revoked on server |
STATUS_REQUIRES_SYNC |
Offline for longer than max_offline_days — internet required |
STATUS_UNKNOWN |
Initial unresolved state |
Core Developer API
LicensingClient(server_url, license_file_path, status_callback, max_offline_days)
| Parameter | Type | Default | Description |
|---|---|---|---|
server_url |
str |
required | Base URL of the Icare Licensing Server |
license_file_path |
str |
"license.lic" |
Absolute path to store the signed license key |
status_callback |
callable |
None |
Function called when license status transitions |
max_offline_days |
int |
3 |
Days the app can run offline before REQUIRES_SYNC |
Methods
| Method | Description |
|---|---|
get_hardware_id() |
Returns the stable OS-native HWID for this machine |
activate_device(key) |
Binds an activation key to this machine's HWID. Returns (bool, str) |
start_monitoring(interval_seconds) |
Starts the background daemon thread (default: every 5 minutes) |
stop_monitoring() |
Stops the background daemon thread cleanly |
runCheck() |
Manually triggers a verification check |
Integration Template
import os
import sys
from icare_licensing import (
LicensingClient,
STATUS_VALID,
STATUS_REQUIRES_SYNC,
STATUS_MISSING,
STATUS_INVALID,
STATUS_EXPIRED
)
def on_license_status_change(new_status):
"""
Callback triggered when the license status transitions.
NOTE: If running a GUI application (e.g., Tkinter/PyQt), route
UI-altering updates safely to the main thread.
"""
if new_status == STATUS_VALID:
print("License valid — premium features unlocked.")
elif new_status == STATUS_REQUIRES_SYNC:
print("Warning: internet connection required to re-verify license.")
elif new_status in (STATUS_MISSING, STATUS_INVALID, STATUS_EXPIRED):
print("No valid license. Redirecting to activation screen.")
# Lock down core software capabilities here
def main():
# 1. Setup secure, writable storage path
app_folder = os.path.join(os.path.expanduser("~"), ".my_app")
os.makedirs(app_folder, exist_ok=True)
# 2. Instantiate client
client = LicensingClient(
server_url="http://localhost:8081",
license_file_path=os.path.join(app_folder, "license.lic"),
status_callback=on_license_status_change,
max_offline_days=3
)
# 3. Force an immediate check on startup
client._run_check()
if client.current_status == STATUS_MISSING:
key = input("Enter Activation Key: ")
success, msg = client.activate_device(key)
print(msg)
# 4. Start background protection thread (checks every 5 minutes)
client.start_monitoring(interval_seconds=300)
if __name__ == "__main__":
main()
Changelog
v1.3.0
- [Breaking Fix] Replaced
uuid.getnode()-based HWID with OS-native stable machine identifiers:- Windows:
HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid - Linux:
/etc/machine-id - macOS:
IOPlatformSerialNumber - Fallback: Hardware-backed MAC address only (random MACs are detected and rejected)
- Windows:
- Removed
platform.processor()andplatform.machine()from HWID computation (they add no uniqueness) - Added explicit
RuntimeErrorinstead of silently returning an unstable HWID
v1.2.0
- Initial public release
- RSA-SHA256 offline signature verification
- 24h background sync daemon
- 3-day offline grace period
- Instant revocation purge on server rejection
- 5-retry online sync with 10s backoff
License
MIT — see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file icare_licensing-1.3.0.tar.gz.
File metadata
- Download URL: icare_licensing-1.3.0.tar.gz
- Upload date:
- Size: 13.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af4676a6f397a22cd8134882ae852ad9373b01ed8657219511c738562eb312f7
|
|
| MD5 |
f3c0ba3eb431a29f14602bf1fc0886e7
|
|
| BLAKE2b-256 |
e799299e418718aecf027513f075dbe3b28ab67492af87d57ef58f0689602004
|
File details
Details for the file icare_licensing-1.3.0-py3-none-any.whl.
File metadata
- Download URL: icare_licensing-1.3.0-py3-none-any.whl
- Upload date:
- Size: 10.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69c7beb5a72d7091615a88055fdc33e5efd595b250c82db7c988711c62cd16bc
|
|
| MD5 |
5c928f01d6b8ee9df65ec21da7619cc0
|
|
| BLAKE2b-256 |
f76d6ab99a29cddb25664a1af6280ee1072a85cd48b4c7e1da67766a533ad27c
|