neels has uploaded this change for review.

View Change

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()

To view, visit change 40823. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-MessageType: newchange
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: Ia55f0d11f8197ca15a948a83a34b3488acf1a0b4
Gerrit-Change-Number: 40823
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr@sysmocom.de>