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

← All posts

2026-09-15

Secure License Verification for Python Native Binaries

Secure License Verification for Python Native Binaries
python compiler cython code protection open source

Secure License Verification for Python Native Binaries

If you ship desktop tools, on-premise software, or commercial Python applications, you already know the problem: Python source is readable, editable, and easy to redistribute. A well-placed if license_valid check in a .py file is not a serious deterrent. Someone can open your source, change one condition, and rebuild.

Python license key verification only becomes meaningful when the check is as hard to tamper with as the rest of your application. That means compiling the verification path into native machine code and embedding the cryptographic material directly in the binary. Py2Native Pro does exactly that: it gives you a signed JWT license flow, an embedded public key, and a compiled verifier that is not a readable Python bytecode shortcut.

This article walks through the license verification workflow end-to-end: generating keys, issuing licenses, adding the verification call to your Python app, and compiling with Py2Native.

Why License Verification Matters for Python Binaries

Shipping .py files gives away both your product and your license enforcement. Python tools such as uncompyle6, pycdc, and ordinary editors can recover source from .pyc bytecode. Even when the code is not fully decompiled, a determined user can patch bytecode or monkey-patch a license function at runtime.

A typical Python license check looks like this:

if license_key in VALID_KEYS:
    run_app()

That is easy to bypass because the check exists as readable bytecode. If the application is compiled with Py2Native, the same verification logic becomes native code. The user has no convenient .py file to edit, and the check is no longer an isolated Python call that can be replaced with a one-line monkey patch.

Py2Native’s job is not to turn third-party libraries into native code. It compiles your custom Python code while leaving dependencies as normal Python packages. For licensing, the Pro plugin adds the missing piece: signed JWT verification compiled into your binary.

What a Proof of License Verification Looks Like

Py2Native Pro uses a signed JWT license key. The license file contains three sections:

header.payload.signature

A real license file might look like this:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lckBleGFtcGxlLmNvbSIsImlzcyI6IlJTSCBTb2Z0d2FyZSBHbWJIIiwiYXVkIjoiVGltZXN0YW1wR0lUIiwiZXhwIjoxNzk4NzYxNjAwfQ.signature

The payload carries the claims that your application can enforce:

  • sub: customer identifier, such as an email address.
  • iss: issuer, expected to be RSJ Software GmbH.
  • aud: audience, expected to be TimestampGIT.
  • exp: expiry time.

Only the public key is stored in the compiled binary. The private key never leaves your build machine. Verification happens inside native code, not in a Python-level fallback that an attacker can replace.

Py2Native Pro provides three commands for the full lifecycle:

uv run py2native keygen private.pem public.pem
uv run py2native sign --private private.pem '<payload>' license.dat
uv run py2native show --public public.pem license.dat

keygen creates an EC P-256 keypair, sign produces the signed JWT license file, and show displays the claims and optionally verifies the signature.

Step-by-Step: Adding License Verification to Your Python App

Prerequisites

You need:

  • CPython 3.11 or newer.
  • uv.
  • A platform C compiler and linker, such as MSVC, GCC, or Clang.
  • The py2nativepro plugin installed in your project environment.
  • A valid Pro license for the build-time license check.

All commands in this workflow run through uv run py2native, not pip.

Step 1: Generate a keypair

Run the key generation command from your project directory:

uv run py2native keygen private.pem public.pem

Keep private.pem in a secure location that is not distributed with the product. You will use public.pem at build time so the binary can verify licenses without needing an external key file.

Step 2: Sign a customer license

Create a JWT payload for the customer and sign it with the private key:

uv run py2native sign --private private.pem \
  '{"sub":"customer@example.com","exp":1798761600,"iss":"RSJ Software GmbH","aud":"TimestampGIT"}' \
  license.dat

This produces license.dat, which you can send to the customer. To inspect it:

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

Step 3: Add the verification call in your code

Py2Native Pro compiles the license verifier and the public key into the binary. In your project, add a small .pxd declaration so the compiled bootstrap exposes the verifier to your application:

# py2native_license.pxd
from _p2n_bootstrap cimport _runtime_verify_es256_jwt

cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)

Now call the verifier from your application code. The compiled verifier signature is:

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

A minimal enforcement function looks like this:

def enforce_license(licenseString: str) -> None:
    license = _runtime_verify_es256_jwt(
        licenseString,
        expected_iss="RSJ Software GmbH",
        expected_aud="TimestampGIT",
    )
    if not license:
        raise RuntimeError("License verification failed")

Then call enforce_license() before your application starts:

from pathlib import Path

def main():
    license_file = Path("license.dat")
    if not license_file.exists():
        print("License file not found. Contact support.")
        return 1

    try:
        enforce_license(license_file.read_text().strip())
    except RuntimeError as exc:
        print(f"License error: {exc}")
        return 1

    start_my_application()

The exact claim names and expected values depend on your license payload, but the example above matches the signed payload from Step 2.

Step 4: Compile with Py2Native

Build the executable or library and include your public key and license file:

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

The build command compiles your Python sources through Py2Native’s pipeline and bakes the public key into the native binary. If the license is invalid, expired, or missing at runtime, the enforcement path in your code returns an error and refuses to start.

Interpreting Verification Results and Handling Edge Cases

A valid signed license with the expected issuer and audience allows the application to run normally. The verifier returns the license claims, and your application proceeds.

An invalid or expired license returns a failure, and your code should fail closed:

license = _runtime_verify_es256_jwt(
    licenseString,
    expected_iss="RSJ Software GmbH",
    expected_aud="TimestampGIT",
)
if not license:
    raise RuntimeError("License verification failed")

Do not continue into the application when verification fails. A fail-open design makes the entire license system optional from an attacker’s perspective.

Common edge cases:

  • Missing license file: Show a clear message and exit with a non-zero status.
  • Tampered license: The JWT signature will not match; treat this as a hard failure.
  • Wrong issuer or audience: The verifier checks both claims, so a license signed for another product or tenant fails.
  • Expired license: The exp claim is enforced; issue a new license and send it to the customer.
  • Clock skew: If the target machine’s clock is wrong, an otherwise valid license may appear expired or not-yet-valid. Document the expected system time for your users.

Why Py2Native Makes License Verification Secure

A pure Python license check is only as safe as the source files containing it. Py2Native changes that equation in a few concrete ways:

  • The verification code compiles into native machine code, so there is no readable Python source for the license decision.
  • The public key is embedded in the binary, so the runtime does not need a separate public key file that can be swapped.
  • The Pro plugin uses ES256 verification through the P-256 elliptic curve, implemented in compiled code without third-party cryptographic libraries.
  • Because the verifier is compiled, it cannot be bypassed by editing a .pyc file or monkey-patching a Python function.

The hard way is to hand-write C extensions or manage raw Cython yourself, but Py2Native keeps the workflow to one uv run py2native build command.

FAQ

Can I use license verification without the Pro plugin?

The open-source Py2Native core does not include built-in license verification. You would need to implement your own verification logic, but that would remain as Python bytecode and be easier to inspect or patch. The Pro plugin provides compiled verification with an embedded public key, making it much harder to bypass.

How do I handle license renewal?

Issue a new license file with an extended expiry using the sign command. The customer replaces the old license.dat with the new one. The verification code checks the expiry claim and accepts the new license without recompiling the binary.

Does the compiled binary need internet access to verify the license?

No. Verification is entirely offline. The public key is embedded in the binary, and the JWT signature is verified locally. This makes the flow suitable for air-gapped and on-premise deployments.

What happens if someone extracts the public key from the binary?

The public key is not secret. It is used only to verify signatures. An attacker with the public key cannot forge a valid license because doing so requires the private key. The private key remains with you and is never distributed.

Conclusion

Python license key verification is only as strong as the runtime boundary around it. Py2Native lets you keep writing ordinary Python and then compile the license path into native code, with the public key embedded directly in the binary. The Pro plugin handles key generation, signing, inspection, and compiled verification without requiring you to manage Cython or write C extensions by hand.

Start with a keypair, sign a test license, add the verifier call to your app, and run uv run py2native build --license license.dat --public public.pem main.py. For a deeper look at how the compiler fits into your packaging workflow, see the Py2Native documentation or the related posts below.

Related posts

EU label: AI-generated content