neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/40203?usp=email )
Change subject: personalization: fix SdKey.apply_val() implementation
......................................................................
personalization: fix SdKey.apply_val() implementation
'securityDomain' elements are decoded to ProfileElementSD instances,
which keep higher level representations of the key data apart from the
decoded[] lists.
So far, apply_val() was dropping binary values in decoded[], which does
not work, because ProfileElementSD._pre_encode() overwrites
self.decoded[] from the higher level representation.
Implement using
- ProfileElementSD.find_key() and SecurityDomainKeyComponent to modify
an exsiting entry, or
- ProfileElementSD.add_key() to create a new entry.
Before this patch, SdKey parameters seemed to patch PES successfully,
but their modifications did not end up in the encoded DER.
(BTW, this does not fix any other errors that may still be present in
the various SdKey subclasses, patches coming up.)
Related: SYS#6768
Change-Id: I07dfc378705eba1318e9e8652796cbde106c6a52
---
M pySim/esim/saip/__init__.py
M pySim/esim/saip/personalization.py
2 files changed, 41 insertions(+), 27 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/03/40203/1
diff --git a/pySim/esim/saip/__init__.py b/pySim/esim/saip/__init__.py
index 21f44ef..0b3d302 100644
--- a/pySim/esim/saip/__init__.py
+++ b/pySim/esim/saip/__init__.py
@@ -991,6 +991,13 @@
'keyVersionNumber': bytes([self.key_version_number]),
'keyComponents': [k.to_saip_dict() for k in self.key_components]}
+ def get_key_component(self, key_type):
+ for kc in self.key_components:
+ if kc.key_type == key_type:
+ return kc.key_data
+ return None
+
+
class ProfileElementSD(ProfileElement):
"""Class representing a securityDomain ProfileElement."""
type = 'securityDomain'
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index e3ded68..0eb0671 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -24,8 +24,10 @@
from osmocom.tlv import camel_to_snake
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
+from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence
+from pySim.esim.saip import SecurityDomainKey, SecurityDomainKeyComponent
+from pySim.global_platform import KeyUsageQualifier, KeyType
def unrpad(s: hexstr, c='f') -> hexstr:
return hexstr(s.rstrip(c))
@@ -498,36 +500,41 @@
default_source = param_source.RandomHexDigitSource
@classmethod
- def _apply_sd(cls, pe: ProfileElement, value):
- assert pe.type == 'securityDomain'
- for key in pe.decoded['keyList']:
- if key['keyIdentifier'][0] == cls.key_id and key['keyVersionNumber'][0] == cls.kvn:
- assert len(key['keyComponents']) == 1
- key['keyComponents'][0]['keyData'] = value
- return
- # Could not find matching key to patch, create a new one
- key = {
- 'keyUsageQualifier': bytes([cls.key_usage_qual]),
- 'keyIdentifier': bytes([cls.key_id]),
- 'keyVersionNumber': bytes([cls.kvn]),
- 'keyComponents': [
- { 'keyType': bytes([cls.key_type]), 'keyData': value },
- ]
- }
- pe.decoded['keyList'].append(key)
+ def apply_val(cls, pes: ProfileElementSequence, val):
+ set_components = [ SecurityDomainKeyComponent(cls.key_type, val) ]
- @classmethod
- def apply_val(cls, pes: ProfileElementSequence, value):
- for pe in pes.get_pes_for_type('securityDomain'):
- cls._apply_sd(pe, value)
+ for pe in pes.pe_list:
+ if pe.type != 'securityDomain':
+ continue
+ assert isinstance(pe, ProfileElementSD)
+
+ key = pe.find_key(key_version_number=cls.kvn, key_id=cls.key_id)
+ if not key:
+ # Could not find matching key to patch, create a new one
+ key = SecurityDomainKey(
+ key_version_number=cls.kvn,
+ key_id=cls.key_id,
+ key_usage_qualifier=KeyUsageQualifier.build(cls.key_usage_qual),
+ key_components=set_components,
+ )
+ pe.add_key(key)
+ else:
+ print(f'{key.key_usage_qualifier=!r}')
+ key.key_components = set_components
@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 { cls.name: b2h(key['keyComponents'][0]['keyData']) }
+ for pe in pes.pe_list:
+ if pe.type != 'securityDomain':
+ continue
+ assert isinstance(pe, ProfileElementSD)
+
+ key = pe.find_key(key_version_number=cls.kvn, key_id=cls.key_id)
+ if not key:
+ continue
+ kc = key.get_key_component(cls.key_type)
+ if kc:
+ yield { cls.name: b2h(kc) }
class SdKeyScp80_01(SdKey):
kvn = 0x01
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40203?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: I07dfc378705eba1318e9e8652796cbde106c6a52
Gerrit-Change-Number: 40203
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/40201?usp=email )
Change subject: personalization: make AlgorithmID a new EnumParam
......................................................................
personalization: make AlgorithmID a new EnumParam
The AlgorithmID has a few preset values, and hardly anyone knows which
is which. So instead of entering '1', '2' or '3', make it work with
prededined values 'Milenage', 'TUAK' and 'usim-test'.
Implement the enum value part abstractly in new EnumParam.
Make AlgorithmID a subclass of EnumParam and define the values as from
pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
Related: SYS#6768
Change-Id: I71c2ec1b753c66cb577436944634f32792353240
---
M pySim/esim/saip/personalization.py
1 file changed, 92 insertions(+), 12 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/01/40201/1
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index 317cac5..1ce420a 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -18,6 +18,7 @@
import abc
import io
import copy
+import re
from typing import List, Tuple, Generator, Optional
from osmocom.tlv import camel_to_snake
@@ -338,6 +339,70 @@
return bytes(val)
+class EnumParam(ConfigurableParameter):
+ value_map = {
+ # For example:
+ #'Meaningful label for value 23': 0x23,
+ # Where 0x23 is a valid value to use for apply_val().
+ }
+ _value_map_reverse = None
+
+ @classmethod
+ def validate_val(cls, val):
+ orig_val = val
+ enum_val = None
+ if isinstance(val, str):
+ enum_name = val
+ enum_val = cls.map_name_to_val(enum_name)
+
+ # if the str is not one of the known value_map.keys(), is it maybe one of value_map.keys()?
+ if enum_val is None and val in cls.value_map.values():
+ enum_val = val
+
+ if enum_val not in cls.value_map.values():
+ raise ValueError(f"{cls.get_name()}: invalid argument: {orig_val!r}. Valid arguments are:"
+ f" {', '.join(cls.value_map.keys())}")
+
+ return enum_val
+
+ @classmethod
+ def map_name_to_val(cls, name:str, strict=True):
+ val = cls.value_map.get(name)
+ if val is not None:
+ return val
+
+ clean_name = cls.clean_name_str(name)
+ for k, v in cls.value_map.items():
+ if clean_name == cls.clean_name_str(k):
+ return v
+
+ if strict:
+ raise ValueError(f"Problem in {cls.get_name()}: {name!r} is not a known value."
+ f" Known values are: {cls.value_map.keys()!r}")
+ return None
+
+ @classmethod
+ def map_val_to_name(cls, val, strict=False) -> str:
+ if cls._value_map_reverse is None:
+ cls._value_map_reverse = dict((v, k) for k, v in cls.value_map.items())
+
+ name = cls._value_map_reverse.get(val)
+ if name:
+ return name
+ if strict:
+ raise ValueError(f"Problem in {cls.get_name()}: {val!r} ({type(val)}) is not a known value."
+ f" Known values are: {cls.value_map.values()!r}")
+ return None
+
+ @classmethod
+ def name_normalize(cls, name:str) -> str:
+ return cls.map_val_to_name(cls.map_name_to_val(name))
+
+ @classmethod
+ def clean_name_str(cls, val):
+ return re.sub('[^0-9A-Za-z-_]', '', val).lower()
+
+
class Iccid(DecimalParam):
"""ICCID Parameter. Input: string of decimal digits.
If the string of digits is only 18 digits long, add a Luhn check digit."""
@@ -714,22 +779,37 @@
# if it is an int (algorithmID), just pass thru as int
yield { cls.name: val }
-
-class AlgorithmID(DecimalParam, AlgoConfig):
+class AlgorithmID(EnumParam, AlgoConfig):
+ '''use validate_val() from EnumParam, and apply_val() from AlgoConfig.
+ In get_values_from_pes(), return enum value names, not raw values.'''
is_abstract = False
- algo_config_key = 'algorithmID'
- allow_len = 1
- default_value = 1 # Milenage
+ name = "Algorithm"
+
+ # as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
+ value_map = {
+ "Milenage" : 1,
+ "TUAK" : 2,
+ "usim-test" : 3,
+ }
+ default_value = "Milenage"
default_source = param_source.ConstantSource
+ algo_config_key = 'algorithmID'
+
+ # EnumParam.validate_val() returns the int values from value_map
+
@classmethod
- def validate_val(cls, val):
- val = super().validate_val(val)
- val = int(val)
- valid = (1, 2, 3)
- if val not in valid:
- raise ValueError(f'Invalid algorithmID {val!r}, must be one of {valid}')
- return val
+ def get_values_from_pes(cls, pes: ProfileElementSequence):
+ # return enum names, not raw values.
+ # use of super(): this intends to call AlgoConfig.get_values_from_pes() so that the cls argument is this cls
+ # here (AlgorithmID); i.e. AlgoConfig.get_values_from_pes(pes) doesn't work, because AlgoConfig needs to look up
+ # cls.algo_config_key.
+ for d in super(cls, cls).get_values_from_pes(pes):
+ if cls.name in d:
+ # convert int to value string
+ val = d[cls.name]
+ d[cls.name] = cls.map_val_to_name(val, strict=True)
+ yield d
class K(BinaryParam, AlgoConfig):
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40201?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: I71c2ec1b753c66cb577436944634f32792353240
Gerrit-Change-Number: 40201
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/40202?usp=email )
Change subject: personalization: add get_typical_input_len() to ConfigurableParameter
......................................................................
personalization: add get_typical_input_len() to ConfigurableParameter
The aim is to tell a user interface how wide an input text field should
be chosen to be convenient -- ideally showing the entire value in all
cases, but not too huge for fields that have no sane size limit.
Change-Id: I2568a032167a10517d4d75d8076a747be6e21890
---
M pySim/esim/saip/personalization.py
1 file changed, 18 insertions(+), 1 deletion(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/02/40202/1
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index 1ce420a..e3ded68 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -259,6 +259,14 @@
return (min(vals), max(vals))
@classmethod
+ def get_typical_input_len(cls):
+ '''return a good length to use as the visible width of a user interface input field.
+ May be overridden by subclasses.
+ This default implementation returns the maximum allowed value length -- a good fit for most subclasses.
+ '''
+ return cls.get_len_range()[1] or 16
+
+ @classmethod
def get_all_implementations(cls, blacklist=None, allow_abstract=False):
# return a set() so that multiple inheritance does not return dups
return set(c
@@ -267,7 +275,6 @@
and ((not blacklist) or (c not in blacklist)))
)
-
class DecimalParam(ConfigurableParameter):
"""Decimal digits. The input value may be a string of decimal digits like '012345', or an int. The output of
validate_val() is a string with only decimal digits 0-9, in the required length with leading zeros if necessary.
@@ -338,6 +345,16 @@
val = super().validate_val(val)
return bytes(val)
+ @classmethod
+ def get_typical_input_len(cls):
+ # override to return twice the length, because of hex digits.
+ min_len, max_len = cls.get_len_range()
+ if max_len is None:
+ return None
+ # two hex characters per value octet.
+ # (maybe *3 to also allow for spaces?)
+ return max_len * 2
+
class EnumParam(ConfigurableParameter):
value_map = {
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40202?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: I2568a032167a10517d4d75d8076a747be6e21890
Gerrit-Change-Number: 40202
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/40206?usp=email )
Change subject: esim param_source: add is_abstract flag
......................................................................
esim param_source: add is_abstract flag
Allow omitting some ParamSource subclassed from
ParamSource.get_all_implementations().
My previous attempts at automagically detecting abstract classes failed
conceptually, and the easiest, most explicit way is alrady used in
ConfigurableParameter: add an is_abstract flag.
Prep for Ie7171c152a7b478736f8825050305606b5af5735
Change-Id: Icfccdd0a8ecb5e0e9d22afa490d73c9f1849a64c
---
M pySim/esim/saip/param_source.py
1 file changed, 7 insertions(+), 1 deletion(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/06/40206/1
diff --git a/pySim/esim/saip/param_source.py b/pySim/esim/saip/param_source.py
index ffb03b2..407882d 100644
--- a/pySim/esim/saip/param_source.py
+++ b/pySim/esim/saip/param_source.py
@@ -31,6 +31,7 @@
class ParamSource:
'abstract parameter source'
+ is_abstract = True
# This name should be short but descriptive, useful for a user interface, like 'random decimal digits'.
name = 'none'
@@ -41,7 +42,7 @@
# return a set() so that multiple inheritance does not return dups
return set(c
for c in all_subclasses_of(cls)
- if ((not blacklist) or (c not in blacklist))
+ if (not c.is_abstract) and ((not blacklist) or (c not in blacklist))
)
@classmethod
@@ -60,6 +61,7 @@
class ConstantSource(ParamSource):
'one value for all'
+ is_abstract = False
name = 'constant'
def __init__(self, val:str):
@@ -70,6 +72,7 @@
class RandomDigitSource(ParamSource):
'return a different sequence of random decimal digits each'
+ is_abstract = False
name = 'random decimal digits'
def __init__(self, num_digits, first_value, last_value):
@@ -109,6 +112,7 @@
class RandomHexDigitSource(ParamSource):
'return a different sequence of random hexadecimal digits each'
+ is_abstract = False
name = 'random hexadecimal digits'
def __init__(self, num_digits):
@@ -131,6 +135,7 @@
class IncDigitSource(RandomDigitSource):
'incrementing sequence of digits'
+ is_abstract = False
name = 'incrementing decimal digits'
def __init__(self, *args, **kwargs):
@@ -160,6 +165,7 @@
class CsvSource(ParamSource):
'apply a column from a CSV row, as passed in to ParamSource.get_next(csv_row)'
+ is_abstract = False
name = 'from CSV'
def __init__(self, csv_column):
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40206?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: Icfccdd0a8ecb5e0e9d22afa490d73c9f1849a64c
Gerrit-Change-Number: 40206
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/40199?usp=email )
Change subject: personalization: allow reading back multiple values from PES
......................................................................
personalization: allow reading back multiple values from PES
Change-Id: Iecb68af7c216c6b9dc3add469564416b6f37f7b2
---
M pySim/esim/saip/personalization.py
1 file changed, 26 insertions(+), 18 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/99/40199/1
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index ca37e3a..34d7df9 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -216,26 +216,24 @@
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence) -> Generator:
- '''This is what subclasses implement: yield all values from a decoded profile package.
+ """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:
+ Yielded value must be a dict(). Usually, an implementation will return only one key, like
- 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()
+ { "ICCID": "1234567890123456789" }
+
+ Some implementations have more than one value to return, like
+
+ { "IMSI": "00101012345678", "IMSI-ACC" : "5" }
Implementation example:
for pe in pes:
if my_condition(pe):
- yield b2h(my_bin_value_from(pe))
- '''
+ yield { cls.name: b2h(my_bin_value_from(pe)) }
+ """
pass
@classmethod
@@ -364,12 +362,12 @@
def get_values_from_pes(cls, pes: ProfileElementSequence):
padded = b2h(pes.get_pe_for_type('header').decoded['iccid'])
iccid = unrpad(padded)
- yield iccid
+ yield { cls.name: 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)))
+ yield { cls.name: 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
@@ -396,8 +394,13 @@
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)
+ acc_pe = pe.decoded.get('ef-acc', None)
+ y = {}
if imsi_pe:
- yield dec_imsi(b2h(file_tuples_content_as_bytes(imsi_pe)))
+ y[cls.name] = dec_imsi(b2h(file_tuples_content_as_bytes(imsi_pe)))
+ if acc_pe:
+ y[cls.name + '-ACC'] = b2h(file_tuples_content_as_bytes(acc_pe))
+ yield y
class SdKey(BinaryParam):
@@ -438,7 +441,7 @@
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'])
+ yield { cls.name: b2h(key['keyComponents'][0]['keyData']) }
class SdKeyScp80_01(SdKey):
kvn = 0x01
@@ -582,7 +585,7 @@
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'])
+ yield { cls.name: cls.decimal_hex_to_str(pukCode['pukValue']) }
class Puk1(Puk):
is_abstract = False
@@ -623,13 +626,14 @@
@classmethod
def _read_all_pinvalues_from_pe(cls, pe: ProfileElement):
+ "This is a separate function because subclasses may feed different pe arguments."
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'])
+ yield { cls.name: cls.decimal_hex_to_str(pinCode['pinValue']) }
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
@@ -698,7 +702,11 @@
algoConfiguration = pe.decoded['algoConfiguration']
if algoConfiguration[0] != 'algoParameter':
continue
- yield algoConfiguration[1][cls.algo_config_key]
+ val = algoConfiguration[1][cls.algo_config_key]
+ if isinstance(val, bytes):
+ val = b2h(val)
+ # if it is an int (algorithmID), just pass thru as int
+ yield { cls.name: val }
class AlgorithmID(DecimalParam, AlgoConfig):
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40199?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: Iecb68af7c216c6b9dc3add469564416b6f37f7b2
Gerrit-Change-Number: 40199
Gerrit-PatchSet: 1
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
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 <nhofmeyr(a)sysmocom.de>