"""Vendor licence signing tool - KEEP THIS AND THE PRIVATE KEY OUT OF CUSTOMER BUILDS. This is the only thing that can create a valid licence. The application can only verify. If the private key leaks, every licence ever issued becomes forgeable and it cannot be rotated without reissuing to every customer, because the public key is compiled into shipped binaries. Usage: Create your keypair once: python license_tool.py keygen Then for each customer, from the fingerprint and components they send you: python license_tool.py sign --customer "Port of Varna" --stations 5 \\ --components '{"board":"ab12...","disk":"cd34...","mac":"ef56...","guid":"78ab..."}' The customer reads their component hashes from the admin licence screen (there is a copy button) - they are hashes, not raw serial numbers, so they carry no identifying hardware detail. """ import argparse import base64 import json import os import sys from cryptography.hazmat.primitives.asymmetric.ed25519 import ( Ed25519PrivateKey, Ed25519PublicKey, ) from cryptography.hazmat.primitives import serialization KEY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'license_private.key') def keygen(args): if os.path.exists(KEY_PATH) and not args.force: print(f"Refusing to overwrite {KEY_PATH}") print("Every licence signed with the existing key would stop verifying.") print("Pass --force only if you are certain.") return 1 private_key = Ed25519PrivateKey.generate() raw = private_key.private_bytes( encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption(), ) with open(KEY_PATH, 'wb') as fh: fh.write(base64.b64encode(raw)) public_raw = private_key.public_key().public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw, ) public_b64 = base64.b64encode(public_raw).decode() print(f"Private key written to {KEY_PATH}") print(" Back this up somewhere safe. It cannot be recovered.") print(" Never commit it, never ship it.\n") print("Put this public key in backend/api/licensing.py as LICENSE_PUBLIC_KEY:\n") print(f"LICENSE_PUBLIC_KEY = '{public_b64}'") return 0 def _load_private_key(): if not os.path.exists(KEY_PATH): print(f"No private key at {KEY_PATH} - run 'keygen' first.") sys.exit(1) with open(KEY_PATH, 'rb') as fh: return Ed25519PrivateKey.from_private_bytes(base64.b64decode(fh.read())) def sign(args): private_key = _load_private_key() try: components = json.loads(args.components) except json.JSONDecodeError as exc: print(f"--components is not valid JSON: {exc}") return 1 known = {'board', 'disk', 'mac', 'guid'} unknown = set(components) - known if unknown: print(f"Unknown component keys: {sorted(unknown)} (expected {sorted(known)})") return 1 present = [k for k, v in components.items() if v] if len(present) < 3: print(f"Only {len(present)} components supplied; at least 3 must match on the") print("customer machine, so signing this licence would lock them out.") return 1 payload = { 'customer': args.customer, 'stations': args.stations, 'components': components, } if args.expires: payload['expires'] = args.expires payload_json = json.dumps(payload, sort_keys=True, separators=(',', ':')) signature = private_key.sign(payload_json.encode('utf-8')) blob = base64.b64encode( f"{payload_json}.{base64.b64encode(signature).decode()}".encode('utf-8') ).decode() print(f"Licence for {args.customer}, {args.stations} station(s)") print(f"Components supplied: {', '.join(sorted(present))}") if args.expires: print(f"Expires: {args.expires}") else: print("Perpetual (no expiry)") print("\nSend this to the customer:\n") print(blob) return 0 def main(): parser = argparse.ArgumentParser(description=__doc__) sub = parser.add_subparsers(dest='command', required=True) p_keygen = sub.add_parser('keygen', help='Create the vendor keypair (once)') p_keygen.add_argument('--force', action='store_true') p_keygen.set_defaults(func=keygen) p_sign = sub.add_parser('sign', help='Sign a licence for a customer machine') p_sign.add_argument('--customer', required=True) p_sign.add_argument('--stations', type=int, required=True) p_sign.add_argument('--components', required=True, help='JSON of hashed components from the admin screen') p_sign.add_argument('--expires', default=None, help='ISO date, e.g. 2027-01-01T00:00:00+00:00 (omit for perpetual)') p_sign.set_defaults(func=sign) args = parser.parse_args() sys.exit(args.func(args)) if __name__ == '__main__': main()