A
8672b0de6b
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>
3.1 KiB
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')
"