Offline license activation
How software checks a license without internet: signed offline leases, signed license files, grace periods, clock tampering and what each one trusts.
Python
To add license key validation to a Python application, send the key to your license server when the app starts, verify the server’s Ed25519 signature with a public key kept as a constant in your code, and only then read the plan, expiry and features. Persist the device secret and a signed offline lease so the app survives restarts and short outages. This guide uses velsigil-client, the Python SDK included with Velsigil, and is honest about what Python packaging can and cannot protect.
Last updated
The SDK declares Python 3.8 or later on Windows, macOS and Linux; its package metadata lists 3.8 to 3.13, so test it on the interpreter you ship. HTTP uses the standard library’s urllib, so there is no dependency on an HTTP library. Ed25519 verification comes from the cryptography package (3.4 or later), with PyNaCl as an optional fallback backend through the nacl extra.
The SDK is included with Velsigil as source code, not on PyPI. Install it from the source tree into your application’s environment:
pip install ./sdks/python
Each request carries the product ID, a fresh 32-byte random nonce and a timestamp; license requests also send the hardware ID and, once issued, the device secret. The server answers with a signed envelope { data, sig, kid }, and the SDK:
data, using only the public key you passed to the constructor (kid is ignored, so the key cannot be swapped);VelsigilResult; ok=True is possible only after all of these steps.Unsigned HTTP errors (rate limits, blocked IPs, server errors) become failure results and can never produce ok=True. If the server reports a signed clock_skew, the SDK learns the offset and retries once with a new nonce.
Keep the API URL, product ID and public key as constants in your code, not in a settings file. Copy them from the product’s Integration tab.
from velsigil_client import VelsigilClient, VelsigilError, FileStore, default_store_path
API_URL = "https://licenses.example.com" # from the product's Integration tab
PRODUCT_ID = "<product id>"
PUBLIC_KEY = "<Ed25519 public key, base64>"
try:
client = VelsigilClient(API_URL, PRODUCT_ID, PUBLIC_KEY,
store=FileStore(default_store_path("MyApp")))
except VelsigilError as exc: # ConfigurationError, CryptoBackendError, HardwareIdError
raise SystemExit("configuration problem: %s" % exc)
result = client.validate_with_offline_fallback(license_key, version="1.2.0")
if not result.ok: # request methods never raise
raise SystemExit("%s [%s] ref %s" % (result.message, result.code, result.request_id))
print(result.license.plan, result.offline, result.expires_at_datetime, result.days_remaining())
if result.has_feature("pro"):
enable_pro()
Request methods never raise: licensing problems and transport failures come back as a result with ok=False and a code. Only the constructor raises (ConfigurationError, CryptoBackendError when no Ed25519 backend is installed, HardwareIdError when no machine ID can be read and you passed no hwid), plus download_to_file, which raises DownloadError. Note that days_remaining() rounds up, so a license with a few hours left reports one day.
Compare codes with the constants in velsigil_client.Code and map them to your own wording; fall back to result.message, which is either a signed server message or fixed SDK text.
from velsigil_client import Code
MESSAGES = {
Code.INVALID_KEY: "That license key was not found. Please check it and try again.",
Code.LICENSE_EXPIRED: "Your license has expired. Renew it to keep using the app.",
Code.DEVICE_LIMIT_REACHED: "This license is active on too many devices. Free one in the customer portal.",
Code.NETWORK_ERROR: "The license server could not be reached, and no offline lease is available.",
Code.INVALID_RESPONSE: "The license check failed. Please contact support.",
}
def license_message(result):
return MESSAGES.get(result.code, result.message)
In a desktop app, validate before the main window opens and again every few hours or before important actions. In a command-line tool, validate at the start of each command that needs a license. In both, keep the result object and check it where each feature is used, instead of setting one global flag that a single edit can flip:
def export_report(result, report):
# has_feature() is False for every failed or forged result.
if not result.has_feature("export"):
raise PermissionError("Export is not included in your plan.")
write_report(report)
A VelsigilClient can be shared between threads. validate, deactivate and get_download are serialized per client because they read and update the device secret and lease, which also keeps concurrent first activations from racing; check_update and validate_offline run concurrently. Separate processes sharing one store file follow last-writer-wins.
When the product allows offline use, a successful validation returns a signed lease bound to the product, this device’s hardware-ID hash and an expiry no later than the license’s own. The SDK verifies it and stores it. validate_with_offline_fallback() uses it only for network_error: a business failure, rate limit or invalid answer never falls back, because an attacker who can tamper with answers must not be able to switch the app into offline mode.
A signed, definitive refusal (invalid key, expired, suspended, revoked or banned license, revoked device and similar) deletes the stored lease. In the Python SDK, if the stored lease is expired or invalid when the fallback runs, the result stays network_error, with a message that says so. Offline validation trusts the local clock, so keep lease hours short; the offline activation guide explains why.
With the default MemoryStore, the device secret is lost on every restart. The server then counts a secret mismatch each time, and products with strict device binding refuse the device until the customer resets it. Use FileStore(default_store_path("MyApp")): it writes atomically (temporary file, fsync, os.replace), with owner-only permissions on POSIX, and on Windows to %LOCALAPPDATA%\MyApp\Velsigil\license.json. To use the OS keychain or your own encrypted settings, implement LicenseStore (load, save, delete).
If saving fails, the SDK logs a warning and keeps the state in memory for the rest of the process. A successful deactivate() and clear_stored_state() remove the secret.
from velsigil_client import DownloadError
update = client.check_update("1.2.0").update
if update and update.update_available:
link = client.get_download(license_key, update.latest_version)
if link.ok:
try:
path = client.download_to_file(link.download, "downloads/app.zip")
except DownloadError as exc: # download_failed, integrity_mismatch, io_error, ...
print("Update failed:", exc.code)
get_download returns a signed link that expires within minutes, plus the file’s signed size and SHA-256. download_to_file streams to a temporary file next to the destination, aborts once the download exceeds the signed size, compares the hash in constant time and only then moves the file into place, so a tampered or truncated download never appears at the destination path.
Python is the easiest of the four languages to inspect. Bytecode decompiles back to readable code, and tools that freeze your app and the interpreter into one executable still contain that bytecode. Packers and obfuscators only slow an attacker down. Plan for that:
if at startup.ok=False, especially invalid_response, as not licensed, and never add a fail-open path.allow_insecure_http in production; a custom ssl_context must keep certificate and hostname verification.For a Python product, the most effective protection is architectural: put what makes it valuable, such as a model, a data feed or a cloud feature, behind your own server, and deliver it only after a successful license check there.
pip install ./sdks/python; it is not on PyPI.result.ok and compare result.code with velsigil_client.Code.FileStore(default_store_path(...)) so the device secret and offline lease survive restarts.network_error, and a definitive signed refusal deletes it.Keep reading
How software checks a license without internet: signed offline leases, signed license files, grace periods, clock tampering and what each one trusts.
How license keys get shared, forged or replayed, and the defenses that work: signed answers, replay protection, hashed keys, device secrets, rate limits.
How hardware-locked licenses bind a key to a device: machine IDs per OS, why hardware IDs can be spoofed, device secrets, limits and customer resets.
Validate license keys in Node.js and Electron: signed answers, device secrets, offline leases, and why the check belongs in the main process.
Early access
Early access is free. In exchange, we ask for your feedback while we prepare the paid launch. Spots are limited, and we reply to every request by email.
Early access ends when paid subscriptions launch; we will tell members by email before then. To keep using Velsigil after that, you need a subscription. The 30% discount applies to the first 12 months of either plan, paid monthly or yearly.
Opens your email app with a short template: company, what you sell, your platforms and languages, expected license volume and how you plan to deploy. You can also write to hello@velsigil.com.