Python

Python license key validation for desktop apps

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

Requirements and installation

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:

Shell
pip install ./sdks/python

How validation works

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:

  1. verifies the Ed25519 signature over the exact bytes of data, using only the public key you passed to the constructor (kid is ignored, so the key cannot be swapped);
  2. only then decodes and parses the payload;
  3. rejects it unless it echoes this request’s nonce, your product ID and the expected response type, which stops replayed or redirected answers;
  4. returns a 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.

Quick start

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.

licensing.py
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.

Result codes

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.

messages.py
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)

Where to put the check

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:

reports.py
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.

Offline leases

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.

Persisting the device secret

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.

Updates and downloads

updates.py
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.

Packaging and hardening

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:

  • Keep the public key in code as a constant. If it came from a config file, an environment variable or the network, a user could swap in their own key and sign their own “valid” answers. Rotating the signing key requires shipping a new build.
  • Check results in several places and re-validate periodically, not with one if at startup.
  • Fail closed. Treat every ok=False, especially invalid_response, as not licensed, and never add a fail-open path.
  • Keep TLS on. Never enable allow_insecure_http in production; a custom ssl_context must keep certificate and hostname verification.
  • Never log license keys, device secrets, lease tokens or download URLs. Show users at most the last five characters of a key.

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.

Key points

  • Install the SDK from the Velsigil source tree with pip install ./sdks/python; it is not on PyPI.
  • Request methods never raise; check result.ok and compare result.code with velsigil_client.Code.
  • Use FileStore(default_store_path(...)) so the device secret and offline lease survive restarts.
  • The offline lease is used only on network_error, and a definitive signed refusal deletes it.
  • Python bytecode is easy to read: keep valuable features behind your own server.

Keep reading

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.

License key security

How license keys get shared, forged or replayed, and the defenses that work: signed answers, replay protection, hashed keys, device secrets, rate limits.

Hardware-locked licensing

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.

Node.js license key validation

Validate license keys in Node.js and Electron: signed answers, device secrets, offline leases, and why the check belongs in the main process.

All guides

Early access

Request 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.

  • Free until paid subscriptions launch
  • 30% off your first year on either plan when they do
  • For businesses established in the United States

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.

Request early access

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.