107 lines
3.1 KiB
Markdown
107 lines
3.1 KiB
Markdown
|
A
|
# 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:
|
||
|
|
```python
|
||
|
|
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):
|
||
|
|
```python
|
||
|
|
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:
|
||
|
|
```python
|
||
|
|
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
|
||
|
|
```bash
|
||
|
|
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')
|
||
|
|
"
|
||
|
|
```
|