mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-16 18:38:32 +03:00
pySim.saip.*: Support for parsing / operating on eSIM profiles
This commit introduces the capability to parse and encode SimAlliance/TCA "Interoperable Profiles" and apply personalization operations on them. Change-Id: I71c252a214a634e1bd6f73472107efe2688ee6d2
This commit is contained in:
1126
pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
Normal file
1126
pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
Normal file
File diff suppressed because it is too large
Load Diff
229
pySim/esim/saip/__init__.py
Normal file
229
pySim/esim/saip/__init__.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile handling
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
from typing import Tuple, List, Optional, Dict
|
||||
|
||||
import asn1tools
|
||||
|
||||
from pySim.utils import bertlv_parse_tag, bertlv_parse_len
|
||||
from pySim.esim import compile_asn1_subdir
|
||||
|
||||
asn1 = compile_asn1_subdir('saip')
|
||||
|
||||
class oid:
|
||||
class OID:
|
||||
@staticmethod
|
||||
def intlist_from_str(instr: str) -> List[int]:
|
||||
return [int(x) for x in instr.split('.')]
|
||||
|
||||
def __init__(self, initializer):
|
||||
if type(initializer) == str:
|
||||
self.intlist = self.intlist_from_str(initializer)
|
||||
else:
|
||||
self.intlist = initializer
|
||||
|
||||
def __str__(self):
|
||||
return '.'.join([str(x) for x in self.intlist])
|
||||
|
||||
def __repr__(self):
|
||||
return 'OID(%s)' % (str(self))
|
||||
|
||||
|
||||
class eOID(OID):
|
||||
"""OID helper for TCA eUICC prefix"""
|
||||
__prefix = [2,23,143,1]
|
||||
def __init__(self, initializer):
|
||||
if type(initializer) == str:
|
||||
initializer = self.intlist_from_str(initializer)
|
||||
super().__init__(self.__prefix + initializer)
|
||||
|
||||
MF = eOID("2.1")
|
||||
DF_CD = eOID("2.2")
|
||||
DF_TELECOM = eOID("2.3")
|
||||
DF_TELECOM_v2 = eOID("2.3.2")
|
||||
ADF_USIM_by_default = eOID("2.4")
|
||||
ADF_USIM_by_default_v2 = eOID("2.4.2")
|
||||
ADF_USIM_not_by_default = eOID("2.5")
|
||||
ADF_USIM_not_by_default_v2 = eOID("2.5.2")
|
||||
ADF_USIM_not_by_default_v3 = eOID("2.5.3")
|
||||
DF_PHONEBOOK_ADF_USIM = eOID("2.6")
|
||||
DF_GSM_ACCESS_ADF_USIM = eOID("2.7")
|
||||
ADF_ISIM_by_default = eOID("2.8")
|
||||
ADF_ISIM_not_by_default = eOID("2.9")
|
||||
ADF_ISIM_not_by_default_v2 = eOID("2.9.2")
|
||||
ADF_CSIM_by_default = eOID("2.10")
|
||||
ADF_CSIM_by_default_v2 = eOID("2.10.2")
|
||||
ADF_CSIM_not_by_default = eOID("2.11")
|
||||
ADF_CSIM_not_by_default_v2 = eOID("2.11.2")
|
||||
DF_EAP = eOID("2.12")
|
||||
DF_5GS = eOID("2.13")
|
||||
DF_5GS_v2 = eOID("2.13.2")
|
||||
DF_5GS_v3 = eOID("2.13.3")
|
||||
DF_5GS_v4 = eOID("2.13.4")
|
||||
DF_SAIP = eOID("2.14")
|
||||
DF_SNPN = eOID("2.15")
|
||||
DF_5GProSe = eOID("2.16")
|
||||
IoT_default = eOID("2.17")
|
||||
IoT_default = eOID("2.18")
|
||||
|
||||
|
||||
class ProfileElement:
|
||||
def _fixup_sqnInit_dec(self):
|
||||
"""asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
|
||||
this."""
|
||||
if self.type != 'akaParameter':
|
||||
return
|
||||
sqn_init = self.decoded.get('sqnInit', None)
|
||||
if not sqn_init:
|
||||
return
|
||||
# this weird '0x' value in a string is what we get from our (slightly hacked) ASN.1 syntax
|
||||
if sqn_init == '0x000000000000':
|
||||
# SEQUENCE (SIZE (32)) OF OCTET STRING (SIZE (6))
|
||||
self.decoded['sqnInit'] = [b'\x00'*6] * 32
|
||||
|
||||
def _fixup_sqnInit_enc(self):
|
||||
"""asn1tools has a bug when working with SEQUENCE OF that have DEFAULT values. Let's work around
|
||||
this."""
|
||||
if self.type != 'akaParameter':
|
||||
return
|
||||
sqn_init = self.decoded.get('sqnInit', None)
|
||||
if not sqn_init:
|
||||
return
|
||||
for s in sqn_init:
|
||||
if any(s):
|
||||
return
|
||||
# none of the fields were initialized with a non-default (non-zero) value, so we can skip it
|
||||
del self.decoded['sqnInit']
|
||||
|
||||
def parse_der(self, der: bytes):
|
||||
"""Parse a sequence of PE and store the result in instance attributes."""
|
||||
self.type, self.decoded = asn1.decode('ProfileElement', der)
|
||||
# work around asn1tools bug regarding DEFAULT for a SEQUENCE OF
|
||||
self._fixup_sqnInit_dec()
|
||||
|
||||
@classmethod
|
||||
def from_der(cls, der: bytes) -> 'ProfileElement':
|
||||
"""Construct an instance from given raw, DER encoded bytes."""
|
||||
inst = cls()
|
||||
inst.parse_der(der)
|
||||
return inst
|
||||
|
||||
def to_der(self) -> bytes:
|
||||
"""Build an encoded DER representation of the instance."""
|
||||
# work around asn1tools bug regarding DEFAULT for a SEQUENCE OF
|
||||
self._fixup_sqnInit_enc()
|
||||
return asn1.encode('ProfileElement', (self.type, self.decoded))
|
||||
|
||||
def __str__(self):
|
||||
return self.type
|
||||
|
||||
|
||||
def bertlv_first_segment(binary: bytes) -> Tuple[bytes, bytes]:
|
||||
"""obtain the first segment of a binary concatenation of BER-TLV objects.
|
||||
Returns: tuple of first TLV and remainder."""
|
||||
tagdict, remainder = bertlv_parse_tag(binary)
|
||||
length, remainder = bertlv_parse_len(remainder)
|
||||
tl_length = len(binary) - len(remainder)
|
||||
tlv_length = tl_length + length
|
||||
return binary[:tlv_length], binary[tlv_length:]
|
||||
|
||||
class ProfileElementSequence:
|
||||
"""A sequence of ProfileElement objects, which is the overall representation of an eSIM profile."""
|
||||
def __init__(self):
|
||||
self.pe_list: List[ProfileElement] = None
|
||||
self.pe_by_type: Dict = {}
|
||||
self.pes_by_naa: Dict = {}
|
||||
|
||||
def get_pes_for_type(self, tname: str) -> List[ProfileElement]:
|
||||
return self.pe_by_type.get(tname, [])
|
||||
|
||||
def get_pe_for_type(self, tname: str) -> Optional[ProfileElement]:
|
||||
l = self.get_pes_for_type(tname)
|
||||
if len(l) == 0:
|
||||
return None
|
||||
assert len(l) == 1
|
||||
return l[0]
|
||||
|
||||
def parse_der(self, der: bytes):
|
||||
"""Parse a sequence of PE and store the result in self.pe_list."""
|
||||
self.pe_list = []
|
||||
remainder = der
|
||||
while len(remainder):
|
||||
first_tlv, remainder = bertlv_first_segment(remainder)
|
||||
self.pe_list.append(ProfileElement.from_der(first_tlv))
|
||||
self._process_pelist()
|
||||
|
||||
def _process_pelist(self):
|
||||
self._rebuild_pe_by_type()
|
||||
self._rebuild_pes_by_naa()
|
||||
|
||||
def _rebuild_pe_by_type(self):
|
||||
self.pe_by_type = {}
|
||||
# build a dict {pe_type: [pe, pe, pe]}
|
||||
for pe in self.pe_list:
|
||||
if pe.type in self.pe_by_type:
|
||||
self.pe_by_type[pe.type].append(pe)
|
||||
else:
|
||||
self.pe_by_type[pe.type] = [pe]
|
||||
|
||||
def _rebuild_pes_by_naa(self):
|
||||
"""rebuild the self.pes_by_naa dict {naa: [ [pe, pe, pe], [pe, pe] ]} form,
|
||||
which basically means for every NAA there's a lsit of instances, and each consists
|
||||
of a list of a list of PEs."""
|
||||
self.pres_by_naa = {}
|
||||
petype_not_naa_related = ['securityDomain', 'rfm', 'application', 'end']
|
||||
naa = ['mf', 'usim', 'isim', 'csim']
|
||||
cur_naa = None
|
||||
cur_naa_list = []
|
||||
for pe in self.pe_list:
|
||||
# skip all PE that are not related to NAA
|
||||
if pe.type in petype_not_naa_related:
|
||||
continue
|
||||
if pe.type in naa:
|
||||
if cur_naa:
|
||||
if not cur_naa in self.pes_by_naa:
|
||||
self.pes_by_naa[cur_naa] = []
|
||||
self.pes_by_naa[cur_naa].append(cur_naa_list)
|
||||
cur_naa = pe.type
|
||||
cur_naa_list = []
|
||||
cur_naa_list.append(pe)
|
||||
# append the final one
|
||||
if cur_naa and len(cur_naa_list):
|
||||
if not cur_naa in self.pes_by_naa:
|
||||
self.pes_by_naa[cur_naa] = []
|
||||
self.pes_by_naa[cur_naa].append(cur_naa_list)
|
||||
|
||||
@classmethod
|
||||
def from_der(cls, der: bytes) -> 'ProfileElementSequence':
|
||||
"""Construct an instance from given raw, DER encoded bytes."""
|
||||
inst = cls()
|
||||
inst.parse_der(der)
|
||||
return inst
|
||||
|
||||
def to_der(self) -> bytes:
|
||||
"""Build an encoded DER representation of the instance."""
|
||||
out = b''
|
||||
for pe in self.pe_list:
|
||||
out += pe.to_der()
|
||||
return out
|
||||
|
||||
def __repr__(self):
|
||||
return "PESequence(%s)" % ', '.join([str(x) for x in self.pe_list])
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.pe_list
|
||||
150
pySim/esim/saip/personalization.py
Normal file
150
pySim/esim/saip/personalization.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile handling
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import abc
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from pySim.esim.saip import ProfileElement, ProfileElementSequence
|
||||
|
||||
def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_key:str) -> List[Tuple]:
|
||||
"""In a list of tuples, remove all tuples whose first part equals 'unwanted_key'."""
|
||||
return list(filter(lambda x: x[0] != unwanted_key, l))
|
||||
|
||||
def file_replace_content(file: List[Tuple], new_content: bytes):
|
||||
"""Completely replace all fillFileContent of a decoded 'File' with the new_content."""
|
||||
file = remove_unwanted_tuples_from_list(file, 'fillFileContent')
|
||||
file.append(('fillFileContent', new_content))
|
||||
return file
|
||||
|
||||
class ClassVarMeta(abc.ABCMeta):
|
||||
"""Metaclass that puts all additional keyword-args into the class."""
|
||||
def __new__(metacls, name, bases, namespace, **kwargs):
|
||||
#print("Meta_new_(metacls=%s, name=%s, bases=%s, namespace=%s, kwargs=%s)" % (metacls, name, bases, namespace, kwargs))
|
||||
x = super().__new__(metacls, name, bases, namespace)
|
||||
for k, v in kwargs.items():
|
||||
setattr(x, k, v)
|
||||
return x
|
||||
|
||||
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
||||
"""Base class representing a part of the eSIM profile that is configurable during the
|
||||
personalization process (with dynamic data from elsewhere)."""
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply(self, pe_seq: ProfileElementSequence):
|
||||
pass
|
||||
|
||||
class Iccid(ConfigurableParameter):
|
||||
"""Configurable ICCID. Expects the value to be in EF.ICCID format."""
|
||||
name = 'iccid'
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
# patch the header; FIXME: swap nibbles!
|
||||
pes.get_pe_by_type('header').decoded['iccid'] = self.value
|
||||
# patch MF/EF.ICCID
|
||||
file_replace_content(pes.get_pe_by_type('mf').decoded['ef-iccid'], self.value)
|
||||
|
||||
class Imsi(ConfigurableParameter):
|
||||
"""Configurable IMSI. Expects value to be n EF.IMSI format."""
|
||||
name = 'imsi'
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
# patch ADF.USIM/EF.IMSI
|
||||
for pe in pes.get_pes_by_type('usim'):
|
||||
file_replace_content(pe.decoded['ef-imsi'], self.value)
|
||||
# TODO: DF.GSM_ACCESS if not linked?
|
||||
|
||||
def obtain_singleton_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
|
||||
filtered = list(filter(lambda x: x.type == wanted_type, l))
|
||||
assert len(filtered) == 1
|
||||
return filtered[0]
|
||||
|
||||
def obtain_first_pe_from_pelist(l: List[ProfileElement], wanted_type: str) -> ProfileElement:
|
||||
filtered = list(filter(lambda x: x.type == wanted_type, l))
|
||||
return filtered[0]
|
||||
|
||||
class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
keyReference = None
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
mf_pes = pes.pes_by_naa['mf'][0]
|
||||
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
|
||||
for pukCode in pukCodes.decoded['pukCodes']:
|
||||
if pukCode['keyReference'] == self.keyReference:
|
||||
pukCode['pukValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pukCode')
|
||||
class Puk1(Puk, keyReference=0x01):
|
||||
pass
|
||||
class Puk2(Puk, keyReference=0x81):
|
||||
pass
|
||||
|
||||
class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
keyReference = None
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
mf_pes = pes.pes_by_naa['mf'][0]
|
||||
pinCodes = obtain_first_pe_from_pelist(mf_pes, 'pinCodes')
|
||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||
return
|
||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||
if pinCode['keyReference'] == self.keyReference:
|
||||
pinCode['pinValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pinCode')
|
||||
class AppPin(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
keyReference = None
|
||||
def _apply_one(self, pe: ProfileElement):
|
||||
pinCodes = obtain_first_pe_from_pelist(pe, 'pinCodes')
|
||||
if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
|
||||
return
|
||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||
if pinCode['keyReference'] == self.keyReference:
|
||||
pinCode['pinValue'] = self.value
|
||||
return
|
||||
raise ValueError('cannot find pinCode')
|
||||
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
for naa in pes.pes_by_naa:
|
||||
if naa not in ['usim','isim','csim','telecom']:
|
||||
continue
|
||||
for instance in pes.pes_by_naa[naa]:
|
||||
self._apply_one(instance)
|
||||
class Pin1(Pin, keyReference=0x01):
|
||||
pass
|
||||
# PIN2 is special: telecom + usim + isim + csim
|
||||
class Pin2(AppPin, keyReference=0x81):
|
||||
pass
|
||||
class Adm1(Pin, keyReference=0x0A):
|
||||
pass
|
||||
class Adm2(Pin, keyReference=0x0B):
|
||||
pass
|
||||
|
||||
|
||||
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
key = None
|
||||
def apply(self, pes: ProfileElementSequence):
|
||||
for pe in pes.get_pes_for_type('akaParameter'):
|
||||
algoConfiguration = pe.decoded['algoConfiguration']
|
||||
if algoConfiguration[0] != 'algoParameter':
|
||||
continue
|
||||
algoConfiguration[1][self.key] = self.value
|
||||
|
||||
class K(AlgoConfig, key='key'):
|
||||
pass
|
||||
class Opc(AlgoConfig, key='opc'):
|
||||
pass
|
||||
class AlgorithmID(AlgoConfig, key='algorithmID'):
|
||||
pass
|
||||
|
||||
96
pySim/esim/saip/validation.py
Normal file
96
pySim/esim/saip/validation.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile handling
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from pySim.esim.saip import *
|
||||
|
||||
class ProfileError(Exception):
|
||||
pass
|
||||
|
||||
class ProfileConstraintChecker:
|
||||
def check(self, pes: ProfileElementSequence):
|
||||
for name in dir(self):
|
||||
if name.startswith('check_'):
|
||||
method = getattr(self, name)
|
||||
method(pes)
|
||||
|
||||
class CheckBasicStructure(ProfileConstraintChecker):
|
||||
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
|
||||
opt_pe = pes.get_pe_for_type(opt)
|
||||
if opt_pe:
|
||||
after_pe = pes.get_pe_for_type(after)
|
||||
if not after_pe:
|
||||
raise ProfileError('PE-%s without PE-%s' % (opt.upper(), after.upper()))
|
||||
# FIXME: check order
|
||||
|
||||
def check_start_and_end(self, pes: ProfileElementSequence):
|
||||
if pes.pe_list[0].type != 'header':
|
||||
raise ProfileError('first element is not header')
|
||||
if pes.pe_list[1].type != 'mf':
|
||||
# strictly speaking: permitted, but we don't support MF via GenericFileManagement
|
||||
raise ProfileError('second element is not mf')
|
||||
if pes.pe_list[-1].type != 'end':
|
||||
raise ProfileError('last element is not end')
|
||||
|
||||
def check_number_of_occurrence(self, pes: ProfileElementSequence):
|
||||
# check for invalid number of occurrences
|
||||
if len(pes.get_pes_for_type('header')) != 1:
|
||||
raise ProfileError('multiple ProfileHeader')
|
||||
if len(pes.get_pes_for_type('mf')) != 1:
|
||||
# strictly speaking: 0 permitted, but we don't support MF via GenericFileManagement
|
||||
raise ProfileError('multiple PE-MF')
|
||||
for tn in ['end', 'cd', 'telecom',
|
||||
'usim', 'isim', 'csim', 'opt-usim','opt-isim','opt-csim',
|
||||
'df-saip', 'df-5gs']:
|
||||
if len(pes.get_pes_for_type(tn)) > 1:
|
||||
raise ProfileError('multiple PE-%s' % tn.upper())
|
||||
|
||||
def check_optional_ordering(self, pes: ProfileElementSequence):
|
||||
# ordering and required depenencies
|
||||
self._is_after_if_exists(pes,'opt-usim', 'usim')
|
||||
self._is_after_if_exists(pes,'opt-isim', 'isim')
|
||||
self._is_after_if_exists(pes,'gsm-access', 'usim')
|
||||
self._is_after_if_exists(pes,'phonebook', 'usim')
|
||||
self._is_after_if_exists(pes,'df-5gs', 'usim')
|
||||
self._is_after_if_exists(pes,'df-saip', 'usim')
|
||||
self._is_after_if_exists(pes,'opt-csim', 'csim')
|
||||
|
||||
def check_mandatory_services(self, pes: ProfileElementSequence):
|
||||
"""Ensure that the PE for the mandatory services exist."""
|
||||
m_svcs = pes.get_pe_for_type('header').decoded['eUICC-Mandatory-services']
|
||||
if 'usim' in m_svcs and not pes.get_pe_for_type('usim'):
|
||||
raise ProfileError('no PE-USIM for mandatory usim service')
|
||||
if 'isim' in m_svcs and not pes.get_pe_for_type('isim'):
|
||||
raise ProfileError('no PE-ISIM for mandatory isim service')
|
||||
if 'csim' in m_svcs and not pes.get_pe_for_type('csim'):
|
||||
raise ProfileError('no PE-ISIM for mandatory csim service')
|
||||
if 'gba-usim' in m_svcs and not 'usim' in m_svcs:
|
||||
raise ProfileError('gba-usim mandatory, but no usim')
|
||||
if 'gba-isim' in m_svcs and not 'isim' in m_svcs:
|
||||
raise ProfileError('gba-isim mandatory, but no isim')
|
||||
if 'multiple-usim' in m_svcs and not 'usim' in m_svcs:
|
||||
raise ProfileError('multiple-usim mandatory, but no usim')
|
||||
if 'multiple-isim' in m_svcs and not 'isim' in m_svcs:
|
||||
raise ProfileError('multiple-isim mandatory, but no isim')
|
||||
if 'multiple-csim' in m_svcs and not 'csim' in m_svcs:
|
||||
raise ProfileError('multiple-csim mandatory, but no csim')
|
||||
if 'get-identity' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
raise ProfileError('get-identity mandatory, but no usim or isim')
|
||||
if 'profile-a-x25519' in m_svcs and not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
raise ProfileError('profile-a-x25519 mandatory, but no usim or isim')
|
||||
if 'profile-a-p256' in m_svcs and not not ('usim' in m_svcs or 'isim' in m_svcs):
|
||||
raise ProfileError('profile-a-p256 mandatory, but no usim or isim')
|
||||
67
tests/test_esim_saip.py
Executable file
67
tests/test_esim_saip.py
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import unittest
|
||||
import logging
|
||||
import copy
|
||||
|
||||
from pySim.utils import h2b, b2h
|
||||
from pySim.esim.saip import *
|
||||
from pySim.esim.saip.personalization import *
|
||||
from pprint import pprint as pp
|
||||
|
||||
|
||||
class SaipTest(unittest.TestCase):
|
||||
with open('smdpp-data/upp/TS48 V2 eSIM_GTP_SAIP2.3_NoBERTLV.rename2der', 'rb') as f:
|
||||
per_input = f.read()
|
||||
pes = ProfileElementSequence.from_der(per_input)
|
||||
expected_pet_list = ['header', 'mf', 'pukCodes', 'pinCodes', 'telecom', 'pinCodes', 'genericFileManagement', 'usim', 'opt-usim', 'pinCodes', 'akaParameter', 'gsm-access', 'df-5gs', 'df-saip','csim', 'opt-csim', 'pinCodes', 'cdmaParameter', 'isim', 'opt-isim', 'pinCodes', 'akaParameter', 'genericFileManagement', 'genericFileManagement', 'securityDomain', 'rfm', 'rfm', 'rfm', 'rfm', 'end']
|
||||
|
||||
def test_reencode_sequence(self):
|
||||
"""Test that we can decode and re-encode the entire DER encoded UPP."""
|
||||
reencoded_der = self.pes.to_der()
|
||||
self.assertEqual(reencoded_der, self.per_input)
|
||||
|
||||
def test_reencode_pe(self):
|
||||
"""Test that we can decode and re-encode reach individual ProfileElement."""
|
||||
remainder = self.per_input
|
||||
while len(remainder):
|
||||
first_tlv, remainder = bertlv_first_segment(remainder)
|
||||
pe = ProfileElement.from_der(first_tlv)
|
||||
with self.subTest(pe.type):
|
||||
reenc_tlv = pe.to_der()
|
||||
self.assertEqual(reenc_tlv, first_tlv)
|
||||
|
||||
|
||||
def test_sequence_helpers(self):
|
||||
"""Verify that the convenience helpers worked as expected."""
|
||||
self.assertEqual([x.type for x in self.pes.pe_list], self.expected_pet_list)
|
||||
self.assertEqual(len(self.pes.pes_by_naa), 4)
|
||||
|
||||
def test_personalization(self):
|
||||
"""Test some of the personalization operations."""
|
||||
pes = copy.deepcopy(self.pes)
|
||||
params = [Puk1(value=b'01234567'), Puk2(value=b'98765432'), Pin1(b'1111'), Pin2(b'2222'), Adm1(b'11111111'),
|
||||
K(h2b('000102030405060708090a0b0c0d0e0f')), Opc(h2b('101112131415161718191a1b1c1d1e1f'))]
|
||||
for p in params:
|
||||
p.apply(pes)
|
||||
# TODO: we don't actually test the results here, but we just verify there is no exception
|
||||
pes.to_der()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user