diff --git a/CHANGELOG.md b/CHANGELOG.md index ea0a7d8..7dcef62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Real SGP.22 SAIP profile generation** (`profile.format: saip`). `server/saip_profile.py` encodes a DER `ProfileElement` sequence — ProfileHeader - (ICCID), PE-USIM personalizing EF.IMSI against the standard USIM template - (`2.23.143.1.2.4`), and Milenage AKA parameters (Ki/OPc) — with `asn1tools` + (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`. diff --git a/README.md b/README.md index 439441c..eacdc5e 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,9 @@ Two formats are selectable via `profile.format`: nibble-swapped BCD per 3GPP TS 31.102) and an SGP.22 **LPA activation code** (`LPA:1$$`). - **`saip`** — a real DER-encoded **SGP.22 SAIP `ProfileElement` sequence**: - ProfileHeader (ICCID), PE-USIM personalizing EF.IMSI against the standard USIM - template, and Milenage AKA parameters (Ki/OPc). Built with `asn1tools` against + ProfileHeader (ICCID), PE-MF personalizing EF.ICCID, PE-USIM personalizing + EF.IMSI (both against the standard "created by default" templates), and Milenage + AKA parameters (Ki/OPc). Built with `asn1tools` against the official `PE_Definitions` ASN.1 schema. Enable by installing `requirements-saip.txt` (see that file for the schema source) and setting `profile.format: saip`. If prerequisites are missing the server logs a warning diff --git a/server/saip_profile.py b/server/saip_profile.py index 90b9202..8c3aa87 100644 --- a/server/saip_profile.py +++ b/server/saip_profile.py @@ -26,9 +26,11 @@ logger = logging.getLogger(__name__) _ASN_RELPATH = os.path.join("esim", "asn1", "saip", "PE_Definitions-3.3.1.asn") -# Standard "created by default" USIM ADF template (TCA eUICC prefix 2.23.143.1 -# + templates.2 + usim.4). The eUICC creates ADF.USIM and its default EFs from -# this template; the profile only personalizes the EFs it fills (here EF.IMSI). +# Standard "created by default" templates (TCA eUICC prefix 2.23.143.1 + +# templates.2 + N). The eUICC creates the file structure from the template; the +# profile only personalizes the EFs it fills (here EF.ICCID at MF, EF.IMSI at +# ADF.USIM). +MF_TEMPLATE_OID = "2.23.143.1.2.1" USIM_TEMPLATE_OID = "2.23.143.1.2.4" @@ -81,8 +83,9 @@ def create_saip_profile( ) -> bytes: """Build a DER-encoded SAIP ProfileElement sequence. - Emits: ProfileHeader (with ICCID), PE-USIM (personalizing EF.IMSI against the - standard USIM template), Milenage PE-AKAParameter (Ki/OPc), and PE-End. + Emits: ProfileHeader (ICCID), PE-MF (personalizing EF.ICCID against the MF + template), PE-USIM (personalizing EF.IMSI against the USIM template), Milenage + PE-AKAParameter (Ki/OPc), and PE-End. """ asn = _compiler() if asn is None: @@ -105,8 +108,16 @@ def create_saip_profile( "eUICC-Mandatory-services": {"usim": None, "milenage": None}, "eUICC-Mandatory-GFSTEList": [], }) + mf = ("mf", { + "mf-header": {"identification": 1}, + "templateID": MF_TEMPLATE_OID, + "mf": [], + "ef-iccid": [("fillFileContent", enc_iccid(iccid))], + # Other mandatory files are left to the template (empty = no override). + "ef-dir": [], "ef-arr": [], + }) usim = ("usim", { - "usim-header": {"identification": 1}, + "usim-header": {"identification": 2}, "templateID": USIM_TEMPLATE_OID, "adf-usim": [], "ef-imsi": [("fillFileContent", enc_imsi(imsi))], @@ -115,7 +126,7 @@ def create_saip_profile( "ef-acc": [], "ef-ecc": [], }) aka = ("akaParameter", { - "aka-header": {"identification": 2}, + "aka-header": {"identification": 3}, "algoConfiguration": ("algoParameter", { "algorithmID": 1, # Milenage "algorithmOptions": b"\x00", @@ -123,10 +134,11 @@ def create_saip_profile( "opc": opc_bytes, }), }) - end = ("end", {"end-header": {"identification": 3}}) + end = ("end", {"end-header": {"identification": 4}}) logger.debug(f"Encoding SAIP profile for IMSI {imsi} (ICCID {iccid})") - return b"".join(asn.encode("ProfileElement", pe) for pe in (header, usim, aka, end)) + pes = (header, mf, usim, aka, end) + return b"".join(asn.encode("ProfileElement", pe) for pe in pes) def _split_der(data: bytes): diff --git a/tests/test_saip.py b/tests/test_saip.py index d8228d7..cca17e6 100644 --- a/tests/test_saip.py +++ b/tests/test_saip.py @@ -58,10 +58,11 @@ class TestSaipEncoding: iccid = generate_iccid() pkg = saip.create_saip_profile("302720000000001", ki, opc, iccid, "P", "C") pes = saip.decode_saip_profile(pkg) - assert [choice for choice, _ in pes] == ["header", "usim", "akaParameter", "end"] - header = dict(pes)["header"] - assert header["iccid"] == enc_iccid(iccid) - assert header["major-version"] == 3 + assert [choice for choice, _ in pes] == ["header", "mf", "usim", "akaParameter", "end"] + by = dict(pes) + assert by["header"]["iccid"] == enc_iccid(iccid) + assert by["header"]["major-version"] == 3 + assert by["mf"]["ef-iccid"] == [("fillFileContent", enc_iccid(iccid))] @saip_required def test_imsi_embedded_in_usim(self):