Skip to main content

latte-py

Python SDK for LicenseLatte, the software licensing platform. An idiomatic, from-scratch Python implementation of license activation and verification.

Full docs: licenselatte.com/docs/sdks/python

Read the Threat Model section below before relying on this package for anything security-sensitive.

[!NOTE] The Python SDK versions independently from the other language bindings and follows semver. It's currently on v0.x, meaning the public API may still change without a major version bump. It moves to 1.0.0 once the API is validated across real integrations.


What this package verifies

LicenseLatte licenses are issued as a chain of Ed25519-signed JWTs:

Master (root, hardcoded in the SDK)
  -> Submaster cert
       -> Project cert
            -> Daily cert
                 -> Activation token (what you actually check against a machine)

Each link is a standard compact-serialization JWT (base64url(header).base64url(payload).base64url(signature), alg: EdDSA, signed with Ed25519, see RFC 8037). Verifying a license means:

  1. Verify the submaster cert's signature against the hardcoded master public key, extract the submaster's own public key from its spk claim.
  2. Verify the project cert's signature against the submaster's public key, extract ppk.
  3. Verify the daily cert's signature against the project's public key, extract dpk.
  4. Verify the activation token's signature against the daily key.
  5. Cross-check the claims (project ID agreement, timing consistency between the activation token and the daily cert that signed it).
  6. Apply grace-period math: is the token still within its hard expiry, and, if the device has been offline, still within its configured grace window (30–90 days, chosen when the license is issued)?

This is a standard certificate-chain-of-trust design (the same shape as an X.509 chain, just JWTs instead of X.509 certs), documented publicly here per Kerckhoffs's principle: the mechanism is not the secret, the master private key is. This SDK ships only the master public key; key rotation cadence, key storage, and the tooling that issues certs are intentionally not documented in any SDK repo.

Cryptography

  • Ed25519 signature verification via cryptography's hazmat primitives (cryptography.hazmat.primitives.asymmetric.ed25519): an audited, widely used library; no hand-rolled crypto anywhere in this package.
  • JWT compact-serialization parsing is hand-written (src/latte/jwt.py): this is structural (base64url + JSON), not cryptographic, so implementing it directly instead of pulling in a general-purpose JWT library is a reasonable, minimal-dependency choice for four call sites with one fixed algorithm.

Installation

pip install -e .

Quick start: activating a license

from latte import Config, Sdk, LatteError

sdk = Sdk(Config(app_id="pk_live_..."))  # from the LicenseLatte dashboard

try:
    lic = sdk.activate("USER-PROVIDED-LICENSE-KEY", "opaque-machine-id")
    print("license OK, expires", lic.expires_at)
    if lic.in_grace_period:
        print("warning: offline a while, please reconnect soon")
    # Keep lic.activation_id around (in your own storage) so you can call
    # sdk.renew(lic.activation_id, ...) later.
except LatteError as e:
    print("activation failed:", e)

By default, a successful activate/renew is written to an on-disk cache, and a later activate call for the same key returns the cached result without a network round trip as long as it's still valid. There's no background renewal: call renew yourself on whatever schedule fits your application. Set Config(cache=False) to disable the cache entirely (e.g. a sandboxed environment with no writable filesystem).

Checking a cached activation without a network call

from latte import LicenseExpiredError, NotActivatedError

try:
    lic = sdk.check("opaque-machine-id")
    print("license OK, expires", lic.expires_at)
except LicenseExpiredError:
    print("license expired, please renew")
except NotActivatedError:
    print("not activated, call activate()")

The cache file

By default, Sdk stores an activated license as a small JSON file under your OS's per-user config directory (via platformdirs), named {project_key}.json:

{
  "timestamp": 1700000000,
  "token": "<activation JWT>",
  "submaster": "<submaster cert JWT>",
  "project": "<project cert JWT>",
  "daily": "<daily cert JWT>"
}

Writes go to a temp file in the same directory and get renamed into place, so a crash or a concurrent write can't leave a half-written file behind. Config.cache_path overrides the location if you want it somewhere else.

Re-verifying a token you're storing yourself

If you'd rather manage persistence yourself instead of using the built-in cache, check_license_at/check_license run the same verify+validate pipeline Sdk.activate/Sdk.check do, against a token/chain you already have:

import time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from latte import check_license, CertChain, VerifyError, ValidateError

master_pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(MASTER_PUBLIC_KEY_HEX))
chain = CertChain(submaster=..., project=..., daily=...)

try:
    lic = check_license(master_pub, token, chain, machine_id)
    print("license OK, expires", lic.expires_at)
    if lic.in_grace_period:
        print("warning: offline a while, please reconnect soon")
except VerifyError as e:
    print("could not verify license:", e)  # chain/signature/format problem
except ValidateError as e:
    print("license rejected:", e)  # verified fine, but expired/out of grace/wrong machine

check_license_at(..., now) is also available for callers who want to pass an explicit timestamp instead of the real system clock: this is what makes this package's test suite fully reproducible against a fixed set of test vectors in testdata/.

Offline grace period

The grace period is an offline tolerance window measured from the license's last issuance/renewal, not from its expiry:

issued_at ------------------------------------> expires_at
              |                   |
              └── grace_period ───┘
                  ^ offline window

While now <= issued_at + grace_period, the license is still usable without a network call. Once that deadline passes, verification raises GraceExpiredError; once now > expires_at, it raises HardExpiredError (checked first: hard expiry always wins).

PublicLicense.in_grace_period is a softer, earlier warning signal: it turns True once more than 60 minutes have passed since the last issuance/renewal without a fresh one arriving, while still inside the grace window: surface it as a "please reconnect soon" hint, distinct from an outright rejection.

What this package does not do

OS-level machine-ID fingerprinting and background renewal scheduling are intentionally out of scope. Pass your own machine-ID string into activate/renew/check/check_license; only the opaque string compared against the token's mid claim matters, not the algorithm that produces it. For renewal, there's no scheduler here: Sdk.renew is the building block; call it on a timer, in response to a UI action, or whatever fits your application.

Threat model

Read this before you rely on latte-py for anything where tamper resistance, not just cryptographic correctness, matters.

This is a statement of fact about the architecture, not a disclaimer to skim past:

  • Python source and compiled bytecode (.pyc) ship human-readable or trivially decompilable. Anyone with a text editor and basic familiarity with Python can open your application's installed package, find the call to check_license/check_license_at, and delete it, or monkeypatch latte.check_license to always return a fabricated PublicLicense before your application code ever runs. This requires no reverse engineering tools beyond a text editor: this is fundamentally different from a compiled binary (Go, Rust, C, C++, D), where bypassing a license check requires actual binary patching or a debugger.
  • This is a known, accepted tradeoff for an interpreted-environment SDK, not a bug in this package. No amount of obfuscation, code-signing the .py files, or "clever" runtime tricks closes this gap: Python's execution model means the interpreter always has the actual source (or bytecode, which trivially decompiles back to source) available to inspect and modify at runtime.
  • What this package does guarantee: the cryptographic verification itself is correct. A forged license (wrong signature, broken chain, tampered claims) will fail verification exactly as it would in latte-go, latte-rs, or latte-c. What it does not guarantee is that a determined user can't simply remove the call to this package from your application entirely.
  • If this distinction matters for your deployment (e.g. you're protecting revenue from a motivated, technically capable user base, not just casual copying), the mitigation is server-side re-validation — but only if you draw the trust boundary in the right place. The mitigation isn't "run the check again" (a re-run of check_license is just as patchable as the first run, and a text editor doesn't care how many times you call the function you're deleting). It's "run the check somewhere the attacker's text editor can't reach": on your server, invoked by your server's own code, gating a resource your server actually controls (an API response, a file download, a feature flag your backend decides). A locally-patched client can lie to itself all day; it can't make your server hand over a server-mediated resource without the server independently confirming a valid, unexpired license first.
    • This only holds if the server does its own verification. If your server instead just trusts something the client reports (a "licensed": true field, a header, a cached result), you've moved the trust boundary back onto the attacker's machine and you're back to square one — that flag is exactly as easy to fabricate as deleting the local check was.
    • GracePeriod/in_grace_period are what your server uses to decide when to insist on a fresh activation check, not a mechanism that makes a client-side check itself tamper-resistant.
  • This tradeoff is specific to Python (and, separately, to Electron/JS; see latte-js's equivalent Threat Model section). The compiled SDKs (latte-go, latte-rs, latte-c, and C++/D bindings) require actual binary reverse engineering to bypass, which is a meaningfully higher bar even though none of them are literally unbreakable either.

Testing

pip install -e ".[dev]"
pytest

Runs unit tests for the checksum algorithm and AppID parsing, chain verification (valid chains, tampered signatures, broken intermediate links, cross-check failures, clock-skew edge cases), grace-period math (including exact boundary conditions), plus the full shared cross-language fixture suite in testdata/ (see ../latte-testvectors/README.md).

ruff check .
mypy src

License

MIT, see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

latte_py-0.2.0.tar.gz (32.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

latte_py-0.2.0-py3-none-any.whl (25.1 kB view details)

Uploaded Python 3

File details

Details for the file latte_py-0.2.0.tar.gz.

File metadata

  • Download URL: latte_py-0.2.0.tar.gz
  • Upload date:
  • Size: 32.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for latte_py-0.2.0.tar.gz
Algorithm Hash digest
SHA256 fa4e18844cf50fd84db47ac40289a5d6db696be5d7a41c868e04f9f378aa9f94
MD5 f6caa48a847014113c839b0f66041bda
BLAKE2b-256 54aae8030e0eef12dee1da3099de5c9b1069d65f22298514db27430c8051c66a

See more details on using hashes here.

Provenance

The following attestation bundles were made for latte_py-0.2.0.tar.gz:

Publisher: release.yml on licenselatte/latte-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file latte_py-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: latte_py-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 25.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for latte_py-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 87a955c7362690a018ce050226184250a965d3fb4cdf49d2a01c4cb58b553ac9
MD5 095a88f8290309cb32a6b62adcf9aad4
BLAKE2b-256 024b6a579c3d00b7aaa2425627d65d7578f842c2e66ef7e7e5cd1ee1b4f2f554

See more details on using hashes here.

Provenance

The following attestation bundles were made for latte_py-0.2.0-py3-none-any.whl:

Publisher: release.yml on licenselatte/latte-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.3.0

2 files

0.3.0

2 files

This release

0.2.0 This release

2 files

0.1.2

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page