Py2Native Compile custom Python into native machine code to protect proprietary code

← All posts

2026-10-03

Offline License Verification in Compiled Python Desktop Apps

Offline License Verification in Compiled Python Desktop Apps
python compiler cython code protection open source

Offline License Verification in Compiled Python Desktop Apps

You ship a desktop app written in Python. A customer buys a license. Your app needs to know that — and it needs to know it on a laptop in a basement with no Wi-Fi, behind a corporate proxy that blocks anything that isn’t an HTTPS call to an approved domain.

That’s the constraint that decides your architecture: offline license key verification in desktop Python apps. Not “offline as a fallback” — offline as the only path. No license server, no heartbeat, no retry queue, no “unable to reach licensing service” dialog at 9 a.m. on a Monday.

There’s a second half to the problem, and it’s the one people usually skip. Verification logic that lives in readable Python source is verification logic someone can delete. If your license check is a .py file shipping next to your app, patching it out is a text editor and ten minutes.

This article covers both halves: how a signed license token is verified with zero network calls, and how that verification runs inside a native binary where the check and the embedded public key aren’t plain source anymore. If you’re still deciding whether compiling is worth it, the cost-benefit breakdown covers that ground; here we assume you’ve decided and want the verification working.

What a Verifiable License Proof Looks Like

A license in this model is a JWT — a JSON Web Token. It’s a string with three dot-separated parts: a header, a payload of claims, and a signature. The claims are things like issuer, audience, and expiry. The signature is what makes it a proof rather than a suggestion.

The token is signed with an EC P-256 private key. Verification only needs the matching public key. That asymmetry is the entire trick:

  • The private key stays on your machine. Only you can mint licenses.
  • The public key goes into every shipped build. It can verify, but it cannot sign.
  • Anyone who extracts the public key from your binary gains nothing — they can’t forge a token with it.

Verification means two things, and both matter:

  1. The ES256 signature is valid for the token’s contents.
  2. The claims are what you expect — correct issuer, correct audience, and not expired.

A signature check alone is insufficient. A perfectly signed token issued to someone else, for a different product, is still a validly signed token. You have to assert the claims.

Py2Native Pro bakes both the signature verification code and the public key into the compiled binary. The check runs natively, as compiled machine code, with no third-party crypto library loaded at runtime — the elliptic key verification is implemented inside the product.

Step-by-Step: Adding Offline License Verification with Py2Native

Everything below starts with uv run py2native. No pip, no manual build scripts.

1. Generate a keypair

The Pro plugin adds keygen, sign, and show commands:

uv run py2native keygen private.pem public.pem

Keep private.pem somewhere safe and offline. public.pem is the one that ships.

2. Sign a license payload

Write the claims you want to grant. For example, payload.json:

{
  "iss": "RSJ Software GmbH",
  "aud": "TimestampGIT",
  "sub": "customer-4711",
  "exp": 1798761600
}

Then sign it:

uv run py2native sign --private private.pem payload.json license.dat

license.dat is the JWT you hand to the customer.

3. Inspect the token before you ship it

Confirm the claims landed correctly:

uv run py2native show --public public.pem license.dat

show prints the JWT claims, and with --public it also verifies the signature against the key. Run this before every release so you never ship a token signed by a stray key.

4. Declare the verification function in a .pxd file

The compiled bootstrap exposes a runtime verification function. Declare its signature so the compiler knows how to bind the call:

# _p2n_bootstrap.pxd
cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)

That declaration file is the only Cython-flavored line in this whole workflow. You are not writing Cython, not writing C, and not running cythonize by hand — Py2Native’s compiler consumes the declaration and wires it up.

5. Call it from your Python code

Import the function in the module that gates features, and call it with the license string and the claims you expect:

# licensing.py — compiled to native code by Py2Native
from _p2n_bootstrap cimport _runtime_verify_es256_jwt

def load_license(path="license.dat"):
    with open(path, "r", encoding="utf-8") as fh:
        licenseString = fh.read().strip()

    license = _runtime_verify_es256_jwt(
        licenseString,
        expected_iss="RSJ Software GmbH",
        expected_aud="TimestampGIT",
    )
    return license

The signature you’re matching is exactly the one declared in the .pxd:

_runtime_verify_es256_jwt(token, expected_iss="RSJ Software GmbH", expected_aud="TimestampGIT")

6. Build the protected binary

uv run py2native build main.py *.py --license license.dat --public public.pem --embed dist

The --license flag requires a valid Pro license for the build itself. --public points at the public key. The plugin embeds the verification logic and the public key into the executable — no third-party crypto libraries are needed at runtime.

That’s the whole integration. Your application code stays normal Python; one declaration file and one function call carry the licensing.

Interpreting Verification Results and Handling Edge Cases

On success, the function returns the decoded claims. Use them as your source of truth:

claims = load_license()
if claims["sub"] != expected_customer:
    raise SystemExit("This license was issued to a different customer.")

On failure, the function raises. Catch it and fail gracefully — a clear message beats a stack trace on a customer’s machine:

try:
    claims = load_license()
except Exception:
    print("No valid license found. Run `myapp --activate license.dat` to install one.")
    raise SystemExit(1)

A few things to get right:

  • Expired tokens are invalid. Handle expiry the same way you handle a bad signature — refuse to run, or degrade to a limited mode.
  • Wrong issuer or wrong audience means invalid. Pass both expectations on every call. Don’t rely on the signature alone.
  • Clock skew is real. If your claims carry time-based fields, a machine with a badly set clock can fail verification for a legitimate license. A small tolerance on time comparisons is usually worth it.
  • Key rotation needs a new build. The public key is embedded at compile time, so rotating keys means shipping an updated binary to existing customers. Plan the rotation window accordingly.
  • Monkey-patching still exists as a category. Third-party libraries remain Python source, and that’s a deliberate trade-off. What changes is that your verification code is no longer readable Python — it’s compiled into the binary.

How Py2Native Compares to the Hard Way

There is a harder path: write .pyx files by hand, invoke cythonize yourself, assemble C extensions, write platform-specific linker configuration, and hand-roll ES256 verification with a crypto library. It works. It also means owning Cython syntax, build configuration, and per-platform linking for the rest of your product’s life.

Py2Native compiles plain Python. It invokes Cython under the hood and hides the toolchain — no .pyx rewrites, no manual cythonize step, no C extensions you author. Third-party libraries are left unmodified and work as-is with minimal or no configuration, which is where a lot of Python code-protection setups quietly fall apart.

The core compiler is open source under the MIT license. The Pro plugin is what adds JWT license verification and string compression.

If your concern is source readability rather than licensing, protecting Python source code from being read is the more direct read.

Deployment Considerations for Offline Verification

Two output modes matter here:

  • --embed <dir> creates a uv-managed deployment directory containing the compiled binary and its runtime. This is the straightforward route for shipping a desktop app.
  • --wheel <dir> produces a PEP 427 wheel containing the compiled .pyd/.so, for library-mode distribution.

In library mode, the wheel ships three things: the compiled shared library, an __init__.py that acts as a meta-path finder redirecting import mypkg.submodule into the native library, and a __main__.py that enables python -m mypkg. Both Python files are required — the first bridges the import system, the second provides the runnable entry point.

Platform coverage:

  • Windows 8+, manylinux2014/musl Linux, macOS
  • x86-64 or ARM64
  • CPython 3.11–3.15, including free-threaded 3.14t and 3.15t

Requirements are a platform C compiler (MSVC, GCC, or Clang), internet access during build for downloading Python distributions and libraries, and the usual dependency set: cython >= 3.2.4, setuptools >= 82.0.1, uv >= 0.11.8, plus auditwheel >= 6.6.0 on Linux.

One constraint worth designing around: no modules may share identical names across your source files. __init__.py is skipped during compilation since it’s a namespace marker, and in library mode __main__.py is excluded from sources because the wheel builder generates it.

FAQ

Can I verify licenses without an internet connection?

Yes. Py2Native Pro bakes the public key and the ES256 verification logic into the compiled binary, so the license check runs entirely offline with no network call.

Do I need to write Cython or C code to add license verification?

No. You write plain Python and call the runtime verification function from the compiled bootstrap. You only add a .pxd declaration so the compiler knows the function signature. Py2Native handles Cython, C compilation, and linking for you.

What happens if someone extracts the embedded public key?

The public key is only used to verify signatures. It cannot be used to forge new license tokens, so embedding it in the executable is safe.

How do I rotate license keys?

Because the public key is embedded at build time, key rotation requires shipping a new build with the updated public key. Plan your rotation schedule accordingly.

Conclusion

Offline license verification comes down to two decisions: use an asymmetric proof so the shipped artifact can verify but not forge, and put the verification where it can’t be edited with a text editor. Signed JWTs solve the first. Compiling your Python solves the second — and with Py2Native, the compiling part is a build flag, not a rewrite.

Generate a keypair, sign a payload, declare one function, and build with --license and --public. Your app then validates licenses on a plane, in a SCIF, or behind a proxy that hates you — and the check itself is native code with the public key baked in.

Start with the Pro plugin if you need the license verification commands: uv run py2native keygen private.pem public.pem is the first line you’ll type. If you want to see the compiled output before wiring in licensing, compile a test app to an EXE first and confirm the pipeline end to end.

Related posts

EU label: AI-generated content