SAIP: add PE-MF personalizing EF.ICCID

Prepend a PE-MF (referencing the MF "created by default" template,
OID 2.23.143.1.2.1) that personalizes EF.ICCID, so the profile sequence is now
header -> MF(EF.ICCID) -> USIM(EF.IMSI) -> AKA(Ki/OPc) -> End. 93 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
A ashkan@beigi.net 2026-07-02 02:46:33 +00:00
parent 470498bf07
commit a5fc9a3e91
4 changed files with 31 additions and 17 deletions

View File

@ -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`.

View File

@ -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$<smdp>$<matching-id>`).
- **`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

View File

@ -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):

View File

@ -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):