diff --git a/README.md b/README.md index 47c804e..c483c78 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,186 @@ # ria-ran--sim-drop +**Closed-loop eSIM provisioning for emergency / NTN 5G deployments.** + +A self-contained appliance (Raspberry Pi or Android) that connects to a 5G gNB +for backhaul, broadcasts an open WiFi AP, and hands out eSIM credentials to +connecting devices through a captive portal. Users go from "no signal" to +"connected to the ad-hoc 5G network" in under a minute, with no operator in the +loop. + +> Deep design rationale lives in [`plan/design.md`](plan/design.md). This README +> is the operational guide. Notable changes are tracked in +> [`CHANGELOG.md`](CHANGELOG.md). + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ Server Host (srsRAN gNB) │ +│ - srsRAN gNB │ +│ - Open5GS Core (or free5gc) │ +│ - Subscriber database │ +└─────────────┬───────────────────────────┘ + │ 5G NR + ▼ +┌─────────────────────────────────────────┐ +│ UE WiFi AP Drop │ +│ (Android device or Raspberry Pi) │ +│ - WiFi AP (open network) │ +│ - eSIM Provisioning Server (Flask) │ +│ - Captive Portal (nginx + dnsmasq) │ +└─────────────┬───────────────────────────┘ + │ WiFi + ▼ + End User Devices +``` + +## User flow + +1. User connects to the open WiFi network `Emergency-5G`. +2. The captive portal auto-redirects to the activation page. +3. User taps **Activate eSIM**. +4. The server generates a unique IMSI / Ki / OPc / ICCID. +5. An eSIM profile is generated and offered as a one-time download. +6. User installs the profile via device Settings. +7. The device connects to the 5G network automatically. + +## Quick start (local dev) + +```bash +git clone https://riahub.ai/qoherent/ria-ran--sim-drop.git +cd ria-ran--sim-drop +python3 -m venv venv && source venv/bin/activate +pip install -r requirements.txt +cp config.yaml.example config.yaml # edit MCC/MNC/op_key as needed +python scripts/init_db.py +python -m server.app # → http://localhost:5000 +``` + +Open and click **Activate eSIM**. + +## Configuration + +All configuration is in `config.yaml` (start from `config.yaml.example`). + +| Field | Description | Example | +|-------|-------------|---------| +| `network.mcc` | Mobile Country Code (3 digits) | `"302"` | +| `network.mnc` | Mobile Network Code (2–3 digits) | `"720"` | +| `network.op_key` | Operator key, 16-byte hex (32 chars) | `"63bfa50e…"` | +| `wifi.ip` | WiFi AP IP address | `"192.168.4.1"` | +| `server.host` / `server.port` | Flask bind address | `"0.0.0.0"` / `5000` | +| `server.admin_token` | Token guarding `/api/export`; empty = export disabled | `""` | +| `database.path` | SQLite database path | `"/opt/…/subscribers.db"` | +| `rate_limiting.window_hours` | One profile per MAC per window | `1` | +| `profile.apn` | APN / DNN used in the profile and core session | `"internet"` | +| `profile.smdp_address` | SM-DP+ address embedded in the LPA activation code | `"esim.local"` | +| `core.type` | Core adapter: `open5gs` / `free5gc` / `none` | `"open5gs"` | +| `core.mongodb_uri` | MongoDB connection for the core | `"mongodb://localhost:27017"` | + +`op_key` is validated at startup (must be 16-byte hex) and the config file is +rejected if it is empty or missing a required section. + +## API reference + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/activate` | GET | Captive portal page | +| `/api/provision` | POST | Generate credentials + profile; returns a one-time `profile_url` | +| `/api/profile/` | GET | Download the profile via its unguessable capability token | +| `/api/export` | GET | Export all subscribers (JSON). Requires `X-Admin-Token`; disabled unless `server.admin_token` is set | +| `/api/status` | GET | Health check (subscriber count, uptime, core adapter) | + +`POST /api/provision` accepts `{"mac_address": "aa:bb:cc:dd:ee:ff"}`; if omitted, +the server resolves the client MAC from the ARP table. Rate limiting is per MAC. + +## Profile format + +`create_esim_profile` produces a `profile-package-v2` bundle containing the raw +credentials plus **real 3GPP field encodings** — `EF_ICCID` and `EF_IMSI` +(nibble-swapped BCD, 3GPP TS 31.102) — and an SGP.22 **LPA activation code** +(`LPA:1$$`). + +Full SGP.22 SIMalliance Interoperable Profile (ASN.1 UPP) generation via osmocom +pySim is the planned follow-up; consumer over-the-air install additionally +requires an SM-DP+ / LPA. The current package targets programmable SIMs and +core-network sync. See [`CHANGELOG.md`](CHANGELOG.md). + +## Deployment + +### Raspberry Pi + +Requirements: Pi 4 (4GB+), a compatible 5G modem (e.g. Quectel RM500Q), Raspberry +Pi OS Lite 64-bit, Python 3.11+, and a pre-provisioned SIM for gNB backhaul. + +```bash +sudo bash scripts/setup_wifi_ap.sh +``` + +This installs hostapd/dnsmasq/nginx, configures the AP and captive-portal +redirect, deploys the server under `/opt/esim-provisioning`, creates the `esim` +service user, restricts `config.yaml` to `600`, initialises the DB, and enables +the systemd services. Then edit `/etc/esim-provisioning/config.yaml` and +`systemctl restart esim-provisioning`. + +### Android / Termux + +```bash +bash android/termux_setup.sh +~/esim-provisioning/start.sh +``` + +See [`android/README.md`](android/README.md) for details and limitations. + +## Core network sync + +With `core.type: none`, credentials are stored locally only. To push them to a +core network: + +```bash +python scripts/sync_subscribers.py # sync unsynced subscribers to Open5GS/free5gc +python scripts/export_subscribers.py # dump all subscribers as JSON (reads the DB directly) +``` + +Adding a new core is a single class in `server/core_adapters/` implementing the +`CoreAdapter` interface. + +## Security notes + +- Profiles are served at an unguessable one-time token URL, never an enumerable + id — the profile body contains Ki/OPc. +- `/api/export` returns all key material and is **disabled by default**; set + `server.admin_token` and send it as `X-Admin-Token` to enable. +- The deployed `config.yaml` (holds `op_key`) is `chmod 600`, owned by the + service user; the systemd unit runs with `NoNewPrivileges`, `PrivateTmp`, and + `ProtectSystem=strict`. +- The WiFi network is intentionally open (emergency access); there is no + transport encryption on the local link. + +## Testing + +```bash +pip install -r requirements.txt +python -m pytest tests/ -q +``` + +The suite covers credential generation and 3GPP encodings, the database and rate +limiting, the core adapters, config/utility validation, and the Flask API +(provisioning, token download, rate-limit and export auth). + +## Project layout + +``` +server/ Flask app, profile generator, database, core adapters +portal/ Captive-portal frontend (HTML/CSS/JS) +scripts/ init_db, sync/export subscribers, Pi AP setup +configs/ nginx / dnsmasq / hostapd reference configs +systemd/ Service units +android/ Termux deployment +tests/ Test suite +plan/ Design doc and per-unit implementation notes +``` + +## License + +MIT (or as appropriate for the deployment context).