#!/usr/bin/env python3 """Generate a fresh Ed25519 keypair for production license signing. **Creator-only.** Run once, write the private key somewhere safe, configure the build pipeline with the public key. Usage:: python scripts/generate_keypair.py python scripts/generate_keypair.py --json python scripts/generate_keypair.py --output keys.txt The output looks like:: DATATOOLS_LICENSE_PRIVKEY=<64 hex chars> # KEEP SECRET DATATOOLS_LICENSE_PUBKEY=<64 hex chars> # BAKE INTO BUILD The private key never goes near the buyer-facing binary. Stash it in a password manager / KMS / hardware token; the only places it gets loaded are: - ``scripts/generate_license.py`` when minting a buyer's blob - Your CI's signing step, if you've automated blob minting The public key gets set as ``DATATOOLS_LICENSE_PUBKEY`` in the PyInstaller build env (so the shipped binary verifies against it), and the production-safe runtime check refuses to start any frozen build that's still using the in-source dev key. """ from __future__ import annotations import argparse import json import sys from pathlib import Path from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey def generate() -> tuple[str, str]: """Return ``(private_hex, public_hex)`` for a fresh keypair.""" priv = Ed25519PrivateKey.generate() priv_hex = priv.private_bytes( encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption(), ).hex() pub_hex = priv.public_key().public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw, ).hex() return priv_hex, pub_hex def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--json", action="store_true", help="Emit JSON instead of env-file format.") p.add_argument("--output", "-o", type=Path, default=None, help="Write to this file instead of stdout.") args = p.parse_args(argv) priv_hex, pub_hex = generate() if args.json: payload = json.dumps( {"private_key": priv_hex, "public_key": pub_hex}, indent=2, ) else: payload = ( f"# DataTools license keypair — generated by generate_keypair.py\n" f"# KEEP THE PRIVATE KEY SECRET. Lose it and your existing\n" f"# licenses can't be renewed (you'd have to ship a new build\n" f"# with a new public key and re-issue every active license).\n" f"\n" f"DATATOOLS_LICENSE_PRIVKEY={priv_hex}\n" f"DATATOOLS_LICENSE_PUBKEY={pub_hex}\n" ) if args.output: args.output.write_text(payload + "\n", encoding="utf-8") # chmod 600 — best-effort; ignored on Windows. try: args.output.chmod(0o600) except OSError: pass print(f"Wrote {args.output} (mode 600)", file=sys.stderr) else: print(payload) print( "\nNext steps:\n" " 1. Store the private key in your password manager.\n" " 2. Bake the public key into the PyInstaller build:\n" " DATATOOLS_LICENSE_PUBKEY= pyinstaller ...\n" " 3. Mint buyer licenses by setting the private key:\n" " DATATOOLS_LICENSE_PRIVKEY= " "python scripts/generate_license.py --name 'Buyer' --email b@x.com\n", file=sys.stderr, ) return 0 if __name__ == "__main__": sys.exit(main())