mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-24 06:18:33 +03:00
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
This commit is contained in:
@@ -17,15 +17,20 @@
|
|||||||
|
|
||||||
import abc
|
import abc
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
import copy
|
import copy
|
||||||
from typing import List, Tuple, Generator
|
from typing import List, Tuple, Generator, Optional
|
||||||
|
|
||||||
from osmocom.tlv import camel_to_snake
|
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 ProfileElement, ProfileElementSequence
|
||||||
from pySim.ts_51_011 import EF_SMSP
|
from pySim.ts_51_011 import EF_SMSP
|
||||||
from pySim.esim.saip import param_source
|
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]:
|
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'."""
|
"""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))
|
return list(filter(lambda x: x[0] not in unwanted_keys, l))
|
||||||
@@ -48,6 +53,22 @@ class ClassVarMeta(abc.ABCMeta):
|
|||||||
setattr(x, 'name', camel_to_snake(name))
|
setattr(x, 'name', camel_to_snake(name))
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
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:
|
class ConfigurableParameter:
|
||||||
r"""Base class representing a part of the eSIM profile that is configurable during the
|
r"""Base class representing a part of the eSIM profile that is configurable during the
|
||||||
personalization process (with dynamic data from elsewhere).
|
personalization process (with dynamic data from elsewhere).
|
||||||
@@ -206,6 +227,30 @@ class ConfigurableParameter:
|
|||||||
Write the given val in the right format in all the right places in pes."""
|
Write the given val in the right format in all the right places in pes."""
|
||||||
pass
|
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
|
@classmethod
|
||||||
def get_len_range(cls):
|
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
|
"""considering all of min_len, max_len and allow_len, get a tuple of the resulting (min, max) of permitted
|
||||||
@@ -272,6 +317,17 @@ class DecimalHexParam(DecimalParam):
|
|||||||
# a DecimalHexParam subclass expects the apply_val() input to be a bytes instance ready for the pes
|
# a DecimalHexParam subclass expects the apply_val() input to be a bytes instance ready for the pes
|
||||||
return h2b(val)
|
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 IntegerParam(ConfigurableParameter):
|
class IntegerParam(ConfigurableParameter):
|
||||||
allow_types = (str, int)
|
allow_types = (str, int)
|
||||||
allow_chars = '0123456789'
|
allow_chars = '0123456789'
|
||||||
@@ -347,6 +403,17 @@ class Iccid(DecimalParam):
|
|||||||
# patch MF/EF.ICCID
|
# patch MF/EF.ICCID
|
||||||
file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(val)))
|
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):
|
class Imsi(DecimalParam):
|
||||||
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
|
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
|
||||||
the last digit of the IMSI."""
|
the last digit of the IMSI."""
|
||||||
@@ -368,13 +435,21 @@ class Imsi(DecimalParam):
|
|||||||
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
|
file_replace_content(pe.decoded['ef-acc'], acc.to_bytes(2, 'big'))
|
||||||
# TODO: DF.GSM_ACCESS if not linked?
|
# 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 SmspTpScAddr(ConfigurableParameter):
|
class SmspTpScAddr(ConfigurableParameter):
|
||||||
"""Configurable SMSC (SMS Service Centre) TP-SC-ADDR. Expects to be a phone number in national or
|
"""Configurable SMSC (SMS Service Centre) TP-SC-ADDR. Expects to be a phone number in national or
|
||||||
international format (designated by a leading +). Automatically sets the NPI to E.164 and the TON based on
|
international format (designated by a leading +). Automatically sets the NPI to E.164 and the TON based on
|
||||||
presence or absence of leading +"""
|
presence or absence of leading +."""
|
||||||
|
|
||||||
def validate(self):
|
@classmethod
|
||||||
addr_str = str(self.input_value)
|
def validate_val(cls, val):
|
||||||
|
addr_str = str(val)
|
||||||
if addr_str[0] == '+':
|
if addr_str[0] == '+':
|
||||||
digits = addr_str[1:]
|
digits = addr_str[1:]
|
||||||
international = True
|
international = True
|
||||||
@@ -385,10 +460,14 @@ class SmspTpScAddr(ConfigurableParameter):
|
|||||||
raise ValueError('TP-SC-ADDR must not exceed 20 digits')
|
raise ValueError('TP-SC-ADDR must not exceed 20 digits')
|
||||||
if not digits.isdecimal():
|
if not digits.isdecimal():
|
||||||
raise ValueError('TP-SC-ADDR must only contain decimal digits')
|
raise ValueError('TP-SC-ADDR must only contain decimal digits')
|
||||||
self.value = (international, digits)
|
return (international, digits)
|
||||||
|
|
||||||
def apply(self, pes: ProfileElementSequence):
|
@classmethod
|
||||||
international, digits = self.value
|
def apply_val(cls, pes: ProfileElementSequence, val):
|
||||||
|
"""val must be a tuple (international[bool], digits[str]).
|
||||||
|
For example, an input of "+1234" corresponds to (True, "1234");
|
||||||
|
An input of "1234" corresponds to (False, "1234")."""
|
||||||
|
international, digits = val
|
||||||
for pe in pes.get_pes_for_type('usim'):
|
for pe in pes.get_pes_for_type('usim'):
|
||||||
# obtain the File instance from the ProfileElementUSIM
|
# obtain the File instance from the ProfileElementUSIM
|
||||||
f_smsp = pe.files['ef-smsp']
|
f_smsp = pe.files['ef-smsp']
|
||||||
@@ -411,6 +490,32 @@ class SmspTpScAddr(ConfigurableParameter):
|
|||||||
# re-generate the pe.decoded member from the File instance
|
# re-generate the pe.decoded member from the File instance
|
||||||
pe.file2pe(f_smsp)
|
pe.file2pe(f_smsp)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
||||||
|
for pe in pes.get_pes_for_type('usim'):
|
||||||
|
f_smsp = pe.files['ef-smsp']
|
||||||
|
ef_smsp = EF_SMSP()
|
||||||
|
ef_smsp_dec = ef_smsp.decode_record_bin(f_smsp.body, 1)
|
||||||
|
|
||||||
|
tp_sc_addr = ef_smsp_dec.get('tp_sc_addr', None)
|
||||||
|
if not tp_sc_addr:
|
||||||
|
continue
|
||||||
|
|
||||||
|
digits = tp_sc_addr.get('call_number', None)
|
||||||
|
if not digits:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ton_npi = tp_sc_addr.get('ton_npi', None)
|
||||||
|
if not ton_npi:
|
||||||
|
continue
|
||||||
|
international = ton_npi.get('type_of_number', None)
|
||||||
|
if international is None:
|
||||||
|
continue
|
||||||
|
international = (international == 'international')
|
||||||
|
|
||||||
|
yield (international, digits)
|
||||||
|
|
||||||
|
|
||||||
class SdKey(BinaryParam, metaclass=ClassVarMeta):
|
class SdKey(BinaryParam, metaclass=ClassVarMeta):
|
||||||
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
|
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
|
||||||
# these will be set by subclasses
|
# these will be set by subclasses
|
||||||
@@ -443,6 +548,14 @@ class SdKey(BinaryParam, metaclass=ClassVarMeta):
|
|||||||
for pe in pes.get_pes_for_type('securityDomain'):
|
for pe in pes.get_pes_for_type('securityDomain'):
|
||||||
cls._apply_sd(pe, value)
|
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, permitted_len=[16,24,32]): # AES key type
|
class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
|
||||||
pass
|
pass
|
||||||
class SdKeyScp80_01Kic(SdKeyScp80_01, key_id=0x01, key_usage_qual=0x18): # FIXME: ordering?
|
class SdKeyScp80_01Kic(SdKeyScp80_01, key_id=0x01, key_usage_qual=0x18): # FIXME: ordering?
|
||||||
@@ -529,6 +642,14 @@ class Puk(DecimalHexParam):
|
|||||||
raise ValueError("input template UPP has unexpected structure:"
|
raise ValueError("input template UPP has unexpected structure:"
|
||||||
f" cannot find pukCode with keyReference={cls.keyReference}")
|
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):
|
class Puk1(Puk):
|
||||||
is_abstract = False
|
is_abstract = False
|
||||||
name = 'PUK1'
|
name = 'PUK1'
|
||||||
@@ -566,6 +687,20 @@ class Pin(DecimalHexParam):
|
|||||||
raise ValueError('input template UPP has unexpected structure:'
|
raise ValueError('input template UPP has unexpected structure:'
|
||||||
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference}')
|
+ 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):
|
class Pin1(Pin):
|
||||||
is_abstract = False
|
is_abstract = False
|
||||||
name = 'PIN1'
|
name = 'PIN1'
|
||||||
@@ -589,6 +724,14 @@ class Pin2(Pin1):
|
|||||||
raise ValueError('input template UPP has unexpected structure:'
|
raise ValueError('input template UPP has unexpected structure:'
|
||||||
+ f' {cls.get_name()} cannot find pinCode with keyReference={cls.keyReference} in {naa=}')
|
+ 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):
|
class Adm1(Pin):
|
||||||
is_abstract = False
|
is_abstract = False
|
||||||
name = 'ADM1'
|
name = 'ADM1'
|
||||||
@@ -615,6 +758,22 @@ class AlgoConfig(ConfigurableParameter):
|
|||||||
raise ValueError('input template UPP has unexpected structure:'
|
raise ValueError('input template UPP has unexpected structure:'
|
||||||
f' {cls.__name__} cannot find algoParameter with key={cls.algo_config_key}')
|
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 len(algoConfiguration) < 2:
|
||||||
|
continue
|
||||||
|
if algoConfiguration[0] != 'algoParameter':
|
||||||
|
continue
|
||||||
|
if not algoConfiguration[1]:
|
||||||
|
continue
|
||||||
|
val = algoConfiguration[1].get(cls.algo_config_key, None)
|
||||||
|
if val is None:
|
||||||
|
continue
|
||||||
|
yield algoConfiguration[1][cls.algo_config_key]
|
||||||
|
|
||||||
|
|
||||||
class AlgorithmID(DecimalParam, AlgoConfig):
|
class AlgorithmID(DecimalParam, AlgoConfig):
|
||||||
is_abstract = False
|
is_abstract = False
|
||||||
algo_config_key = 'algorithmID'
|
algo_config_key = 'algorithmID'
|
||||||
|
|||||||
Reference in New Issue
Block a user