DEV Community

Mithun Kumar
Mithun Kumar

Posted on

License Key Validation in Python: The Right Way

The first time I shipped a paid desktop tool, I added a "license check" in about
ten minutes and felt clever. It called my server, asked "is this key valid?",
and unlocked the app if the answer was yes.

It took someone about ten minutes to defeat it.

They did not guess a key or patch my binary. They pointed my app at a fake
local server that always says "valid."
My whole licensing system trusted one
network reply, and that reply was trivially forgeable.

Here is the trap I fell into, why it fails, and the fix that actually works — with
code you can copy.

The naive license check (that everyone writes first)

import requests

def is_licensed(key: str) -> bool:
    r = requests.post("https://api.myapp.com/validate", json={"key": key})
    return r.json().get("valid") is True

if not is_licensed(user_key):
    raise SystemExit("Unlicensed")
Enter fullscreen mode Exit fullscreen mode

Looks fine, right? It is not. Anyone who runs your program can:

  1. Watch that request (it is their machine).
  2. Run a tiny local server that returns {"valid": true} for everything.
  3. Redirect your app to it — a hosts-file line, a proxy, or a one-character patch to the URL.

Now is_licensed() returns True forever, offline, for every key. The check was
never the hard part. Making the reply impossible to fake is.

Why this is really a cryptography problem

The fix is to stop trusting the reply and start verifying it. The server
should sign every response with a private key that never leaves the server.
Your app ships only the matching public key and checks the signature locally.

  • A fake server does not have your private key, so it cannot produce a valid signature.
  • A forged {"valid": true} fails the signature check instantly.
  • Even a real reply cannot be replayed, because the signature covers a one-time nonce your app generates per request.

This is exactly how software updates and TLS certificates are trusted. Licensing
deserves the same bar.

Doing it properly in ~5 minutes

You could build the signing, key management, nonce handling, and verification
yourself. I did, and then I turned it into a hosted tool called
Licers so I would never write it again. (Full disclosure: I
built it. It is free, and you can absolutely roll your own with the same ideas.)

Here is the whole client with the SDK:

pip install pylicensify
Enter fullscreen mode Exit fullscreen mode
from pylicensify import LicenseClient

# The PUBLIC key is safe to embed in your distributed app.
client = LicenseClient(public_key="YOUR_PUBLIC_KEY")

result = client.validate("PY-XXXXXXXXXXXXXXXX")
if result:
    print("Licensed. Features:", result.features)
    unlock_app()
else:
    print("Not licensed:", result.error)
    raise SystemExit(1)
Enter fullscreen mode Exit fullscreen mode

validate() sends a fresh nonce, then verifies the Ed25519 signature on the
response and rejects anything stale or replayed. Point the app at a fake server
and it raises SignatureError instead of unlocking. That is the entire
difference between "licensing" and "a suggestion."

Bonus: stop one key from running everywhere

Signed validation stops fake servers. To stop sharing, bind a key to a limited
number of devices:

# The SDK derives a stable device id and registers it against the key.
result = client.validate("PY-XXXX...")
# In the dashboard you set the max devices per key; extra machines get rejected.
Enter fullscreen mode Exit fullscreen mode

One gotcha if you roll your own: do not use uuid.getnode() as the device
id. On Android and many VMs the MAC address is not readable, so it returns a
random value every run — every launch looks like a new device. Persist a device
id to disk instead (the SDK does this for you).

The takeaway

If your license check trusts a plain server reply, assume it is already bypassed.
The upgrade is small and worth it:

  1. Sign every response server-side with a key that never leaves the server.
  2. Verify the signature in your app with an embedded public key.
  3. Add a nonce so replies cannot be replayed.
  4. Optionally bind keys to devices to stop sharing.

That is the difference between licensing that stops a determined user and
licensing that a fake local server defeats before lunch.


I write about shipping and protecting software. If you want the signed-validation
setup without building it yourself, Licers is free to start
and has a Python SDK. Happy to answer licensing
questions in the comments.

Top comments (0)