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

← All posts

2026-08-29

How to Verify JWT License Keys in Python Applications

How to Verify JWT License Keys in Python Applications
python compiler cython code protection open source

How to Verify JWT License Keys in Python Applications

If your Python application ships with a license check written in plain .py, that check is one source-file edit away from being patched out. The problem is not whether Python can verify a license — it can — but whether the verification logic itself survives contact with an end user. This article focuses on verification: how to verify JWT license keys in Python applications in a way that hides the check inside native machine code.

We’ll look at what a JWT license proof actually contains, how to verify it with Py2Native Pro, and how to handle the edge cases that show up when real customers run your software offline, on skewed clocks, or with expired keys.

Why JWT License Verification Matters for Python Developers

Shipping plain Python source code means shipping your license enforcement as readable text. A competent user can find a line like if not license_valid and change the branch. Even if you obfuscate the code, the verification remains patchable because Python objects and bytecode are introspectable at runtime.

JWT, or JSON Web Token, gives you a standard format for license keys that are:

  • Signed: the token carries a cryptographic signature.
  • Tamper-evident: changing a claim invalidates the signature.
  • Expressive: claims can encode expiry, customer identity, tier, and feature flags.

A typical JWT consists of three base64url-encoded parts separated by dots:

header.payload.signature

For licensing, the payload might look like this:

{
  "sub": "customer-123",
  "exp": 1780000000,
  "iat": 1750000000,
  "tier": "pro",
  "features": ["reports", "api"]
}

The signature is created with a private key. Py2Native Pro uses an EC P-256 keypair for signing and verification:

uv run py2native keygen private.pem public.pem

You keep private.pem offline. The corresponding public.pem is used at build time so the verification code can check signatures without exposing the signing key.

The challenge with pure-Python JWT verification is that the public key, algorithm choice, and signature-checking logic are all visible. Py2Native Pro changes that calculus: it compiles your custom Python into native machine code and bakes the verification routine and public key into the resulting executable. There is no Python source left to edit, and no external JWT library required at runtime.

What a JWT License Proof Looks Like

A license proof is the signed JWT that a customer pastes into a file or environment variable. To create one with Py2Native Pro, sign a JSON payload with the private key:

uv run py2native sign \
  --private private.pem \
  '{"sub":"customer-123","exp":1780000000,"iat":1750000000,"tier":"pro","features":["reports","api"]}' \
  license.dat

The resulting license.dat contains a compact JWT. Only someone with private.pem can create a token that validates against the public key you embed in the binary. That is the core proof: the JWT is not just structured data; it is data protected by a signature that your compiled application checks without relying on third-party libraries.

When debugging a customer issue, you can inspect the JWT claims with the Pro show command:

uv run py2native show license.dat

Or verify it explicitly against the public key:

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

This is useful for support workflows where the binary says “invalid license” but you want to see whether the token expired, was signed for a different product, or has the wrong feature set.

Step-by-Step: Verifying JWT License Keys with Py2Native Pro

Py2Native normally keeps Cython out of your way entirely. For Pro license verification, you add one small .pxd declaration file so the Pro plugin can expose the verification routine to your compiled code. That is not a manual Cython workflow; it is the bridge between your plain Python code and the native verification implementation.

Prerequisite: generate a keypair and a license

First, generate the EC P-256 keypair:

uv run py2native keygen private.pem public.pem

Then create a signed license for testing:

uv run py2native sign \
  --private private.pem \
  '{"sub":"customer-123","exp":1780000000,"features":["reports"]}' \
  license.dat

Step 1: Write the Python verification function

Create a Python module that loads the license token and calls the verification routine. The routine is linked in by the Pro plugin, so your Python code stays plain:

# license_check.py
from pathlib import Path

def load_license(path: str = "license.dat") -> str:
    return Path(path).read_text().strip()

def check_license(token: str):
    # verify_license is declared in license_check.pxd and linked
    # into the compiled binary by the Py2Native Pro plugin.
    return  license = _runtime_verify_es256_jwt(token, expected_iss="RSJ Software GmbH", expected_aud="TimestampGIT")

You do not need to implement _runtime_verify_es256_jwt in Python. The Pro plugin supplies the compiled verification logic and embeds the public key during the build.

Step 2: Add the .pxd declaration

Next to license_check.py, add license_check.pxd. This declares the verification function for the compiler:

# license_check.pxd
rom _p2n_bootstrap cimport _runtime_verify_es256_jwt

The exact signature may vary based on your Pro plugin version, but the idea is always the same: declare the verification symbol in .pxd, keep the Python wrapper thin, and let Py2Native handle the native link step.

Step 3: Build with the license and public key

Now build your application with Pro verification enabled:

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

The Pro plugin bakes the signature verification code and public key into the executable. Your application can then verify a JWT without a Python JWT library, without a separate public-key file, and without exposing the verification path in source form.

A minimal entry point can call the compiled verification function and fail closed:

# main.py
from license_check import check_license, load_license

def main():
    token = load_license()

    try:
        claims = check_license(token)
    except Exception as exc:
        print(f"Invalid license: {exc}")
        raise SystemExit(1)

    print(f"Licensed to {claims['sub']} with features={claims['features']}")

if __name__ == "__main__":
    main()

In practice, you would structure your error handling around the specific status values your Pro verification routine returns. The important behavior is that an invalid, expired, or tampered token never reaches your application logic.

Interpreting Verification Results and Handling Edge Cases

A verification function should never return only “valid” or “invalid.” Real deployments need enough status detail to tell a customer what to do.

Typical cases include:

  • Valid token: allow the application to start.
  • Expired token: show a renewal message with the customer ID and expiry date.
  • Tampered token: treat as invalid and refuse to run; do not reveal which part of the token failed.
  • Missing license file: show a clear setup message and exit.

Clock skew is the most common edge case in offline license verification. A customer machine with the clock set five minutes behind should not be locked out of a license that just expired. If your licensing model allows renewals, build a small grace period into the verification logic. The JWT exp claim remains the source of truth, but your compiled code can decide how strictly to enforce it.

Feature-based licensing can use a claim like features to gate specific modules. Because the verification result comes from native code, the gate itself lives in the binary:

if "reports" not in claims["features"]:
    disable_reports_module()

Offline verification works because the JWT signature check is entirely local. No network call is needed to validate the token. The public key is embedded in the binary, so a disconnected customer can still be verified.

The manual debugging path is the hard way: write Cython yourself, manage .pyx and .pxd files, and link C extensions by hand. Py2Native Pro automates that path and gives you a build command plus a show command for inspecting JWT claims.

Best Practices for Secure License Verification in Python

License verification is one layer in a larger code-protection strategy. A few practices make it materially harder to bypass:

  • Keep the private key offline. Sign licenses only in a controlled environment. Never ship private.pem.
  • Distribute only the public key at build time. The compiled binary should contain exactly what it needs to verify, not to sign.
  • Use short-lived tokens or a renewal grace period. Short expiry limits how long a leaked license remains useful.
  • Expire feature flags explicitly. If a customer downgrades, their next token should not carry the removed features.
  • Combine native compilation with Pro string compression. Hiding the verification routine in native code is strongest when the supporting strings and messages are not trivially searchable.
  • Change verification logic periodically. Even a small change between releases forces an attacker to restart reverse-engineering work.

The main weakness to keep in mind is monkey-patching. Third-party libraries remain Python source by design, and LGPL libraries remain replaceable by the end user for compliance reasons. Your core license check should not depend on a third-party Python call that can be swapped at runtime. Keep that check inside your compiled application, where Py2Native Pro places it.

FAQ

Q: Can I verify JWT license keys in pure Python without compiling?

A: Yes, you can use libraries like PyJWT, but the verification logic remains visible and can be bypassed. Py2Native Pro compiles your verification code to native machine code, making it much harder to tamper with.

Q: What is the advantage of embedding the public key in the binary?

A: Embedding the public key ensures that the verification uses the exact key you intend, preventing an attacker from swapping in their own key. Py2Native Pro bakes the public key and verification code into the executable, so no external key file is needed at runtime.

Q: Do I need to include a .pxd file for JWT verification?

A: Yes, Py2Native Pro requires a .pxd file to declare the verification function for Cython compilation. This file tells the compiler how to expose the function to the native code.

Q: Can I use Py2Native Pro to verify licenses offline?

A: Yes, the verification is entirely local. The JWT is signed with your private key, and the public key is embedded in the binary. No network call is needed to verify the license.

Conclusion

Verifying JWT license keys in Python is straightforward. Doing it in a way that survives distribution is the hard part. Pure-Python verification leaves the check readable and patchable, but Py2Native Pro moves the proof into native machine code, embeds the public key, and removes the need for third-party JWT libraries at runtime.

The workflow is simple: generate a keypair, sign a JWT, add a small .pxd declaration, and build with uv run py2native build --license license.dat --public public.pem main.py *.py. Your customers get a binary, and you get a license check that is a lot harder to bypass.

If you are shipping proprietary Python, protect the verification path as deliberately as the code it guards. Get the Pro build at Py2Native.

Related posts

EU label: AI-generated content