neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/40823?usp=email )
Change subject: add test_configurable_parameters.py ......................................................................
add test_configurable_parameters.py
Change-Id: Ia55f0d11f8197ca15a948a83a34b3488acf1a0b4 --- M pySim/esim/saip/personalization.py A tests/unittests/smdpp_data A tests/unittests/test_configurable_parameters.py 3 files changed, 319 insertions(+), 0 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/23/40823/1
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py index e0cb97c..27b570c 100644 --- a/pySim/esim/saip/personalization.py +++ b/pySim/esim/saip/personalization.py @@ -219,6 +219,26 @@ pass
@classmethod + def get_value_from_pes(cls, pes: ProfileElementSequence): + """Same as get_values_from_pes() but expecting a single value. + get_values_from_pes() may return values like this: + [{ 'AlgorithmID': 'Milenage' }, { 'AlgorithmID': 'Milenage' }] + This ensures that all these entries are identical and would return only + { 'AlgorithmID': 'Milenage' }. + + This is relevant for any profile element that may appear multiple times in the same PES (only a few), + where each occurence should reflect the same value (all currently known parameters). + """ + + val = None + for v in cls.get_values_from_pes(pes): + if val is None: + val = v + elif val != v: + raise ValueError(f'get_value_from_pes(): got distinct values: {val!r} != {v!r}') + return val + + @classmethod def get_values_from_pes(cls, pes: ProfileElementSequence) -> Generator: """This is what subclasses implement: yield all values from a decoded profile package. Find all values in the pes, and yield them decoded to a valid cls.input_value format. diff --git a/tests/unittests/smdpp_data b/tests/unittests/smdpp_data new file mode 120000 index 0000000..a2d60f8 --- /dev/null +++ b/tests/unittests/smdpp_data @@ -0,0 +1 @@ +../../smdpp-data \ No newline at end of file diff --git a/tests/unittests/test_configurable_parameters.py b/tests/unittests/test_configurable_parameters.py new file mode 100755 index 0000000..a1ca91b --- /dev/null +++ b/tests/unittests/test_configurable_parameters.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 + +# (C) 2025 by sysmocom - s.f.m.c. GmbH info@sysmocom.de +# +# Author: Neels Hofmeyr +# +# 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 +from importlib import resources +from osmocom.utils import hexstr +from pySim.esim.saip import ProfileElementSequence +import pySim.esim.saip.personalization as p13n +import smdpp_data.upp + +class D: + mandatory = set() + optional = set() + + def __init__(self, **kwargs): + if (set(kwargs.keys()) - set(self.optional)) != set(self.mandatory): + raise RuntimeError(f'{self.__class__.__name__}.__init__():' + f' {set(kwargs.keys())=!r} - {self.optional=!r} != {self.mandatory=!r}') + for k, v in kwargs.items(): + setattr(self, k, v) + for k in self.optional: + if not hasattr(self, k): + setattr(self, k, None) + +class ConfigurableParameterTest(unittest.TestCase): + + def test_parameters(self): + + upp_fnames = ( + 'TS48v5_SAIP2.1A_NoBERTLV.der', + 'TS48v5_SAIP2.3_BERTLV_SUCI.der', + 'TS48v5_SAIP2.1B_NoBERTLV.der', + 'TS48v5_SAIP2.3_NoBERTLV.der', + ) + + class Paramtest(D): + mandatory = ( + 'param_cls', + 'val', + 'expect_val', + ) + optional = ( + 'expect_clean_val', + ) + + param_tests = [ + Paramtest(param_cls=p13n.Imsi, val='123456', + expect_clean_val=str('123456'), + expect_val={'IMSI': hexstr('123456'), + 'IMSI-ACC': '0040'}), + Paramtest(param_cls=p13n.Imsi, val=int(123456), + expect_val={'IMSI': hexstr('123456'), + 'IMSI-ACC': '0040'}), + + Paramtest(param_cls=p13n.Imsi, val='123456789012345', + expect_clean_val=str('123456789012345'), + expect_val={'IMSI': hexstr('123456789012345'), + 'IMSI-ACC': '0020'}), + Paramtest(param_cls=p13n.Imsi, val=int(123456789012345), + expect_val={'IMSI': hexstr('123456789012345'), + 'IMSI-ACC': '0020'}), + + Paramtest(param_cls=p13n.Puk1, + val='12345678', + expect_clean_val=b'12345678', + expect_val='12345678'), + + Paramtest(param_cls=p13n.Puk2, + val='12345678', + expect_clean_val=b'12345678', + expect_val='12345678'), + + Paramtest(param_cls=p13n.Pin1, + val='1234', + expect_clean_val=b'1234\xff\xff\xff\xff', + expect_val='1234'), + Paramtest(param_cls=p13n.Pin1, + val='123456', + expect_clean_val=b'123456\xff\xff', + expect_val='123456'), + Paramtest(param_cls=p13n.Pin1, + val='12345678', + expect_clean_val=b'12345678', + expect_val='12345678'), + + Paramtest(param_cls=p13n.Adm1, + val='1234', + expect_clean_val=b'1234\xff\xff\xff\xff', + expect_val='1234'), + Paramtest(param_cls=p13n.Adm1, + val='123456', + expect_clean_val=b'123456\xff\xff', + expect_val='123456'), + Paramtest(param_cls=p13n.Adm1, + val='12345678', + expect_clean_val=b'12345678', + expect_val='12345678'), + + Paramtest(param_cls=p13n.AlgorithmID, + val='Milenage', + expect_clean_val=1, + expect_val='Milenage'), + Paramtest(param_cls=p13n.AlgorithmID, + val='TUAK', + expect_clean_val=2, + expect_val='TUAK'), + Paramtest(param_cls=p13n.AlgorithmID, + val='usim-test', + expect_clean_val=3, + expect_val='usim-test'), + + Paramtest(param_cls=p13n.K, + val='01020304050607080910111213141516', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516'), + Paramtest(param_cls=p13n.K, + val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516'), + Paramtest(param_cls=p13n.K, + val=bytearray(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16'), + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516'), + + Paramtest(param_cls=p13n.Opc, + val='01020304050607080910111213141516', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516'), + Paramtest(param_cls=p13n.Opc, + val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516'), + Paramtest(param_cls=p13n.Opc, + val=bytearray(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16'), + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516'), + ] + + for sdkey_cls in ( + p13n.SdKeyScp80Kvn01Enc, + p13n.SdKeyScp80Kvn01Dek, + p13n.SdKeyScp80Kvn01Mac, + p13n.SdKeyScp80Kvn02Enc, + p13n.SdKeyScp80Kvn02Dek, + p13n.SdKeyScp80Kvn02Mac, + p13n.SdKeyScp81Kvn81Enc, + p13n.SdKeyScp81Kvn81Dek, + p13n.SdKeyScp81Kvn81Mac, + p13n.SdKeyScp81Kvn82Enc, + p13n.SdKeyScp81Kvn82Dek, + p13n.SdKeyScp81Kvn82Mac, + p13n.SdKeyScp81Kvn83Enc, + p13n.SdKeyScp81Kvn83Dek, + p13n.SdKeyScp81Kvn83Mac, + p13n.SdKeyScp02Kvn20Enc, + p13n.SdKeyScp02Kvn20Dek, + p13n.SdKeyScp02Kvn20Mac, + p13n.SdKeyScp02Kvn21Enc, + p13n.SdKeyScp02Kvn21Dek, + p13n.SdKeyScp02Kvn21Mac, + p13n.SdKeyScp02Kvn22Enc, + p13n.SdKeyScp02Kvn22Dek, + p13n.SdKeyScp02Kvn22Mac, + p13n.SdKeyScp02KvnffEnc, + p13n.SdKeyScp02KvnffDek, + p13n.SdKeyScp02KvnffMac, + p13n.SdKeyScp03Kvn30Enc, + p13n.SdKeyScp03Kvn30Dek, + p13n.SdKeyScp03Kvn30Mac, + p13n.SdKeyScp03Kvn31Enc, + p13n.SdKeyScp03Kvn31Dek, + p13n.SdKeyScp03Kvn31Mac, + p13n.SdKeyScp03Kvn32Enc, + p13n.SdKeyScp03Kvn32Dek, + p13n.SdKeyScp03Kvn32Mac, + ): + + param_tests.extend([ + + Paramtest(param_cls=sdkey_cls, + val='01020304050607080910111213141516', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516', + ), + Paramtest(param_cls=sdkey_cls, + val='010203040506070809101112131415161718192021222324', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16' + b'\x17\x18\x19\x20\x21\x22\x23\x24', + expect_val='010203040506070809101112131415161718192021222324'), + Paramtest(param_cls=sdkey_cls, + val='0102030405060708091011121314151617181920212223242526272829303132', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16' + b'\x17\x18\x19\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x30\x31\x32', + expect_val='0102030405060708091011121314151617181920212223242526272829303132'), + + Paramtest(param_cls=sdkey_cls, + val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516', + ), + Paramtest(param_cls=sdkey_cls, + val=bytearray(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16'), + expect_clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', + expect_val='01020304050607080910111213141516', + ), + ]) + + for upp_fname in upp_fnames: + test_nr = -1 + try: + + der = resources.read_binary(smdpp_data.upp, upp_fname) + + for t in param_tests: + test_nr += 1 + + logloc = f'{upp_fname}[{test_nr}] {t.param_cls.__name__}({t.val=!r}:{type(t.val).__name__})' + + param = None + try: + param = t.param_cls() + param.input_value = t.val + param.validate() + except ValueError: + print(f"Error at: {logloc}") + raise + + clean_val = param.value + logloc = f'{logloc} {clean_val=!r}:{type(clean_val).__name__}' + if t.expect_clean_val is not None and t.expect_clean_val != clean_val: + raise ValueError(f'{logloc}: expected' + f' {t.expect_clean_val=!r}:{type(t.expect_clean_val).__name__}') + + # on my laptop, deepcopy is about 30% slower than decoding the DER from scratch: + # pes = copy.deepcopy(orig_pes) + pes = ProfileElementSequence.from_der(der) + try: + param.apply(pes) + except ValueError: + print(f"Error at: {logloc}: {t.param_cls.get_name()}.apply_val({clean_val=!r})") + raise + + changed_der = pes.to_der() + + pes2 = ProfileElementSequence.from_der(changed_der) + + read_back_val = t.param_cls.get_value_from_pes(pes2) + + # compose log string to show the precise type of dict values + if isinstance(read_back_val, dict): + types = set() + for v in read_back_val.values(): + types.add(f'{type(v).__name__}') + + read_back_val_type = '{' + ', '.join(types) + '}' + else: + read_back_val_type = f'{type(read_back_val).__name__}' + + logloc = (f'{logloc} {read_back_val=!r}:{read_back_val_type}') + + if isinstance(read_back_val, dict) and not t.param_cls.get_name() in read_back_val.keys(): + raise ValueError(f'{logloc}: expected to find name {t.param_cls.get_name()!r} in read_back_val') + + expect_val = t.expect_val + if not isinstance(expect_val, dict): + expect_val = { t.param_cls.get_name(): expect_val } + if read_back_val != expect_val: + raise ValueError(f'{logloc}: expected {expect_val=!r}:{type(t.expect_val).__name__}') + + ok = logloc.replace(' clean_val', '\n\tclean_val' + ).replace(' read_back_val', '\n\tread_back_val' + ).replace('=', '=\t' + ) + print(f'\nok: {ok}') + + except: + print(f'Error while testing UPP {upp_fname}') + raise + + +if __name__ == "__main__": + unittest.main()