Skip to content
Go back

Building a Asymmetric License Verification System with Python and RSA

Updated:

Building a Asymmetric License Verification System with Python and RSA

alt text

generated by nano

The RSA (Rivest-Shamir-Adleman) algorithm is an asymmetric, or public-key, cryptography algorithm. It operates with two different keys: a public key and a private key. The public key is used for encryption and is accessible to anyone, while the private key is used for decoding and must be kept secret by the receiver.

Verifying that a user’s license is valid and hasn’t been tampered with is a hard problem. Other modern curves exist (like Ed25519), but RSA remains the long-standing standard for digital signatures.

In this tutorial, we build a text-based licensing system where:

  1. The server generates a JSON license and signs it using a private key.
  2. The client verifies the signature using a public key and checks if the license has expired.

Why RSA?

RSA keys are larger than modern alternatives, but the algorithm is ubiquitous. It is natively supported by almost every programming language and security library, making it an excellent choice for cross-platform compatibility.

Prerequisites

We will use the pure-Python rsa library.

pip install rsa

Part 1: The Setup (Helpers)

Just like any cryptographic signature, we need to ensure the data we sign is deterministic. We must serialize our JSON dictionary exactly the same way every time (sorted keys, no extra spaces).

import rsa
import json
from datetime import datetime
from typing import Tuple, Dict

def _prepare_data(data: dict) -> bytes:
    """
    Converts a dictionary to a byte string with sorted keys.
    Required for consistent signing.
    """
    return json.dumps(data, sort_keys=True, separators=(',', ':')).encode('utf-8')

Part 2: Generating Keys

RSA keys are typically stored in PEM format. For this tutorial, we will generate them and keep them as byte strings in memory.

def generate_rsa_keys() -> Tuple[bytes, bytes]:
    """
    Generates 1024-bit RSA keys. 
    (Use 2048 for higher security in production).
    """
    public_key, private_key = rsa.newkeys(1024)
    
    # Export to PEM format (bytes)
    pub_pem = public_key.save_pkcs1("PEM")
    priv_pem = private_key.save_pkcs1("PEM")
    
    return pub_pem, priv_pem

Part 3: Signing the License

When you issue a license, you take the data (e.g., domain, expiry date) and sign it using your Private Key. We convert the resulting binary signature into a Hexadecimal string so it’s easy to copy-paste or send via email.

def sign_license(data: Dict, private_key_pem: bytes) -> str:
    # 1. Load Private Key
    private_key = rsa.PrivateKey.load_pkcs1(private_key_pem)

    # 2. Sign the prepared data using SHA-256
    signature_bytes = rsa.sign(_prepare_data(data), private_key, "SHA-256")

    # 3. Return as Hex string (e.g., "a1b2c3...")
    return signature_bytes.hex()

Part 4: Verifying the License

This is the code that lives inside your client application. It accepts the license data and the hex signature. It performs a two-step validation:

  1. Crypto Check: Does the signature match the data?
  2. Logic Check: Is the current date past the expiration date?
def verify_license_status(data: Dict, signature_hex: str, public_key_pem: bytes) -> Dict:
    # --- Step 1: Verify Signature ---
    try:
        public_key = rsa.PublicKey.load_pkcs1(public_key_pem)
        
        # Convert Hex string back to bytes
        signature_bytes = bytes.fromhex(signature_hex)
        
        # Verify (Raises VerificationError if invalid)
        rsa.verify(_prepare_data(data), signature_bytes, public_key)
        
    except (rsa.VerificationError, ValueError):
        return {"valid": False, "message": "Signature Invalid (Tampered Data)"}

    # --- Step 2: Check Expiry Date ---
    if "expiry" not in data:
        return {"valid": False, "message": "No expiry date in license"}

    try:
        # Parse date (Format YYYY-MM-DD)
        expiry_date = datetime.strptime(data["expiry"], "%Y-%m-%d").date()
        today = datetime.now().date()

        if today > expiry_date:
            return {
                "valid": False, 
                "message": f"License Expired on {expiry_date}"
            }
        
        days_left = (expiry_date - today).days
        return {
            "valid": True, 
            "message": "License Active", 
            "days_remaining": days_left
        }

    except ValueError:
        return {"valid": False, "message": "Invalid date format"}

Putting it all together

Here is a demonstration script simulating both the server (issuing) and the client (verifying).

if __name__ == "__main__":
    print("--- 1. Generating RSA Keys ---")
    pub_key, priv_key = generate_rsa_keys()
    # In a real app, you'd save 'priv_key' to a secure file
    # and embed 'pub_key' in your client code.

    print("--- 2. Creating a License ---")
    # A valid license (Future date)
    license_data = {
        "client": "Acme Corp",
        "tier": "Enterprise",
        "expiry": "2030-12-31"
    }

    # Sign it
    signature = sign_license(license_data, priv_key)
    print(f"Generated Hex Signature (Truncated): {signature[:50]}...")

    print("\n--- 3. Client Verification (Success Case) ---")
    result = verify_license_status(license_data, signature, pub_key)
    print(f"Result: {result}")

    print("\n--- 4. Client Verification (Tampering Case) ---")
    # Imagine a user tries to extend their license manually
    hacked_data = license_data.copy()
    hacked_data["expiry"] = "2099-01-01" 

    # They try to use the OLD signature with the NEW date
    result_hack = verify_license_status(hacked_data, signature, pub_key)
    print(f"Result: {result_hack}")

Output

--- 1. Generating RSA Keys ---
--- 2. Creating a License ---
Generated Hex Signature (Truncated): 6f4a2b9d1e...

--- 3. Client Verification (Success Case) ---
Result: {'valid': True, 'message': 'License Active', 'days_remaining': 1849}

--- 4. Client Verification (Tampering Case) ---
Result: {'valid': False, 'message': 'Signature Invalid (Tampered Data)'}

How this holds together

Using RSA lets you implement a licensing system without a central database check for every call. The signature detects tampering, and the logic check catches expiry.

The keys and signatures are longer than newer methods (like Elliptic Curve), but RSA’s wide support makes it a solid choice for enterprise software.


Share this post on:

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