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, which means it operates with two different keys: public and private keys. The Public Key is used for encryption and is accessible to anyone, whereas the Private Key is used for decoding and must be kept secret by the receiver.
In the world of software distribution, ensuring that a user’s license is valid—and hasn’t been tampered with—is a critical challenge. While there are many modern cryptographic curves (like Ed25519), RSA remains the classic, battle-tested standard for digital signatures.
In this tutorial, we will 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.
Crucial 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)'}
Conclusion
Using RSA allows you to implement a robust licensing system without needing a central database connection for every check. The signature guarantees integrity, and the logic check ensures validity over time.
While the keys and signatures are longer than newer methods (like Elliptic Curve), RSA’s widespread support makes it a reliable choice for enterprise software.