# Changelog All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - **Real SGP.22 SAIP profile generation** (`profile.format: saip`). `server/saip_profile.py` encodes a DER `ProfileElement` sequence — ProfileHeader (ICCID), PE-MF personalizing EF.ICCID (`2.23.143.1.2.1`), PE-USIM personalizing EF.IMSI (`2.23.143.1.2.4`), and Milenage AKA parameters (Ki/OPc) — with `asn1tools` against the official `PE_Definitions` ASN.1 schema. No pySim Python imports (the schema is located from an installed pySim, or via `SAIP_ASN_PATH`), so it avoids the pyscard/smpp card-reader stack; the only runtime dep is `asn1tools`. Self-contained and optional: `saip_available()` gates it and the app falls back to `profile-package-v2` when prerequisites are missing. `decode_saip_profile()` parses a package back into its ProfileElements. Optional deps in `requirements-saip.txt`. A fully installable profile (remaining mandatory PEs + card validation) is the documented next layer. - `scripts/make_profile.py`: dev/ops CLI to generate a profile (v2 or saip) to a file/stdout from `config.yaml`, or `--inspect` a SAIP profile file (decode + summarize its ProfileElements). - `pytest.ini` to silence asn1tools/pyparsing deprecation warnings. ### Security - `/api/export` (dumps every subscriber's Ki/OPc) now requires an admin token (`server.admin_token`, sent as `X-Admin-Token`, constant-time compared) and is disabled by default. Previously any WiFi client could pull all credentials unauthenticated. - Profile download URLs now use an unguessable capability token (`secrets.token_urlsafe`) instead of the sequential subscriber id. Profiles contain Ki/OPc, so the old `/api/profile/` scheme let anyone on the WiFi enumerate and harvest every subscriber's keys. Unknown tokens return a plain 404 (indistinguishable from evicted) so tokens cannot be probed. ### Hardened - Core adapters set short MongoDB timeouts (`serverSelectionTimeoutMS` etc.) so an unreachable core fails provisioning fast instead of hanging ~30s. - In-memory profile cache is now a size-capped LRU (`_MAX_CACHED_PROFILES`) and can no longer grow without bound. - `load_config` rejects an empty/non-mapping YAML file and validates `network.op_key` is a 16-byte hex string, with clear error messages. - Fixed `FileNotFoundError` when `database.path` is a bare filename with no directory component: `app.py` and `scripts/init_db.py` now use a shared `ensure_parent_dir` helper that no-ops on an empty parent. - Open5GS/free5gc adapters now use the configured `profile.apn` for the core session/DNN name instead of a hardcoded `"internet"`. - Connections set `PRAGMA busy_timeout` (5s) so concurrent writers under a threaded server wait for the lock instead of raising "database is locked". - Provisioning retries on the (astronomically rare) IMSI/ICCID collision instead of returning a 500. - The in-memory profile cache is guarded by a lock (thread-safe under a threaded server), and the captive portal disables the activate button while a request is in flight to prevent double provisioning. - `setup_wifi_ap.sh`: the deployed `config.yaml` (holds the OP key) is now `chmod 600` and owned by the service user instead of world-readable, and `data/` is chowned *after* `init_db` so the root-created SQLite file is writable by the `esim` service (previously it was root-owned and the service could not write to it). - Added `test_app.py` (API-level: provision, rate-limit, token download, 404 on unknown/enumerated ids, collision retry/give-up), `test_utils.py` (MAC/IMSI/op_key/config/ensure_parent_dir), and adapter APN coverage. ### Added - **Phase-2 profile generation.** `create_esim_profile` now emits a structured profile package with real 3GPP encodings instead of a loose JSON mock: - `enc_iccid` — EF_ICCID encoding (nibble-swapped BCD, 10-byte, 0xF padded) per 3GPP TS 31.102 §4.2.1. - `enc_imsi` — EF_IMSI encoding (length octet + parity nibble + nibble-swapped BCD) per 3GPP TS 31.102 §4.2.2. - `build_lpa_activation_code` — SGP.22 LPA activation string (`LPA:1$$`) so the profile carries a device-consumable activation reference. - `CHANGELOG.md`. ### Changed - Profile package `type` is now `profile-package-v2`; it embeds the raw credentials plus the encoded EF byte values and the LPA activation code. ### Fixed - `generate_imsi` hardcoded a 10-digit MSIN, producing invalid 16-digit IMSIs for a 3-digit MNC (e.g. MCC 302 / MNC 720). 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`), making the rate-limit window unreliable. Both sides are now normalized via SQLite `datetime()`. ### Notes - Full SGP.22 SIMalliance Interoperable Profile (SAIP / ASN.1 UPP) generation via osmocom pySim remains the follow-up step; it needs an SM-DP+ / LPA to be installed onto consumer devices over the air. The current package targets programmable SIMs and core-network sync. ## [0.1.0] — 2026-07-01 ### Added - Initial Phase-1 closed-loop eSIM provisioning stack: Flask API, SQLite subscriber store, credential generator (IMSI/Ki/OPc/ICCID), Open5GS and free5gc core adapters, captive portal, operational scripts, systemd units, network configs, and a test suite.