Skip to content
Go back

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

Updated:

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

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

  1. The Server (You): Generates a license (JSON) and a short Signature string.
  2. 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).

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:

  1. Integrity Check: Is the signature valid? (Did you sign this?)
  2. Expiry Check: Is today’s date past the expiry date 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:

  1. Short Strings: No more dealing with .pem files on the client side.
  2. Date Validation: Logic that checks against today’s system date.
  3. Tamper Proofing: Cryptographic certainty that the license details haven’t been altered.

Share this post on:

Previous Post
Integrate Google Drive with FastAPI Using OAuth2
Next Post
Building a Asymmetric License Verification System with Python and RSA