Push Tracker
ria-ran--sim-drop/plan/02-profile-generator.md
A ashkan@beigi.net 8672b0de6b Add Phase-1 eSIM provisioning implementation
Full working closed-loop eSIM provisioning stack (spec in claude.md / plan/):
Flask API, SQLite subscriber store, credential/profile generator, Open5GS
and free5gc core adapters, captive portal, operational scripts, systemd
units, network configs, and a 44-test suite.

Profile generation currently returns a mock JSON profile; real SGP.22
generation via pySim is the planned Phase-2 upgrade.

Fixes two bugs found while getting the suite green:
- generate_imsi hardcoded a 10-digit MSIN, producing 16-digit IMSIs for a
  3-digit MNC; MSIN length is now 15 - len(mcc) - len(mnc).
- check_rate_limit compared timestamps as strings across mismatched formats
  (SQLite CURRENT_TIMESTAMP vs Python isoformat); both sides now normalized
  via SQLite datetime() so the window check is chronologically correct.

Add .gitignore for venv, caches, runtime config.yaml, and databases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 05:56:31 +00:00

3.1 KiB

Unit 02 — Profile Generator

Goal

Cryptographic credential generation (IMSI, Ki, OPc, ICCID) and Phase 1 mock eSIM profile. Real pySim integration is deferred to Unit 06.

File

  • server/profile_generator.py

Functions

generate_imsi(mcc: str, mnc: str) -> str

  • Concatenate MCC + MNC + 10-digit random MSIN
  • MSIN generated with secrets.randbelow(10**10) zero-padded to 10 digits
  • Result must be exactly 15 digits
  • Validates format before returning

generate_ki() -> bytes

  • secrets.token_bytes(16) — 16 cryptographically random bytes

generate_iccid() -> str

  • Industry prefix: 8910 (Telecom, country code 10)
  • Append MCC+MNC from config
  • Fill remaining digits randomly
  • Total 18 digits + 1 Luhn check digit = 19 digits
  • Luhn algorithm:
    def luhn_checksum(number: str) -> int:
        digits = [int(d) for d in number]
        odd_digits = digits[-1::-2]
        even_digits = digits[-2::-2]
        total = sum(odd_digits)
        for d in even_digits:
            total += sum(divmod(d * 2, 10))
        return (10 - (total % 10)) % 10
    

derive_opc(ki: bytes, op: bytes) -> bytes

Milenage OPc derivation — XOR of OP with AES_Ki(OP):

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

def derive_opc(ki: bytes, op: bytes) -> bytes:
    cipher = Cipher(algorithms.AES(ki), modes.ECB(), backend=default_backend())
    enc = cipher.encryptor()
    encrypted_op = enc.update(op) + enc.finalize()
    return bytes(a ^ b for a, b in zip(encrypted_op, op))

create_esim_profile(imsi, ki, opc, iccid, profile_name, carrier_name) -> bytes

Phase 1 (mock): Returns JSON-encoded bytes with all credential fields:

import json

def create_esim_profile(imsi, ki, opc, iccid, profile_name, carrier_name):
    profile = {
        "version": "1.0",
        "type": "mock",
        "iccid": iccid,
        "imsi": imsi,
        "ki": ki.hex() if isinstance(ki, bytes) else ki,
        "opc": opc.hex() if isinstance(opc, bytes) else opc,
        "profile_name": profile_name,
        "carrier_name": carrier_name,
    }
    return json.dumps(profile).encode("utf-8")

Phase 2 (Unit 06): Replace with real pySim SGP.22 profile generation.


Known Test Vectors (for Unit 10 tests)

From 3GPP TS 35.208:

  • Ki = 000102030405060708090a0b0c0d0e0f
  • OP = 63bfa50ee6523365ff14c1f45f88737d
  • OPc = compute at test time: AES_Ki(OP) XOR OP

Verification

python -c "
from server.profile_generator import generate_imsi, generate_ki, generate_iccid, derive_opc, create_esim_profile
import binascii

imsi = generate_imsi('302', '720')
assert len(imsi) == 15 and imsi.startswith('302720'), f'bad IMSI: {imsi}'

ki = generate_ki()
assert len(ki) == 16

iccid = generate_iccid()
assert len(iccid) == 19, f'bad ICCID length: {len(iccid)}'

op = bytes.fromhex('63bfa50ee6523365ff14c1f45f88737d')
opc = derive_opc(ki, op)
assert len(opc) == 16

profile = create_esim_profile(imsi, ki, opc, iccid, 'Emergency 5G', 'Ad-Hoc Network')
assert len(profile) > 0
print('All checks passed')
"