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>
14 KiB
Closed-Loop eSIM Provisioning System for Emergency/NTN 5G Deployment
Overview
Self-contained eSIM provisioning system for ad-hoc 5G network deployment in emergency or NTN scenarios. Users connect to WiFi AP, automatically receive eSIM profile, then connect to 5G network.
System Architecture
┌─────────────────────────────────────────┐
│ Server Host (srsRAN gNB) │
│ - srsRAN gNB │
│ - Open5GS Core (or free5gc) │
│ - Subscriber database │
└─────────────┬───────────────────────────┘
│ 5G NR
│
▼
┌─────────────────────────────────────────┐
│ UE WiFi AP Drop │
│ (Android device or Raspberry Pi) │
│ │
│ ┌───────────────────────────────────┐ │
│ │ 5G Modem (pre-provisioned SIM) │ │
│ │ Connects to gNB for backhaul │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ WiFi AP (open network) │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ eSIM Provisioning Server │ │
│ │ - Profile generator │ │
│ │ - Captive portal │ │
│ │ - Credential database │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│ WiFi
▼
End User Devices
User Flow
- User device detects open WiFi network
- Connects to WiFi → Auto-redirected to captive portal
- Captive portal displays "Activate eSIM" button
- Click → Server generates unique IMSI/Ki/OPc credentials
- Server generates SGP.22 eSIM profile with credentials
- Profile delivered to device via LPA protocol
- Device installs eSIM profile
- User disconnects from WiFi
- Device connects to 5G network using new eSIM
- Open5GS/free5gc authenticates user
Components
1. UE WiFi AP Drop Device
Hardware Options:
- Primary: Android phone (cheapest if app is simple)
- Fallback: Raspberry Pi 4 + Quectel RM500Q 5G modem
Requirements:
- Pre-provisioned "admin" SIM to connect to gNB
- WiFi hotspot capability
- Runs provisioning server stack
Software Stack:
- Cellular connection to gNB (uses pre-provisioned SIM)
- WiFi AP (hostapd or Android hotspot)
- Captive portal (dnsmasq + nginx)
- eSIM provisioning server (Python)
2. eSIM Provisioning Server
Technology: Python + Flask
Core Functions:
- Generate IMSI/Ki/OPc credentials on-demand
- Create SGP.22-compliant eSIM profiles using pySim
- Serve profiles via LPA protocol
- Store generated credentials in SQLite
- Rate limiting (1 profile per MAC address per hour)
- Export credentials for core sync
Dependencies:
pySim- SIM/eSIM profile generationFlask- Web serversqlite3- Local credential storagecryptography- Key generation
Endpoints:
GET / - Captive portal landing page
POST /activate - Trigger eSIM generation
GET /profile/:id - Download eSIM profile (LPA)
GET /export - Export credentials for core sync
Profile Generation Logic:
# Generate credentials
IMSI = generate_imsi(mcc=302, mnc=720) # Adjust for your network
Ki = random_bytes(16)
OPc = derive_opc(Ki, OP)
# Create eSIM profile
profile = create_esim_profile(
iccid=generate_iccid(),
imsi=IMSI,
ki=Ki,
opc=OPc,
profile_name="Emergency 5G"
)
# Store in database
store_subscriber(imsi, ki, opc, timestamp, mac_address)
# Return profile for LPA download
return profile
3. Captive Portal
Technology: HTML + JavaScript + nginx
Features:
- Auto-redirect any HTTP request to portal
- Single-button interface: "Activate eSIM"
- Instructions for eSIM installation
- Minimal UI, works on any device
DNS Hijacking:
# dnsmasq configuration
address=/#/192.168.4.1
nginx Configuration:
server {
listen 80 default_server;
server_name _;
location / {
return 302 http://192.168.4.1/activate;
}
location /activate {
root /var/www/captive;
index index.html;
}
location /api {
proxy_pass http://127.0.0.1:5000;
}
}
4. Core Integration Module
Design: Adapter pattern for portability
Supported Cores (initially):
- Open5GS
- free5gc
Interface:
class CoreAdapter:
def add_subscriber(self, imsi, ki, opc):
"""Add subscriber to core network"""
pass
def remove_subscriber(self, imsi):
"""Remove subscriber from core network"""
pass
def list_subscribers(self):
"""List all subscribers"""
pass
Open5GS Implementation:
class Open5GSAdapter(CoreAdapter):
def add_subscriber(self, imsi, ki, opc):
# Direct MongoDB insertion
db.subscribers.insert_one({
"imsi": imsi,
"security": {
"k": ki,
"opc": opc,
"amf": "8000",
"sqn": 0
},
"ambr": {
"downlink": {"value": 1, "unit": 3}, # 1 Gbps
"uplink": {"value": 1, "unit": 3}
},
"slice": [{
"sst": 1,
"default_indicator": True,
"session": [{
"name": "internet",
"type": 3, # IPv4v6
"ambr": {
"downlink": {"value": 1, "unit": 3},
"uplink": {"value": 1, "unit": 3}
}
}]
}]
})
Sync Options:
- Push: Provisioning server calls core adapter immediately
- Pull: Core polls provisioning server API periodically
- Manual: Export JSON, manually import to core
Initial Implementation: Export JSON format
{
"subscribers": [
{
"imsi": "302720123456789",
"ki": "00112233445566778899aabbccddeeff",
"opc": "63bfa50ee6523365ff14c1f45f88737d",
"timestamp": "2025-02-11T10:30:00Z",
"mac_address": "aa:bb:cc:dd:ee:ff"
}
]
}
5. eSIM Profile Format
Standard: SGP.22 compliant eSIM profile
Profile Contents:
- ICCID (SIM card identifier)
- IMSI (subscriber identity)
- Ki (authentication key)
- OPc (derived operator key)
- Profile metadata (name, description)
- APN configuration (if needed)
Generation Method: Use pySim's eUICC profile generator:
from pySim.esim import EsimProfile
profile = EsimProfile(
iccid=iccid,
imsi=imsi,
ki=ki,
opc=opc,
profile_name="Emergency 5G",
carrier_name="Ad-Hoc Network"
)
profile_bytes = profile.encode()
Fallback: If SGP.22 generation fails, serve simplified profile (may have compatibility issues with some devices)
Implementation Phases
Phase 1: Core Server Components
- eSIM provisioning server (Python/Flask)
- Credential generation and storage
- Profile generation using pySim
- LPA protocol implementation
- SQLite database schema
Phase 2: Captive Portal
- HTML/JS frontend
- nginx configuration
- DNS hijacking setup
- Auto-redirect logic
Phase 3: Core Integration
- Open5GS adapter
- JSON export format
- Sync script
Phase 4: UE WiFi AP Drop
- Android app (if feasible)
- OR Raspberry Pi setup scripts
- Auto-connection to gNB
- WiFi AP configuration
- Service orchestration
Phase 5: Testing & Hardening
- End-to-end flow testing
- Rate limiting verification
- Profile compatibility testing
- Network isolation verification
Deployment Instructions
Prerequisites
- srsRAN gNB running and configured
- Open5GS core running (or free5gc)
- One pre-provisioned "admin" SIM for backhaul UE
- UE device (Android phone or Raspberry Pi)
UE WiFi AP Drop Setup
If Android:
- Install Termux
- Install required packages (Python, hostapd, dnsmasq, nginx)
- Deploy provisioning server
- Configure WiFi hotspot
- Start services
If Raspberry Pi:
- Install Raspberry Pi OS Lite
- Install Quectel RM500Q modem drivers
- Configure modem for gNB connection
- Install provisioning server stack
- Configure hostapd + dnsmasq
- Start services
Provisioning Server Setup
# Install dependencies
pip install flask pySim cryptography
# Create directory structure
mkdir -p /opt/esim-provisioning/{server,portal,data}
# Deploy server code
cp provisioning_server.py /opt/esim-provisioning/server/
cp -r templates/ /opt/esim-provisioning/server/
# Deploy captive portal
cp -r portal/* /opt/esim-provisioning/portal/
# Initialize database
python init_db.py
# Start server
systemctl start esim-provisioning
Core Integration
# Export subscribers from provisioning server
curl http://192.168.4.1:5000/export > subscribers.json
# Import to Open5GS
python open5gs_import.py subscribers.json
# OR for free5gc
python free5gc_import.py subscribers.json
Configuration
Network Parameters
# config.py
MCC = "302" # Canada (adjust for your region)
MNC = "720" # Adjust for your network
PLMN = f"{MCC}{MNC}"
# IMSI format: MCC + MNC + MSIN
# Example: 302720123456789
IMSI_PREFIX = PLMN
MSIN_LENGTH = 10
# Operator key (OP) - used to derive OPc
OP = bytes.fromhex("63bfa50ee6523365ff14c1f45f88737d")
# WiFi AP configuration
WIFI_SSID = "Emergency-5G"
WIFI_CHANNEL = 6
WIFI_IP = "192.168.4.1"
WIFI_SUBNET = "192.168.4.0/24"
WIFI_DHCP_RANGE = "192.168.4.10,192.168.4.250"
# Rate limiting
MAX_PROFILES_PER_MAC = 1
RATE_LIMIT_HOURS = 1
Open5GS Connection
# open5gs_config.py
MONGODB_URI = "mongodb://localhost:27017"
DATABASE_NAME = "open5gs"
COLLECTION_NAME = "subscribers"
Security Considerations
Current Implementation (Phase 1)
- Open WiFi network (no authentication)
- Rate limiting by MAC address
- No encryption on provisioning server traffic (local network only)
Future Enhancements
- Optional WiFi password (distribute via physical signage)
- Pre-qualified activation codes
- HTTPS for provisioning server
- Profile encryption
- Audit logging
Scalability
On-Demand Generation
- Generates credentials only when requested
- No pre-generation overhead
- Limited only by:
- IMSI space (10^10 possible MSINs with 10-digit MSIN)
- Storage (SQLite can handle millions of records)
- Processing power (generates ~10 profiles/second on modest hardware)
Multi-Core Session Handover
Design Consideration: Profile format supports core mobility
Current design doesn't preclude future session handover between cores:
- IMSI/Ki/OPc are standardized, portable between cores
- Export format is core-agnostic JSON
- Core adapters can be extended to support handover signaling
Future Implementation:
- Add
home_core_idfield to subscriber record - Export includes core affinity metadata
- Handover protocol exchanges subscriber credentials between cores
Troubleshooting
Profile Generation Fails
- Check pySim installation:
python -c "import pySim" - Verify OP key is 16 bytes
- Check IMSI format (15 digits total)
Device Won't Download Profile
- Verify device supports eSIM (eUICC)
- Check LPA protocol implementation
- Verify profile format is SGP.22 compliant
- Try simplified profile format
Can't Connect to 5G After eSIM Install
- Verify subscriber added to Open5GS/free5gc
- Check IMSI/Ki/OPc match between profile and core
- Verify PLMN configuration matches gNB
- Check AMF/UDM logs for authentication failures
WiFi Captive Portal Not Triggering
- Verify DNS hijacking:
nslookup google.com 192.168.4.1 - Check nginx is running:
systemctl status nginx - Verify iptables rules for captive portal
File Structure
/opt/esim-provisioning/
├── server/
│ ├── provisioning_server.py # Main Flask application
│ ├── profile_generator.py # eSIM profile generation
│ ├── database.py # SQLite interface
│ ├── core_adapter.py # Core integration adapters
│ └── config.py # Configuration
├── portal/
│ ├── index.html # Captive portal page
│ ├── style.css
│ └── script.js
├── data/
│ ├── subscribers.db # SQLite database
│ └── profiles/ # Generated profiles (temp)
├── scripts/
│ ├── sync_open5gs.py # Open5GS sync script
│ ├── sync_free5gc.py # free5gc sync script
│ └── export_subscribers.py # Export to JSON
└── systemd/
├── esim-provisioning.service # Systemd unit file
└── captive-portal.service # Captive portal service
Next Steps
- Implement provisioning server (Python/Flask)
- Implement profile generator using pySim
- Create captive portal frontend
- Build Open5GS adapter
- Test end-to-end flow
- Deploy to UE WiFi AP drop device
References
- SGP.22 eSIM Specification: https://www.gsma.com/esim/
- pySim Documentation: https://github.com/osmocom/pysim
- Open5GS Documentation: https://open5gs.org/
- srsRAN Documentation: https://docs.srsran.com/