Push Tracker
ria-ran--sim-drop/plan/design.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

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

  1. User device detects open WiFi network
  2. Connects to WiFi → Auto-redirected to captive portal
  3. Captive portal displays "Activate eSIM" button
  4. Click → Server generates unique IMSI/Ki/OPc credentials
  5. Server generates SGP.22 eSIM profile with credentials
  6. Profile delivered to device via LPA protocol
  7. Device installs eSIM profile
  8. User disconnects from WiFi
  9. Device connects to 5G network using new eSIM
  10. 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 generation
  • Flask - Web server
  • sqlite3 - Local credential storage
  • cryptography - 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:

  1. Push: Provisioning server calls core adapter immediately
  2. Pull: Core polls provisioning server API periodically
  3. 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:

  1. Install Termux
  2. Install required packages (Python, hostapd, dnsmasq, nginx)
  3. Deploy provisioning server
  4. Configure WiFi hotspot
  5. Start services

If Raspberry Pi:

  1. Install Raspberry Pi OS Lite
  2. Install Quectel RM500Q modem drivers
  3. Configure modem for gNB connection
  4. Install provisioning server stack
  5. Configure hostapd + dnsmasq
  6. 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_id field 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

  1. Implement provisioning server (Python/Flask)
  2. Implement profile generator using pySim
  3. Create captive portal frontend
  4. Build Open5GS adapter
  5. Test end-to-end flow
  6. Deploy to UE WiFi AP drop device

References