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

11 KiB

eSIM Provisioning System - Project Setup Instructions

Project Overview

Build a closed-loop eSIM provisioning system for emergency/NTN 5G deployments. System runs on a UE device (Android or Raspberry Pi) that connects to a 5G gNB and provides WiFi AP with automatic eSIM provisioning.

Repository Structure

Create the following directory structure:

esim-provisioning/
├── README.md
├── requirements.txt
├── config.yaml.example
├── docker-compose.yml (optional)
├── server/
│   ├── __init__.py
│   ├── app.py                      # Main Flask application
│   ├── profile_generator.py        # eSIM profile generation using pySim
│   ├── database.py                 # SQLite database interface
│   ├── core_adapters/
│   │   ├── __init__.py
│   │   ├── base.py                 # Base adapter class
│   │   ├── open5gs.py              # Open5GS adapter
│   │   └── free5gc.py              # free5gc adapter
│   ├── models.py                   # Database models
│   └── utils.py                    # Helper functions
├── portal/
│   ├── index.html                  # Captive portal landing page
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── script.js
│   └── assets/
│       └── (images if needed)
├── scripts/
│   ├── init_db.py                  # Initialize database
│   ├── sync_subscribers.py         # Sync to core network
│   ├── export_subscribers.py       # Export subscriber data
│   └── setup_wifi_ap.sh            # WiFi AP setup (for Pi)
├── systemd/
│   ├── esim-provisioning.service
│   └── captive-portal.service
├── configs/
│   ├── nginx.conf                  # nginx captive portal config
│   ├── dnsmasq.conf               # DNS/DHCP config
│   └── hostapd.conf               # WiFi AP config (for Pi)
├── android/
│   ├── README.md                   # Android setup instructions
│   └── termux_setup.sh            # Termux installation script
└── tests/
    ├── test_profile_generator.py
    ├── test_database.py
    └── test_core_adapters.py

Step 1: Initialize Project

Create the repository structure and initialize Python virtual environment:

# Create project directory
mkdir esim-provisioning
cd esim-provisioning

# Initialize git
git init

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Create all directories
mkdir -p server/core_adapters portal/css portal/js portal/assets scripts systemd configs android tests

Step 2: Create Requirements File

Create requirements.txt with:

Flask==3.0.0
pySim==1.0.0
cryptography==41.0.7
pymongo==4.6.0
requests==2.31.0
pyyaml==6.0.1

Step 3: Create Configuration Template

Create config.yaml.example:

network:
  mcc: "302"
  mnc: "720"
  plmn: "302720"
  op_key: "63bfa50ee6523365ff14c1f45f88737d"  # Hex string

wifi:
  ssid: "Emergency-5G"
  channel: 6
  ip: "192.168.4.1"
  subnet: "192.168.4.0/24"
  dhcp_range: "192.168.4.10,192.168.4.250"

server:
  host: "0.0.0.0"
  port: 5000
  debug: false

database:
  path: "/opt/esim-provisioning/data/subscribers.db"

rate_limiting:
  max_profiles_per_mac: 1
  window_hours: 1

core:
  type: "open5gs"  # or "free5gc"
  mongodb_uri: "mongodb://localhost:27017"
  database_name: "open5gs"

profile:
  carrier_name: "Ad-Hoc Network"
  profile_name: "Emergency 5G"
  apn: "internet"

Step 4: Core Server Components

database.py

Create database interface with:

  • SQLite connection management
  • Schema creation (subscribers table)
  • CRUD operations for subscribers
  • Rate limiting checks by MAC address
  • Export functionality

Schema:

CREATE TABLE subscribers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    imsi TEXT UNIQUE NOT NULL,
    iccid TEXT UNIQUE NOT NULL,
    ki TEXT NOT NULL,
    opc TEXT NOT NULL,
    mac_address TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    synced_to_core BOOLEAN DEFAULT 0
);

CREATE INDEX idx_imsi ON subscribers(imsi);
CREATE INDEX idx_mac_address ON subscribers(mac_address);

profile_generator.py

Implement eSIM profile generation:

  • Generate random IMSI (15 digits: MCC + MNC + MSIN)
  • Generate random Ki (16 bytes)
  • Derive OPc from Ki and OP using Milenage algorithm
  • Generate ICCID (19-20 digits)
  • Create SGP.22 eSIM profile using pySim
  • Return profile bytes and credentials

Key functions:

def generate_imsi(mcc, mnc) -> str
def generate_iccid() -> str
def generate_ki() -> bytes
def derive_opc(ki: bytes, op: bytes) -> bytes
def create_esim_profile(imsi, ki, opc, iccid, profile_name, carrier_name) -> bytes

core_adapters/base.py

Define abstract base class:

class CoreAdapter(ABC):
    @abstractmethod
    def add_subscriber(self, imsi, ki, opc):
        pass
    
    @abstractmethod
    def remove_subscriber(self, imsi):
        pass
    
    @abstractmethod
    def list_subscribers(self):
        pass

core_adapters/open5gs.py

Implement Open5GS adapter using MongoDB:

  • Connect to MongoDB
  • Insert subscriber with proper schema
  • Configure slice, APN, AMBR settings
  • Handle errors gracefully

app.py

Main Flask application with endpoints:

GET  /                    # Captive portal redirect
GET  /activate            # Serve captive portal HTML
POST /api/provision       # Generate and provision eSIM
GET  /api/profile/:id     # Download eSIM profile (LPA)
GET  /api/export          # Export subscribers JSON
GET  /api/status          # System status

Implement:

  • Rate limiting middleware (check MAC address)
  • Profile generation workflow
  • LPA protocol handling
  • Error handling and logging

Step 5: Captive Portal

portal/index.html

Simple single-page interface:

  • "Activate eSIM" button
  • Instructions for installation
  • Progress indicator
  • Success/error messages
  • Mobile-friendly responsive design

portal/js/script.js

Client-side logic:

  • POST to /api/provision with MAC address
  • Poll for profile generation status
  • Trigger eSIM download via LPA URL scheme
  • Handle errors

Step 6: System Scripts

scripts/init_db.py

Initialize SQLite database with schema

scripts/sync_subscribers.py

Sync subscribers to core:

# Read from SQLite
# For each unsynced subscriber:
#   - Call core adapter
#   - Mark as synced
#   - Log result

scripts/export_subscribers.py

Export to JSON format:

{
  "exported_at": "2025-02-11T10:30:00Z",
  "count": 123,
  "subscribers": [
    {
      "imsi": "302720123456789",
      "ki": "00112233445566778899aabbccddeeff",
      "opc": "63bfa50ee6523365ff14c1f45f88737d",
      "iccid": "8910302720123456789",
      "created_at": "2025-02-11T09:15:00Z"
    }
  ]
}

scripts/setup_wifi_ap.sh

Bash script for Raspberry Pi WiFi AP setup:

  • Install hostapd, dnsmasq, nginx
  • Configure network interfaces
  • Set up iptables for captive portal
  • Enable IP forwarding
  • Configure services

Step 7: Configuration Files

configs/nginx.conf

Configure captive portal:

  • Listen on port 80
  • Catch-all server block
  • Redirect to /activate
  • Proxy /api/* to Flask backend

configs/dnsmasq.conf

DNS/DHCP configuration:

  • Interface binding
  • DHCP range
  • DNS hijacking (return WiFi AP IP for all domains)
  • Lease time

configs/hostapd.conf

WiFi AP configuration:

  • SSID
  • Channel
  • Interface
  • Open network (no password)

Step 8: Systemd Services

systemd/esim-provisioning.service

Flask server service:

  • Run as dedicated user
  • Auto-restart on failure
  • Log to journal
  • Load config from /etc/esim-provisioning/config.yaml

systemd/captive-portal.service

Nginx + dnsmasq orchestration

Step 9: Testing

Create unit tests for:

  • Profile generation (valid IMSI, Ki, OPc formats)
  • Database operations (CRUD, rate limiting)
  • Core adapters (mock MongoDB)
  • API endpoints (Flask test client)

Step 10: Documentation

README.md

Include:

  • Project overview
  • System requirements
  • Installation instructions (Pi and Android)
  • Configuration guide
  • Usage instructions
  • Troubleshooting
  • Architecture diagram
  • License

Implementation Priority

  1. First: Database layer + models
  2. Second: Profile generator (without pySim, use mock for testing)
  3. Third: Flask API with all endpoints
  4. Fourth: Core adapters (Open5GS first)
  5. Fifth: Captive portal frontend
  6. Sixth: Integration with pySim for real profile generation
  7. Seventh: System scripts and configs
  8. Eighth: Testing and documentation

Key Implementation Notes

  • Use environment variables or config.yaml for all configuration (never hardcode)
  • Log everything (use Python logging module)
  • Handle all errors gracefully (no stack traces to user)
  • Validate all inputs (IMSI format, MAC address format)
  • Use secrets module for cryptographic randomness
  • Make core adapters truly modular (easy to add new cores)
  • Keep captive portal extremely simple (mobile-first)
  • Rate limiting must be robust (prevent abuse)
  • Database operations should be atomic
  • Profile generation should be fast (<1 second)

Testing Checklist

  • Can generate valid IMSI
  • Can generate valid Ki/OPc
  • Can create eSIM profile
  • Can store subscriber in database
  • Rate limiting works (same MAC can't request multiple times)
  • Can export subscribers to JSON
  • Open5GS adapter can add subscriber
  • API endpoints return correct responses
  • Captive portal loads on mobile
  • Profile downloads correctly
  • End-to-end: WiFi → provision → 5G connection

Deployment Targets

Raspberry Pi:

  • Raspberry Pi 4 (4GB+ RAM)
  • Quectel RM500Q 5G modem
  • Raspberry Pi OS Lite 64-bit
  • Python 3.11+

Android:

  • Android 10+ with root/Termux
  • 5G modem built-in
  • Termux + proot environment
  • Python 3.11+

Security Considerations (Future)

Currently implementing with no security for simplicity, but design should allow easy addition of:

  • WiFi password authentication
  • HTTPS for API endpoints
  • Activation code requirements
  • Subscriber audit logging
  • Profile encryption at rest

Success Criteria

System is complete when:

  1. User connects to WiFi
  2. Captive portal loads automatically
  3. Click "Activate eSIM" button
  4. eSIM profile downloads to device
  5. Device installs profile
  6. Device connects to 5G using new eSIM
  7. Open5GS authenticates successfully

Total user time: <60 seconds


Starting Point

Begin by creating the project structure, then implement in this order:

  1. database.py - Get data persistence working
  2. profile_generator.py - Mock implementation first
  3. app.py - Flask API with all endpoints
  4. portal/index.html - Simple UI
  5. core_adapters/open5gs.py - Core integration
  6. Integration testing
  7. Real pySim integration
  8. Deployment scripts

Focus on getting a working end-to-end flow before optimizing or adding features.