Building a Asymmetric License Verification System with Python and RSA

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:
- The server generates a JSON license and signs it using a private key.
- 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.
- Private Key: Used to sign licenses. Never share this.
- Public Key: Distributed with your application. Used to verify licenses.
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:
- Crypto Check: Does the signature match the data?
- 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.