How to Build a Secure, Text-Based License Verification System in Python

generated by nano banana 🍌
If you are distributing software to clients, you often need a way to ensure their license is valid and hasn’t expired. You might be tempted to hide a check inside your code like if date > 2025: exit(), but that is easily bypassed by changing the system clock or patching the code.
A better approach is using Digital Signatures.
In this tutorial, we will build a lightweight, text-based licensing system. We will move away from bulky RSA keys and files, opting instead for Ed25519 (Elliptic Curve) cryptography. This allows us to use short, copy-pasteable strings for keys and signatures.
What We Are Building
- The Server (You): Generates a license (JSON) and a short Signature string.
- The Client (Your App): Verifies the signature to ensure the data hasn’t been tampered with, then checks if the license is expired.
Prerequisites
We will use PyNaCl, a modern, high-security cryptography library.
pip install pynacl
Part 1: The Core Logic
We need a way to ensure that the JSON data we sign is exactly the same, byte-for-byte, when we verify it. We’ll use a helper function to standardise our JSON.
import json
import base64
from datetime import datetime
from nacl.signing import SigningKey, VerifyKey
from nacl.encoding import Base64Encoder
from nacl.exceptions import BadSignatureError
def _prepare_data(data: dict) -> bytes:
"""
Sorts keys and removes spaces to ensure
{'a': 1, 'b': 2} results in the exact same bytes as {'b': 2, 'a': 1}
"""
return json.dumps(data, sort_keys=True, separators=(',', ':')).encode('utf-8')
Part 2: Generating Keys (The Admin Side)
Unlike RSA keys which are huge blocks of text, Ed25519 keys are tiny (~44 characters).
- Private Key: Keep this secret. You use it to create licenses.
- Public Key: Embed this in your client application. It verifies licenses.
def generate_keys():
"""Generates a pair of Base64 encoded keys."""
signing_key = SigningKey.generate()
verify_key = signing_key.verify_key
return (
# Public Key (Safe to share)
verify_key.encode(encoder=Base64Encoder).decode('utf-8'),
# Private Key (KEEP SECRET)
signing_key.encode(encoder=Base64Encoder).decode('utf-8')
)
Part 3: Creating a License
When a client buys your software, you create a payload (usually containing their domain or ID and an expiry date) and sign it.
def create_license_signature(data: dict, private_key_b64: str) -> str:
# Load the private key
private_obj = SigningKey(private_key_b64.encode('utf-8'), encoder=Base64Encoder)
# Sign the data
signed = private_obj.sign(_prepare_data(data))
# Return just the signature as a short Base64 string
return base64.b64encode(signed.signature).decode('utf-8')
Part 4: Verifying the License (The Client Side)
This is the code that goes into your software. It performs two checks:
- Integrity Check: Is the signature valid? (Did you sign this?)
- Expiry Check: Is today’s date past the
expirydate in the JSON?
def verify_license(data: dict, signature_b64: str, public_key_b64: str) -> dict:
# --- Step 1: Verify Signature ---
try:
public_obj = VerifyKey(public_key_b64.encode('utf-8'), encoder=Base64Encoder)
signature_bytes = base64.b64decode(signature_b64)
# This will raise an error if the signature doesn't match the data
public_obj.verify(_prepare_data(data), signature_bytes)
except (BadSignatureError, ValueError):
return {"valid": False, "message": "Signature Invalid: Data tampered or key mismatch."}
# --- Step 2: Verify Expiry Date ---
if "expiry" not in data:
return {"valid": False, "message": "Invalid License: No expiry date found."}
try:
expiration_date = datetime.strptime(data["expiry"], "%Y-%m-%d").date()
today = datetime.now().date()
if today > expiration_date:
return {
"valid": False,
"message": f"License Expired. (Ended: {expiration_date})"
}
days_left = (expiration_date - today).days
return {
"valid": True,
"message": "License Active",
"days_remaining": days_left
}
except ValueError:
return {"valid": False, "message": "Date format error. Use YYYY-MM-DD."}
Putting it all together
Here is how the flow looks in action.
if __name__ == "__main__":
# 1. Admin generates keys ONCE.
public_key, private_key = generate_keys()
print(f"Public Key to embed in app: {public_key}")
# 2. Client buys a 1-year license
client_data = {
"domain": "example.com",
"expiry": "2030-12-31" # A future date
}
# 3. Admin generates a signature
signature = create_license_signature(client_data, private_key)
print(f"License Signature: {signature}")
# --- SIMULATING THE CLIENT APP ---
# Client App verifies the data using the Public Key
result = verify_license(client_data, signature, public_key)
print(f"\nVerification Result: {result}")
Why is this secure?
You might ask: “Can’t the user just open the JSON file and change the date from 2023 to 2030?”
Let’s try it:
# Hacker attempts to modify the expiry date
hacked_data = client_data.copy()
hacked_data["expiry"] = "2099-01-01"
# They try to verify it using the OLD signature
result = verify_license(hacked_data, signature, public_key)
print(f"\nHack Attempt Result: {result}")
# Output: {'valid': False, 'message': 'Signature Invalid: Data tampered...'}
The verification fails instantly. The signature is mathematically tied to the specific combination of characters in the original data. If a single byte changes in the data, the signature becomes invalid. Since the user doesn’t have your Private Key, they cannot generate a new valid signature for their fake date.
Summary
By switching to PyNaCl (Ed25519), we achieved:
- Short Strings: No more dealing with
.pemfiles on the client side. - Date Validation: Logic that checks against today’s system date.
- Tamper Proofing: Cryptographic certainty that the license details haven’t been altered.