#!/usr/bin/env python3 """Generate or inspect an eSIM profile — a dev/ops utility. Generates one profile (v2 JSON or real SGP.22 SAIP DER) using the network and profile settings from config.yaml, with random or supplied credentials, and writes it to a file or stdout. Can also decode an existing SAIP profile file. Examples: python scripts/make_profile.py --format saip --out /tmp/p.der python scripts/make_profile.py --inspect /tmp/p.der """ import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from server import saip_profile from server.profile_generator import ( create_esim_profile, derive_opc, generate_iccid, generate_imsi, generate_ki, ) from server.utils import hex_to_bytes, load_config def build_profile(cfg: dict, fmt: str = "v2", imsi=None, iccid=None): """Return (imsi, iccid, ki, opc, profile_bytes) for a freshly built profile.""" net = cfg["network"] prof = cfg.get("profile", {}) imsi = imsi or generate_imsi(net["mcc"], net["mnc"]) iccid = iccid or generate_iccid() ki = generate_ki() opc = derive_opc(ki, hex_to_bytes(net["op_key"])) if fmt == "saip": if not saip_profile.saip_available(): raise SystemExit("SAIP unavailable: install requirements-saip.txt " "(asn1tools + SAIP ASN.1 schema)") data = saip_profile.create_saip_profile( imsi, ki, opc, iccid, prof.get("profile_name", ""), prof.get("carrier_name", ""), ) else: data = create_esim_profile( imsi, ki, opc, iccid, prof.get("profile_name", ""), prof.get("carrier_name", ""), smdp_address=prof.get("smdp_address", "esim.local"), apn=prof.get("apn", "internet"), ) return imsi, iccid, ki, opc, data def summarize_saip(data: bytes) -> str: """Return a human-readable summary of a SAIP profile's ProfileElements.""" pes = saip_profile.decode_saip_profile(data) lines = [f"{len(data)} bytes, {len(pes)} ProfileElement(s):"] for choice, value in pes: detail = "" if choice == "header": detail = f" iccid={value.get('iccid', b'').hex()}" elif choice in ("mf", "usim") and isinstance(value, dict): detail = f" templateID={value.get('templateID')}" lines.append(f" - {choice}{detail}") return "\n".join(lines) def main(argv=None): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--config", default=os.environ.get("CONFIG_PATH", "config.yaml")) ap.add_argument("--format", choices=["v2", "saip"], default="v2") ap.add_argument("--imsi", help="use this IMSI instead of a random one") ap.add_argument("--iccid", help="use this ICCID instead of a random one") ap.add_argument("--out", help="output file (default: stdout)") ap.add_argument("--inspect", metavar="FILE", help="decode a SAIP profile file and exit") args = ap.parse_args(argv) if args.inspect: with open(args.inspect, "rb") as f: print(summarize_saip(f.read())) return 0 cfg = load_config(args.config) imsi, iccid, _ki, _opc, data = build_profile(cfg, args.format, args.imsi, args.iccid) if args.out: with open(args.out, "wb") as f: f.write(data) print(f"Wrote {len(data)} bytes to {args.out} " f"(format={args.format}, IMSI {imsi}, ICCID {iccid})", file=sys.stderr) else: sys.stdout.buffer.write(data) return 0 if __name__ == "__main__": sys.exit(main())