laforge has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/37415?usp=email )
Change subject: pySim.esim.saip: Meaningful defaults in PE Constructor + test ......................................................................
pySim.esim.saip: Meaningful defaults in PE Constructor + test
Let's make sure the constructor of ProfileElement subclasses set meaningful defaults to the self.decoded member, so that the to_der() method can actually encode it. This is required when constructing a profile from scratch, as opposed to loading an existing one from DER.
Also, add a test to verify that the encoder passes without exception; doesn't test the generated binary data.
Change-Id: I401bca16e58461333733877ec79102a5ae7fe410 --- M pySim/esim/saip/__init__.py M tests/test_esim_saip.py 2 files changed, 170 insertions(+), 1 deletion(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/15/37415/1
diff --git a/pySim/esim/saip/__init__.py b/pySim/esim/saip/__init__.py index e690e6c..e35e1a5 100644 --- a/pySim/esim/saip/__init__.py +++ b/pySim/esim/saip/__init__.py @@ -265,6 +265,10 @@ def from_der(cls, der: bytes) -> 'ProfileElement': class4petype = { 'securityDomain': ProfileElementSD, + 'mf': ProfileElementMF, + 'pukCodes': ProfileElementPuk, + 'pinCodes': ProfileElementPin, + 'telecom': ProfileElementTelecom, 'usim': ProfileElementUSIM, 'isim': ProfileElementISIM, } @@ -294,6 +298,110 @@ def __str__(self) -> str: return self.type
+class ProfileElementMF(ProfileElement): + type = 'mf' + + def __init__(self, decoded: Optional[dict] = None): + super().__init__() + if decoded: + self.decoded = decoded + return + # provide some reasonable defaults + self.decoded = OrderedDict() + self.decoded['mf-header'] = { 'mandated': None, 'identification': None} + self.decoded['templateID'] = str(oid.MF) + for fname in ['mf', 'ef-iccid', 'ef-dir', 'ef-arr']: + self.decoded[fname] = [] + # TODO: resize EF.DIR? + +class ProfileElementPuk(ProfileElement): + type = 'pukCodes' + + def __init__(self, decoded: Optional[dict] = None): + super().__init__() + if decoded: + self.decoded = decoded + return + # provide some reasonable defaults + self.decoded = OrderedDict() + self.decoded['puk-Header'] = { 'mandated': None, 'identification': None} + self.decoded['pukCodes'] = [] + self.add_puk(0x01, b'11111111') + self.add_puk(0x81, b'22222222') + + def add_puk(self, key_ref: int, puk_value: bytes, max_attempts:int = 10, retries_left:int = 10): + """Add a PUK to the pukCodes ProfileElement""" + if key_ref < 0 or key_ref > 0xff: + raise ValueError('key_ref must be uint8') + if len(puk_value) != 8: + raise ValueError('puk_value must be 8 bytes long') + if max_attempts < 0 or max_attempts > 0xf: + raise ValueError('max_attempts must be 4 bit') + if retries_left < 0 or max_attempts > 0xf: + raise ValueError('retries_left must be 4 bit') + puk = { + 'keyReference': key_ref, + 'pukValue': puk_value, + 'maxNumOfAttemps-retryNumLeft': (max_attempts << 4) | retries_left, + } + self.decoded['pukCodes'].append(puk) + + +class ProfileElementPin(ProfileElement): + type = 'pinCodes' + + def __init__(self, decoded: Optional[dict] = None): + super().__init__() + if decoded: + self.decoded = decoded + return + # provide some reasonable defaults + self.decoded = OrderedDict() + self.decoded['pin-Header'] = { 'mandated': None, 'identification': None} + self.decoded['pinCodes'] = ('pinconfig', []) + self.add_pin(0x01, b'0000\xff\xff\xff\xff', unblock_ref=1, pin_attrib=6) + self.add_pin(0x10, b'11111111', pin_attrib=3) + + def add_pin(self, key_ref: int, pin_value: bytes, max_attempts : int = 3, retries_left : int = 3, + unblock_ref: Optional[int] = None, pin_attrib: int = 7): + """Add a PIN to the pinCodes ProfileElement""" + if key_ref < 0 or key_ref > 0xff: + raise ValueError('key_ref must be uint8') + if pin_attrib < 0 or pin_attrib > 0xff: + raise ValueError('pin_attrib must be uint8') + if len(pin_value) != 8: + raise ValueError('pin_value must be 8 bytes long') + if max_attempts < 0 or max_attempts > 0xf: + raise ValueError('max_attempts must be 4 bit') + if retries_left < 0 or max_attempts > 0xf: + raise ValueError('retries_left must be 4 bit') + pin = { + 'keyReference': key_ref, + 'pinValue': pin_value, + 'maxNumOfAttemps-retryNumLeft': (max_attempts << 4) | retries_left, + 'pinAttributes': pin_attrib, + } + if unblock_ref: + pin['unblockingPINReference'] = unblock_ref + self.decoded['pinCodes'][1].append(pin) + + +class ProfileElementTelecom(ProfileElement): + type = 'telecom' + + def __init__(self, decoded: Optional[dict] = None): + super().__init__() + if decoded: + self.decoded = decoded + return + # provide some reasonable defaults for a MNO-SD + self.decoded = OrderedDict() + self.decoded['telecom-header'] = { 'mandated': None, 'identification': None} + self.decoded['templateID'] = str(oid.DF_TELECOM_v2) + for fname in ['df-telecom', 'ef-arr']: + self.decoded[fname] = [] + + class SecurityDomainKeyComponent: """Representation of a key-component of a key for a security domain.""" def __init__(self, key_type: str, key_data: bytes, mac_length: int = 8): @@ -444,9 +552,23 @@
class ProfileElementUSIM(ProfileElement): type = 'usim' + + def __init__(self, decoded: Optional[dict] = None): + super().__init__() + if decoded: + self.decoded = decoded + return + # provide some reasonable defaults for a MNO-SD + self.decoded = OrderedDict() + self.decoded['usim-header'] = { 'mandated': None, 'identification': None} + self.decoded['templateID'] = str(oid.ADF_USIM_by_default_v2) + for fname in ['adf-usim', 'ef-imsi', 'ef-arr', 'ef-ust', 'ef-spn', 'ef-est', 'ef-acc', 'ef-ecc']: + self.decoded[fname] = [] + @property def adf_name(self) -> str: return b2h(self.decoded['adf-usim'][0][1]['dfName']) + @property def imsi(self) -> Optional[str]: f = File('ef-imsi', self.decoded['ef-imsi']) @@ -454,6 +576,19 @@
class ProfileElementISIM(ProfileElement): type = 'isim' + + def __init__(self, decoded: Optional[dict] = None): + super().__init__() + if decoded: + self.decoded = decoded + return + # provide some reasonable defaults for a MNO-SD + self.decoded = OrderedDict() + self.decoded['isim-header'] = { 'mandated': None, 'identification': None} + self.decoded['templateID'] = str(oid.ADF_ISIM_by_default) + for fname in ['adf-isim', 'ef-impi', 'ef-impu', 'ef-domain', 'ef-ist', 'ef-arr']: + self.decoded[fname] = [] + @property def adf_name(self) -> str: return b2h(self.decoded['adf-isim'][0][1]['dfName']) @@ -470,10 +605,16 @@ 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_list: List[ProfileElement] = [] self.pe_by_type: Dict = {} self.pes_by_naa: Dict = {}
+ def append(self, pe: ProfileElement): + """Append a PE to the PE Sequence""" + self.pe_list.append(pe) + self._process_pelist() + self.renumber_identification() + def get_pes_for_type(self, tname: str) -> List[ProfileElement]: """Return list of profile elements present for given profile element type.""" return self.pe_by_type.get(tname, []) diff --git a/tests/test_esim_saip.py b/tests/test_esim_saip.py index a70c149..a43a139 100755 --- a/tests/test_esim_saip.py +++ b/tests/test_esim_saip.py @@ -63,6 +63,17 @@ # TODO: we don't actually test the results here, but we just verify there is no exception pes.to_der()
+ def test_constructor_encode(self): + """Test that DER-encoding of PE created by "empty" constructor works without raising exception.""" + for cls in [ProfileElementMF, ProfileElementPuk, ProfileElementPin, ProfileElementTelecom, + ProfileElementUSIM, ProfileElementISIM]: + with self.subTest(cls.__name__): + pes = ProfileElementSequence() + inst = cls() + pes.append(inst) + pes.to_der() + +
if __name__ == "__main__": unittest.main()