neels has uploaded this change for review. ( 
https://gerrit.osmocom.org/c/pysim/+/40198?usp=email )


Change subject: personalization: implement reading back values from a PES
......................................................................

personalization: implement reading back values from a PES

Implement get_values_from_pes(), the reverse direction of apply_val():
read back and return values from a ProfileElementSequence. Implement for
all ConfigurableParameter subclasses.

Future: SdKey.get_values_from_pes() is reading pe.decoded[], which works
fine, but I07dfc378705eba1318e9e8652796cbde106c6a52 will change this
implementation to use the higher level ProfileElementSD members.

Implementation detail:

Implement get_values_from_pes() as classmethod that returns a generator.
Subclasses should yield all occurences of their parameter in a given
PES.

For example, the ICCID can appear in multiple places.
Iccid.get_values_from_pes() yields all of the individual values. A set()
of the results quickly tells whether the PES is consistent.

Rationales for reading back values:

This allows auditing an eSIM profile, particularly for producing an
output.csv from a batch personalization (that generated lots of random
key material which now needs to be fed to an HLR...).

Reading back from a binary result is more reliable than storing the
values that were fed into a personalization.
By auditing final DER results with this code, I discovered:
- "oh, there already was some key material in my UPP template."
- "all IMSIs ended up the same, forgot to set up the parameter."
- the SdKey.apply() implementations currently don't work, see
  I07dfc378705eba1318e9e8652796cbde106c6a52 for a fix.

Change-Id: I234fc4317f0bdc1a486f0cee4fa432c1dce9b463
---
M pySim/esim/saip/personalization.py
1 file changed, 121 insertions(+), 2 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/98/40198/1

diff --git a/pySim/esim/saip/personalization.py 
b/pySim/esim/saip/personalization.py
index aaa4d8f..ca37e3a 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -18,13 +18,17 @@
 import abc
 import io
 import copy
-from typing import List, Tuple, Generator
+from typing import List, Tuple, Generator, Optional

 from osmocom.tlv import camel_to_snake
-from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid, 
all_subclasses_of
+from osmocom.utils import hexstr
+from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, 
rpad, sanitize_iccid, all_subclasses_of
 from pySim.esim.saip import ProfileElement, ProfileElementSequence
 from pySim.esim.saip import param_source

+def unrpad(s: hexstr, c='f') -> hexstr:
+    return hexstr(s.rstrip(c))
+
 def remove_unwanted_tuples_from_list(l: List[Tuple], unwanted_keys: List[str]) 
-> List[Tuple]:
     """In a list of tuples, remove all tuples whose first part equals 
'unwanted_key'."""
     return list(filter(lambda x: x[0] not in unwanted_keys, l))
@@ -36,6 +40,22 @@
     file.append(('fillFileContent', new_content))
     return file

+def file_tuples_content_as_bytes(l: List[Tuple]) -> Optional[bytes]:
+    """linearize a list of fillFileContent / fillFileOffset tuples into a 
stream of bytes."""
+    stream = io.BytesIO()
+    for k, v in l:
+        if k == 'doNotCreate':
+            return None
+        if k == 'fileDescriptor':
+            pass
+        elif k == 'fillFileOffset':
+            stream.seek(v, os.SEEK_CUR)
+        elif k == 'fillFileContent':
+            stream.write(v)
+        else:
+            return ValueError("Unknown key '%s' in tuple list" % k)
+    return stream.getvalue()
+
 class ConfigurableParameter:
     r"""Base class representing a part of the eSIM profile that is 
configurable during the
     personalization process (with dynamic data from elsewhere).
@@ -195,6 +215,30 @@
         pass

     @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.
+           Should be a generator function, i.e. use 'yield' instead of 
'return'.
+
+           Usage example:
+
+             cls = esim.saip.personalization.Iccid
+             # use a set() to get a list of unique values from all results
+             vals = set( cls.get_values_from_pes(pes) )
+             if len(vals) != 1:
+                 raise ValueError(f'{cls.name}: need exactly one value, got 
{vals}')
+             # the set contains a single value, return it
+             return vals.pop()
+
+           Implementation example:
+
+             for pe in pes:
+                if my_condition(pe):
+                    yield b2h(my_bin_value_from(pe))
+           '''
+        pass
+
+    @classmethod
     def get_len_range(cls):
         """considering all of min_len, max_len and allow_len, get a tuple of 
the resulting (min, max) of permitted
         value length. For example, if an input value is an int, which needs to 
be represented with a minimum nr of
@@ -260,6 +304,17 @@
         # a DecimalHexParam subclass expects the apply_val() input to be a 
bytes instance ready for the pes
         return h2b(val)

+    @classmethod
+    def decimal_hex_to_str(cls, val):
+        'useful for get_values_from_pes() implementations of subclasses'
+        if isinstance(val, bytes):
+            val = b2h(val)
+        assert isinstance(val, hexstr)
+        if cls.rpad is not None:
+            c = cls.rpad_char or 'f'
+            val = unrpad(val, c)
+        return val.to_bytes().decode('ascii')
+

 class BinaryParam(ConfigurableParameter):
     allow_types = (str, io.BytesIO, bytes, bytearray)
@@ -305,6 +360,17 @@
         # patch MF/EF.ICCID
         file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], 
h2b(enc_iccid(val)))

+    @classmethod
+    def get_values_from_pes(cls, pes: ProfileElementSequence):
+        padded = b2h(pes.get_pe_for_type('header').decoded['iccid'])
+        iccid = unrpad(padded)
+        yield iccid
+
+        for pe in pes.get_pes_for_type('mf'):
+            iccid_pe = pe.decoded.get('ef-iccid', None)
+            if iccid_pe:
+                yield dec_iccid(b2h(file_tuples_content_as_bytes(iccid_pe)))
+
 class Imsi(DecimalParam):
     """Configurable IMSI. Expects value to be a string of digits. 
Automatically sets the ACC to
     the last digit of the IMSI."""
@@ -326,6 +392,13 @@
             file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
         # TODO: DF.GSM_ACCESS if not linked?

+    @classmethod
+    def get_values_from_pes(cls, pes: ProfileElementSequence):
+        for pe in pes.get_pes_for_type('usim'):
+            imsi_pe = pe.decoded.get('ef-imsi', None)
+            if imsi_pe:
+                yield dec_imsi(b2h(file_tuples_content_as_bytes(imsi_pe)))
+

 class SdKey(BinaryParam):
     """Configurable Security Domain (SD) Key.  Value is presented as bytes."""
@@ -359,6 +432,14 @@
         for pe in pes.get_pes_for_type('securityDomain'):
             cls._apply_sd(pe, value)

+    @classmethod
+    def get_values_from_pes(cls, pes: ProfileElementSequence):
+        for pe in pes.get_pes_for_type('securityDomain'):
+            for key in pe.decoded['keyList']:
+                if key['keyIdentifier'][0] == cls.key_id and 
key['keyVersionNumber'][0] == cls.kvn:
+                    if len(key['keyComponents']) >= 1:
+                        yield b2h(key['keyComponents'][0]['keyData'])
+
 class SdKeyScp80_01(SdKey):
     kvn = 0x01
     key_type = 0x88 # AES key type
@@ -495,6 +576,14 @@
         raise ValueError("input template UPP has unexpected structure:"
                          f" cannot find pukCode with 
keyReference={cls.keyReference}")

+    @classmethod
+    def get_values_from_pes(cls, pes: ProfileElementSequence):
+        mf_pes = pes.pes_by_naa['mf'][0]
+        for pukCodes in obtain_all_pe_from_pelist(mf_pes, 'pukCodes'):
+            for pukCode in pukCodes.decoded['pukCodes']:
+                if pukCode['keyReference'] == cls.keyReference:
+                    yield cls.decimal_hex_to_str(pukCode['pukValue'])
+
 class Puk1(Puk):
     is_abstract = False
     name = 'PUK1'
@@ -532,6 +621,20 @@
             raise ValueError('input template UPP has unexpected structure:'
                              + f' {cls.get_name()} cannot find pinCode with 
keyReference={cls.keyReference}')

+    @classmethod
+    def _read_all_pinvalues_from_pe(cls, pe: ProfileElement):
+        for pinCodes in obtain_all_pe_from_pelist(pe, 'pinCodes'):
+            if pinCodes.decoded['pinCodes'][0] != 'pinconfig':
+                continue
+
+            for pinCode in pinCodes.decoded['pinCodes'][1]:
+                if pinCode['keyReference'] == cls.keyReference:
+                     yield cls.decimal_hex_to_str(pinCode['pinValue'])
+
+    @classmethod
+    def get_values_from_pes(cls, pes: ProfileElementSequence):
+        yield from cls._read_all_pinvalues_from_pe(pes.pes_by_naa['mf'][0])
+
 class Pin1(Pin):
     is_abstract = False
     name = 'PIN1'
@@ -555,6 +658,14 @@
                     raise ValueError('input template UPP has unexpected 
structure:'
                             + f' {cls.get_name()} cannot find pinCode with 
keyReference={cls.keyReference} in {naa=}')

+    @classmethod
+    def get_values_from_pes(cls, pes: ProfileElementSequence):
+        for naa in pes.pes_by_naa:
+            if naa not in ['usim','isim','csim','telecom']:
+                continue
+            for pe in pes.pes_by_naa[naa]:
+                yield from cls._read_all_pinvalues_from_pe(pe)
+
 class Adm1(Pin):
     is_abstract = False
     name = 'ADM1'
@@ -581,6 +692,14 @@
             raise ValueError('input template UPP has unexpected structure:'
                              f' {cls.__name__} cannot find algoParameter with 
key={cls.algo_config_key}')

+    @classmethod
+    def get_values_from_pes(cls, pes: ProfileElementSequence):
+        for pe in pes.get_pes_for_type('akaParameter'):
+            algoConfiguration = pe.decoded['algoConfiguration']
+            if algoConfiguration[0] != 'algoParameter':
+                continue
+            yield algoConfiguration[1][cls.algo_config_key]
+

 class AlgorithmID(DecimalParam, AlgoConfig):
     is_abstract = False

--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40198?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.osmocom.org/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I234fc4317f0bdc1a486f0cee4fa432c1dce9b463
Gerrit-Change-Number: 40198
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofm...@sysmocom.de>

Reply via email to