#!/usr/bin/env python3 # (C) 2025 by sysmocom - s.f.m.c. GmbH # # 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 . import io import sys import unittest import io 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 import xo update_expected_output = False def valstr(val): if isinstance(val, io.BytesIO): val = val.getvalue() if isinstance(val, bytearray): val = bytes(val) return f'{val!r}' def valtypestr(val): if isinstance(val, dict): types = [] for v in val.values(): types.append(f'{type(v).__name__}') val_type = '{' + ', '.join(types) + '}' else: val_type = f'{type(val).__name__}' return f'{valstr(val)}:{val_type}' 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: def __init__(self, param_cls, val, expect_val, expect_clean_val=None): self.param_cls = param_cls self.val = val self.expect_clean_val = expect_clean_val self.expect_val = expect_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.Puk1, val=int(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.Pin1, val=int(1234), expect_clean_val=b'1234\xff\xff\xff\xff', expect_val='1234'), Paramtest(param_cls=p13n.Pin1, val=int(123456), expect_clean_val=b'123456\xff\xff', expect_val='123456'), Paramtest(param_cls=p13n.Pin1, val=int(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.Adm1, val=int(123456), expect_clean_val=b'123456\xff\xff', expect_val='123456'), 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.AlgorithmID, val=1, expect_clean_val=1, expect_val='Milenage'), Paramtest(param_cls=p13n.AlgorithmID, val=2, expect_clean_val=2, expect_val='TUAK'), Paramtest(param_cls=p13n.AlgorithmID, val=3, 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.K, val=io.BytesIO(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=int(11020304050607080910111213141516), expect_clean_val=b'\x11\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16', expect_val='11020304050607080910111213141516'), 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'), Paramtest(param_cls=p13n.Opc, val=io.BytesIO(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'), ] # SdKey classes don't make sense yet, will come in Ic92ddea6e1fad8167ea75baf78ffc3eb419838c4 outputs = [] for upp_fname in upp_fnames: test_idx = -1 try: der = resources.read_binary(smdpp_data.upp, upp_fname) for t in param_tests: test_idx += 1 logloc = f'{upp_fname} {t.param_cls.__name__}(val={valtypestr(t.val)})' param = None try: param = t.param_cls() param.input_value = t.val param.validate() except ValueError as e: raise ValueError(f'{logloc}: {e}') from e clean_val = param.value logloc = f'{logloc} clean_val={valtypestr(clean_val)}' if t.expect_clean_val is not None and t.expect_clean_val != clean_val: raise ValueError(f'{logloc}: expected' f' expect_clean_val={valtypestr(t.expect_clean_val)}') # 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 as e: raise ValueError(f'{logloc} apply_val(clean_val): {e}') from e 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={valtypestr(read_back_val)}') 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' ) output = f'\nok: {ok}' outputs.append(output) print(output) except Exception as e: raise RuntimeError(f'Error while testing UPP {upp_fname} {test_idx=}: {e}') from e output = '\n'.join(outputs) + '\n' xo_name = 'test_configurable_parameters' if update_expected_output: with resources.path(xo, xo_name) as xo_path: with open(xo_path, 'w', encoding='utf-8') as f: f.write(output) else: xo_str = resources.read_text(xo, xo_name) if xo_str != output: at = 0 while at < len(output): if output[at] == xo_str[at]: at += 1 continue break raise RuntimeError(f'output differs from expected output at position {at}: "{output[at:at+20]}" != "{xo_str[at:at+20]}"') if __name__ == "__main__": if '-u' in sys.argv: update_expected_output = True sys.argv.remove('-u') unittest.main()