laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/41779?usp=email )
Change subject: personalization: refactor SmspTpScAddr
......................................................................
personalization: refactor SmspTpScAddr
Refactor SmspTpScAddr to the new ConfigurableParameter implementation
style.
Change-Id: I2600369e195e9f5aed7f4e6ff99ae273ed3ab3bf
---
M pySim/esim/saip/personalization.py
1 file changed, 19 insertions(+), 8 deletions(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index 8415d64..75a43aa 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -338,10 +338,17 @@
class SmspTpScAddr(ConfigurableParameter):
"""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
- presence or absence of leading +"""
+ presence or absence of leading +."""
+ allow_chars = '+0123456789'
+ strip_chars = ' \t\r\n'
+ max_len = 21 # '+' and 20 digits
+ min_len = 1
- def validate(self):
- addr_str = str(self.input_value)
+
+ @classmethod
+ def validate_val(cls, val):
+ val = super().validate_val(val)
+ addr_str = str(val)
if addr_str[0] == '+':
digits = addr_str[1:]
international = True
@@ -349,13 +356,17 @@
digits = addr_str
international = False
if len(digits) > 20:
- raise ValueError('TP-SC-ADDR must not exceed 20 digits')
+ raise ValueError(f'TP-SC-ADDR must not exceed 20 digits: {digits!r}')
if not digits.isdecimal():
- raise ValueError('TP-SC-ADDR must only contain decimal digits')
- self.value = (international, digits)
+ raise ValueError(f'TP-SC-ADDR must only contain decimal digits: {digits!r}')
+ return (international, digits)
- def apply(self, pes: ProfileElementSequence):
- international, digits = self.value
+ @classmethod
+ 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'):
# obtain the File instance from the ProfileElementUSIM
f_smsp = pe.files['ef-smsp']
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/41779?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I2600369e195e9f5aed7f4e6ff99ae273ed3ab3bf
Gerrit-Change-Number: 41779
Gerrit-PatchSet: 5
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/39747?usp=email )
Change subject: personalization: refactor SdKey
......................................................................
personalization: refactor SdKey
Refactor SdKey (and subclasses) to the new ConfigurableParameter
implementation style, keeping the same implementation.
But duly note that this implementation does not work!
It correctly patches pe.decoded[], but that gets overridden by
ProfileElementSD._pre_encode().
For a fix, see I07dfc378705eba1318e9e8652796cbde106c6a52.
Change-Id: I427ea851bfa28b2b045e70a19a9e35d361f0d393
---
M pySim/esim/saip/personalization.py
1 file changed, 13 insertions(+), 20 deletions(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index c715118..8415d64 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -378,44 +378,37 @@
# re-generate the pe.decoded member from the File instance
pe.file2pe(f_smsp)
-class SdKey(ConfigurableParameter, metaclass=ClassVarMeta):
+class SdKey(BinaryParam, metaclass=ClassVarMeta):
"""Configurable Security Domain (SD) Key. Value is presented as bytes."""
- # these will be set by derived classes
+ # these will be set by subclasses
key_type = None
key_id = None
kvn = None
key_usage_qual = None
- permitted_len = []
- def validate(self):
- if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
- raise ValueError('Value must be of bytes-like type')
- if self.permitted_len:
- if len(self.input_value) not in self.permitted_len:
- raise ValueError('Value length must be %s' % self.permitted_len)
- self.value = self.input_value
-
- def _apply_sd(self, pe: ProfileElement):
+ @classmethod
+ def _apply_sd(cls, pe: ProfileElement, value):
assert pe.type == 'securityDomain'
for key in pe.decoded['keyList']:
- if key['keyIdentifier'][0] == self.key_id and key['keyVersionNumber'][0] == self.kvn:
+ if key['keyIdentifier'][0] == cls.key_id and key['keyVersionNumber'][0] == cls.kvn:
assert len(key['keyComponents']) == 1
- key['keyComponents'][0]['keyData'] = self.value
+ key['keyComponents'][0]['keyData'] = value
return
# Could not find matching key to patch, create a new one
key = {
- 'keyUsageQualifier': bytes([self.key_usage_qual]),
- 'keyIdentifier': bytes([self.key_id]),
- 'keyVersionNumber': bytes([self.kvn]),
+ 'keyUsageQualifier': bytes([cls.key_usage_qual]),
+ 'keyIdentifier': bytes([cls.key_id]),
+ 'keyVersionNumber': bytes([cls.kvn]),
'keyComponents': [
- { 'keyType': bytes([self.key_type]), 'keyData': self.value },
+ { 'keyType': bytes([cls.key_type]), 'keyData': value },
]
}
pe.decoded['keyList'].append(key)
- def apply(self, pes: ProfileElementSequence):
+ @classmethod
+ def apply_val(cls, pes: ProfileElementSequence, value):
for pe in pes.get_pes_for_type('securityDomain'):
- self._apply_sd(pe)
+ cls._apply_sd(pe, value)
class SdKeyScp80_01(SdKey, kvn=0x01, key_type=0x88, permitted_len=[16,24,32]): # AES key type
pass
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/39747?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I427ea851bfa28b2b045e70a19a9e35d361f0d393
Gerrit-Change-Number: 39747
Gerrit-PatchSet: 15
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-CC: dexter <pmaier(a)sysmocom.de>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/39745?usp=email )
Change subject: personalization: refactor AlgorithmID, K, Opc
......................................................................
personalization: refactor AlgorithmID, K, Opc
Refactor AlgorithmID, K, Opc to the new ConfigurableParameter
implementation style.
K and Opc use a common abstract BinaryParam.
Note from the future: AlgorithmID so far takes "raw" int values, but
will turn to be an "enum" parameter with predefined meaningful strings
in I71c2ec1b753c66cb577436944634f32792353240
Change-Id: I6296fdcfd5d2ed313c4aade57ff43cc362375848
---
M pySim/esim/saip/personalization.py
1 file changed, 98 insertions(+), 40 deletions(-)
Approvals:
Jenkins Builder: Verified
laforge: Looks good to me, approved
neels: Looks good to me, but someone else must approve
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index d1998dc..c715118 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -254,6 +254,51 @@
# a DecimalHexParam subclass expects the apply_val() input to be a bytes instance ready for the pes
return h2b(val)
+class IntegerParam(ConfigurableParameter):
+ allow_types = (str, int)
+ allow_chars = '0123456789'
+
+ # two integers, if the resulting int should be range limited
+ min_val = None
+ max_val = None
+
+ @classmethod
+ def validate_val(cls, val):
+ val = super().validate_val(val)
+ val = int(val)
+ exceeds_limits = False
+ if cls.min_val is not None:
+ if val < cls.min_val:
+ exceeds_limits = True
+ if cls.max_val is not None:
+ if val > cls.max_val:
+ exceeds_limits = True
+ if exceeds_limits:
+ raise ValueError(f'Value {val} is out of range, must be [{cls.min_val}..{cls.max_val}]')
+ return val
+
+class BinaryParam(ConfigurableParameter):
+ allow_types = (str, io.BytesIO, bytes, bytearray)
+ allow_chars = '0123456789abcdefABCDEF'
+ strip_chars = ' \t\r\n'
+
+ @classmethod
+ def validate_val(cls, val):
+ # take care that min_len and max_len are applied to the binary length by converting to bytes first
+ if isinstance(val, str):
+ if cls.strip_chars is not None:
+ val = ''.join(c for c in val if c not in cls.strip_chars)
+ if len(val) & 1:
+ raise ValueError('Invalid hexadecimal string, must have even number of digits:'
+ f' {val!r} {len(val)=}')
+ try:
+ val = h2b(val)
+ except ValueError as e:
+ raise ValueError(f'Invalid hexadecimal string: {val!r} {len(val)=}') from e
+
+ val = super().validate_val(val)
+ return bytes(val)
+
class Iccid(DecimalParam):
"""ICCID Parameter. Input: string of decimal digits.
@@ -513,42 +558,60 @@
class Adm2(Pin):
keyReference = 0x0B
+class AlgoConfig(ConfigurableParameter):
+ algo_config_key = None
-class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
- """Configurable Algorithm parameter."""
- key = None
- def validate(self):
- if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
- raise ValueError('Value must be of bytes-like type')
- self.value = self.input_value
- def apply(self, pes: ProfileElementSequence):
+ @classmethod
+ def apply_val(cls, pes: ProfileElementSequence, val):
+ found = 0
for pe in pes.get_pes_for_type('akaParameter'):
algoConfiguration = pe.decoded['algoConfiguration']
if algoConfiguration[0] != 'algoParameter':
continue
- algoConfiguration[1][self.key] = self.value
+ algoConfiguration[1][cls.algo_config_key] = val
+ found += 1
+ if not found:
+ raise ValueError('input template UPP has unexpected structure:'
+ f' {cls.__name__} cannot find algoParameter with key={cls.algo_config_key}')
-class K(AlgoConfig, key='key'):
- pass
-class Opc(AlgoConfig, key='opc'):
- pass
-class AlgorithmID(AlgoConfig, key='algorithmID'):
- def validate(self):
- if self.input_value not in [1, 2, 3]:
- raise ValueError('Invalid algorithmID %s' % (self.input_value))
- self.value = self.input_value
-class MilenageRotationConstants(AlgoConfig, key='rotationConstants'):
+class AlgorithmID(DecimalParam, AlgoConfig):
+ algo_config_key = 'algorithmID'
+ allow_len = 1
+
+ @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
+
+class K(BinaryParam, AlgoConfig):
+ """use validate_val() from BinaryParam, and apply_val() from AlgoConfig"""
+ algo_config_key = 'key'
+ allow_len = (128 // 8, 256 // 8) # length in bytes (from BinaryParam); TUAK also allows 256 bit
+
+class Opc(K):
+ algo_config_key = 'opc'
+
+class MilenageRotationConstants(BinaryParam, AlgoConfig):
"""rotation constants r1,r2,r3,r4,r5 of Milenage, Range 0..127. See 3GPP TS 35.206 Sections 2.3 + 5.3.
Provided as octet-string concatenation of all 5 constants. Expects a bytes-like object of length 5, with
each byte in the range of 0..127. The default value by 3GPP is '4000204060' (hex notation)"""
- def validate(self):
- super().validate()
- if len(self.input_value) != 5:
- raise ValueError('Length of value must be 5 octets')
- for r in self.input_value:
- if r > 127:
- raise ValueError('r values must be between 0 and 127')
-class MilenageXoringConstants(AlgoConfig, key='xoringConstants'):
+ algo_config_key = 'rotationConstants'
+ allow_len = 5 # length in bytes (from BinaryParam)
+
+ @classmethod
+ def validate_val(cls, val):
+ "allow_len checks the length, this in addition checks the value range"
+ val = super().validate_val(val)
+ assert isinstance(val, bytes)
+ if any(r > 127 for r in val):
+ raise ValueError('r values must be in the range 0..127')
+ return val
+
+class MilenageXoringConstants(BinaryParam, AlgoConfig):
"""XOR-ing constants c1,c2,c3,c4,c5 of Milenage, 128bit each. See 3GPP TS 35.206 Sections 2.3 + 5.3.
Provided as octet-string concatenation of all 5 constants. The default value by 3GPP is the concetenation
of:
@@ -558,16 +621,11 @@
00000000000000000000000000000004
00000000000000000000000000000008
"""
- def validate(self):
- super().validate()
- if len(self.input_value) != 80:
- raise ValueError('Length of value must be 80 octets')
-class TuakNumberOfKeccak(AlgoConfig, key='numberOfKeccak'):
- """Number of iterations of Keccak-f[1600] permutation as recomended by Section 7.2 of 3GPP TS 35.231.
- The default value by 3GPP is 1."""
- def validate(self):
- if not isinstance(self.input_value, int):
- raise ValueError('Value must be an integer')
- if self.input_value < 1 or self.input_value > 255:
- raise ValueError('Value must be an integer between 1 and 255')
- self.value = self.input_value
+ algo_config_key = 'xoringConstants'
+ allow_len = 80 # length in bytes (from BinaryParam)
+
+class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
+ """Number of iterations of Keccak-f[1600] permutation as recomended by Section 7.2 of 3GPP TS 35.231"""
+ algo_config_key = 'numberOfKeccak'
+ min_val = 1
+ max_val = 255
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/39745?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I6296fdcfd5d2ed313c4aade57ff43cc362375848
Gerrit-Change-Number: 39745
Gerrit-PatchSet: 15
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: neels <nhofmeyr(a)sysmocom.de>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/39742?usp=email )
Change subject: personalization: refactor ConfigurableParameter, Iccid, Imsi
......................................................................
personalization: refactor ConfigurableParameter, Iccid, Imsi
Main points/rationales of the refactoring, details below:
1) common validation implementation
2) offer classmethods
The new features are optional, and will be heavily used by batch
personalization patches coming soon.
Implement Iccid and Imsi to use the new way, with a common abstract
DecimalParam implementation.
So far leave the other parameter classes working as they always did, to
follow suit in subsequent commits.
Details:
1) common validation implementation:
There are very common validation steps in the various parameter
implementations. It is more convenient and much more readable to
implement those once and set simple validation parameters per subclass.
So there now is a validate_val() classmethod, which subclasses can use
as-is to apply the validation parameters -- or subclasses can override
their cls.validate_val() for specialized validation.
(Those subclasses that this patch doesn't touch still override the
self.validate() instance method. Hence they still work as before this
patch, but don't use the new common features yet.)
2) offer stateless classmethods:
It is useful for...
- batch processing of multiple profiles (in upcoming patches) and
- user input validation
to be able to have classmethods that do what self.validate() and
self.apply() do, but do not modify any self.* members.
So far the paradigm was to create a class instance to keep state about
the value. This remains available, but in addition we make available the
paradigm of a singleton that is stateless (the classmethods).
Using self.validate() and self.apply() still work the same as before
this patch, i.e. via self.input_value and self.value -- but in addition,
there are now classmethods that don't touch self.* members.
Related: SYS#6768
Change-Id: I6522be4c463e34897ca9bff2309b3706a88b3ce8
---
M pySim/esim/saip/personalization.py
1 file changed, 199 insertions(+), 35 deletions(-)
Approvals:
laforge: Looks good to me, approved
Jenkins Builder: Verified
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index 3a77990..ecc0b76 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -47,56 +47,220 @@
return x
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
- """Base class representing a part of the eSIM profile that is configurable during the
- personalization process (with dynamic data from elsewhere)."""
- def __init__(self, input_value):
+ r"""Base class representing a part of the eSIM profile that is configurable during the
+ personalization process (with dynamic data from elsewhere).
+
+ This class is abstract, you will only use subclasses in practice.
+
+ Subclasses have to implement the apply_val() classmethods, and may choose to override the default validate_val()
+ implementation.
+ The default validate_val() is a generic validator that uses the following class members (defined in subclasses) to
+ configure the validation; if any of them is None, it means that the particular validation is skipped:
+
+ allow_types: a list of types permitted as argument to validate_val(); allow_types = (bytes, str,)
+ allow_chars: if val is a str, accept only these characters; allow_chars = "0123456789"
+ strip_chars: if val is a str, remove these characters; strip_chars = ' \t\r\n'
+ min_len: minimum length of an input str; min_len = 4
+ max_len: maximum length of an input str; max_len = 8
+ allow_len: permit only specific lengths; allow_len = (8, 16, 32)
+
+ Subclasses may change the meaning of these by overriding validate_val(), for example that the length counts
+ resulting bytes instead of a hexstring length. Most subclasses will be covered by the default validate_val().
+
+ Usage examples, by example of Iccid:
+
+ 1) use a ConfigurableParameter instance, with .input_value and .value state:
+
+ iccid = Iccid()
+ try:
+ iccid.input_value = '123456789012345678'
+ iccid.validate()
+ except ValueError:
+ print(f"failed to validate {iccid.name} == {iccid.input_value}")
+
+ pes = ProfileElementSequence.from_der(der_data_from_file)
+ try:
+ iccid.apply(pes)
+ except ValueError:
+ print(f"failed to apply {iccid.name} := {iccid.input_value}")
+
+ changed_der = pes.to_der()
+
+ 2) use a ConfigurableParameter class, without state:
+
+ cls = Iccid
+ input_val = '123456789012345678'
+
+ try:
+ clean_val = cls.validate_val(input_val)
+ except ValueError:
+ print(f"failed to validate {cls.get_name()} = {input_val}")
+
+ pes = ProfileElementSequence.from_der(der_data_from_file)
+ try:
+ cls.apply_val(pes, clean_val)
+ except ValueError:
+ print(f"failed to apply {cls.get_name()} = {input_val}")
+
+ changed_der = pes.to_der()
+ """
+
+ # A subclass can set an explicit string as name (like name = "PIN1").
+ # If name is left None, then __init__() will set self.name to a name derived from the python class name (like
+ # "pin1"). See also the get_name() classmethod when you have no instance at hand.
+ name = None
+ allow_types = (str, int, )
+ allow_chars = None
+ strip_chars = None
+ min_len = None
+ max_len = None
+ allow_len = None # a list of specific lengths
+
+ def __init__(self, input_value=None):
self.input_value = input_value # the raw input value as given by caller
self.value = None # the processed input value (e.g. with check digit) as produced by validate()
- def validate(self):
- """Optional validation method. Can be used by derived classes to perform validation
- of the input value (self.value). Will raise an exception if validation fails."""
- # default implementation: simply copy input_value over to value
- self.value = self.input_value
+ # if there is no explicit name string set, use the class name
+ self.name = self.get_name()
- @abc.abstractmethod
+ @classmethod
+ def get_name(cls):
+ """Return cls.name when it is set, otherwise return the python class name converted from 'CamelCase' to
+ 'snake_case'.
+ When using class *instances*, you can just use my_instance.name.
+ When using *classes*, cls.get_name() returns the same name a class instance would have.
+ """
+ if cls.name:
+ return cls.name
+ return camel_to_snake(cls.__name__)
+
+ def validate(self):
+ """Validate self.input_value and place the result in self.value.
+ This is also called implicitly by apply(), if self.value is still None.
+ To override validation in a subclass, rather re-implement the classmethod validate_val()."""
+ try:
+ self.value = self.__class__.validate_val(self.input_value)
+ except (TypeError, ValueError, KeyError) as e:
+ raise ValueError(f'{self.name}: {e}') from e
+
def apply(self, pes: ProfileElementSequence):
+ """Place self.value into the ProfileElementSequence at the right place.
+ If self.value is None, this implicitly calls self.validate() first, to generate a sanitized self.value from
+ self.input_value.
+ To override apply() in a subclass, rather override the classmethod apply_val()."""
+ if self.value is None:
+ self.validate()
+ assert self.value is not None
+ try:
+ self.__class__.apply_val(pes, self.value)
+ except (TypeError, ValueError, KeyError) as e:
+ raise ValueError(f'{self.name}: {e}') from e
+
+ @classmethod
+ def validate_val(cls, val):
+ """This is a default implementation, with the behavior configured by subclasses' allow_types...max_len settings.
+ subclasses may override this function:
+ Validate the contents of val, and raise ValueError on validation errors.
+ Return a sanitized version of val, that is ready for cls.apply_val().
+ """
+
+ if cls.allow_types is not None:
+ if not isinstance(val, cls.allow_types):
+ raise ValueError(f'input value must be one of {cls.allow_types}, not {type(val)}')
+ elif val is None:
+ raise ValueError('there is no value (val is None)')
+
+ if isinstance(val, str):
+ if cls.strip_chars is not None:
+ val = ''.join(c for c in val if c not in cls.strip_chars)
+ if cls.allow_chars is not None:
+ if any(c not in cls.allow_chars for c in val):
+ raise ValueError(f"invalid characters in input value {val!r}, valid chars are {cls.allow_chars}")
+ if cls.allow_len is not None:
+ l = cls.allow_len
+ # cls.allow_len could be one int, or a tuple of ints. Wrap a single int also in a tuple.
+ if not isinstance(l, (tuple, list)):
+ l = (l,)
+ if len(val) not in l:
+ raise ValueError(f'length must be one of {cls.allow_len}, not {len(val)}: {val!r}')
+ if cls.min_len is not None:
+ if len(val) < cls.min_len:
+ raise ValueError(f'length must be at least {cls.min_len}, not {len(val)}: {val!r}')
+ if cls.max_len is not None:
+ if len(val) > cls.max_len:
+ raise ValueError(f'length must be at most {cls.max_len}, not {len(val)}: {val!r}')
+ return val
+
+ @classmethod
+ def apply_val(cls, pes: ProfileElementSequence, val):
+ """This is what subclasses implement: store a value in a decoded profile package.
+ Write the given val in the right format in all the right places in pes."""
pass
-class Iccid(ConfigurableParameter):
- """Configurable ICCID. Expects the value to be a string of decimal digits.
- If the string of digits is only 18 digits long, a Luhn check digit will be added."""
+ @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
+ digits, this function is useful to easily get that minimum permitted length.
+ """
+ vals = []
+ if cls.allow_len is not None:
+ if isinstance(cls.allow_len, (tuple, list)):
+ vals.extend(cls.allow_len)
+ else:
+ vals.append(cls.allow_len)
+ if cls.min_len is not None:
+ vals.append(cls.min_len)
+ if cls.max_len is not None:
+ vals.append(cls.max_len)
+ if not vals:
+ return (None, None)
+ return (min(vals), max(vals))
- def validate(self):
- # convert to string as it might be an integer
- iccid_str = str(self.input_value)
- if len(iccid_str) < 18 or len(iccid_str) > 20:
- raise ValueError('ICCID must be 18, 19 or 20 digits long')
- if not iccid_str.isdecimal():
- raise ValueError('ICCID must only contain decimal digits')
- self.value = sanitize_iccid(iccid_str)
- def apply(self, pes: ProfileElementSequence):
+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.
+ """
+ allow_types = (str, int)
+ allow_chars = '0123456789'
+
+ @classmethod
+ def validate_val(cls, val):
+ if isinstance(val, int):
+ min_len, max_len = cls.get_len_range()
+ l = min_len or 1
+ val = '%0*d' % (l, val)
+ return super().validate_val(val)
+
+
+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."""
+ min_len = 18
+ max_len = 20
+
+ @classmethod
+ def validate_val(cls, val):
+ iccid_str = super().validate_val(val)
+ return sanitize_iccid(iccid_str)
+
+ @classmethod
+ def apply_val(cls, pes: ProfileElementSequence, val):
# patch the header
- pes.get_pe_for_type('header').decoded['iccid'] = h2b(rpad(self.value, 20))
+ pes.get_pe_for_type('header').decoded['iccid'] = h2b(rpad(val, 20))
# patch MF/EF.ICCID
- file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(self.value)))
+ file_replace_content(pes.get_pe_for_type('mf').decoded['ef-iccid'], h2b(enc_iccid(val)))
-class Imsi(ConfigurableParameter):
+class Imsi(DecimalParam):
"""Configurable IMSI. Expects value to be a string of digits. Automatically sets the ACC to
the last digit of the IMSI."""
+ min_len = 6
+ max_len = 15
- def validate(self):
- # convert to string as it might be an integer
- imsi_str = str(self.input_value)
- if len(imsi_str) < 6 or len(imsi_str) > 15:
- raise ValueError('IMSI must be 6..15 digits long')
- if not imsi_str.isdecimal():
- raise ValueError('IMSI must only contain decimal digits')
- self.value = imsi_str
-
- def apply(self, pes: ProfileElementSequence):
- imsi_str = self.value
+ @classmethod
+ def apply_val(cls, pes: ProfileElementSequence, val):
+ imsi_str = val
# we always use the least significant byte of the IMSI as ACC
acc = (1 << int(imsi_str[-1]))
# patch ADF.USIM/EF.IMSI
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/39742?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I6522be4c463e34897ca9bff2309b3706a88b3ce8
Gerrit-Change-Number: 39742
Gerrit-PatchSet: 19
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de>
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: neels <nhofmeyr(a)sysmocom.de>
Gerrit-CC: pespin <pespin(a)sysmocom.de>
laforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/39743?usp=email )
Change subject: personalization: refactor Puk
......................................................................
personalization: refactor Puk
Implement abstract DecimalHexParam, and use it to refactor Puk1 and Puk2
to the new ConfigurableParameter implementation style.
DecimalHexParam will also be used for Pin and Adm soon.
Change-Id: I271e6c030c890778ab7af9ab3bc7997e22018f6a
---
M pySim/esim/saip/personalization.py
1 file changed, 39 insertions(+), 20 deletions(-)
Approvals:
laforge: Looks good to me, approved
Jenkins Builder: Verified
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index ecc0b76..462add6 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -234,6 +234,27 @@
return super().validate_val(val)
+class DecimalHexParam(DecimalParam):
+ """The input value is decimal digits. The decimal value is stored such that each hexadecimal digit represents one
+ decimal digit, useful for various PIN type parameters.
+
+ Optionally, the value is stored with padding, for example: rpad = 8 would store '123' as '123fffff'. This is also
+ common in PIN type parameters.
+ """
+ rpad = None
+ rpad_char = 'f'
+
+ @classmethod
+ def validate_val(cls, val):
+ val = super().validate_val(val)
+ val = ''.join('%02x' % ord(x) for x in val)
+ if cls.rpad is not None:
+ c = cls.rpad_char
+ val = rpad(val, cls.rpad, c)
+ # a DecimalHexParam subclass expects the apply_val() input to be a bytes instance ready for the pes
+ return h2b(val)
+
+
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."""
@@ -415,34 +436,32 @@
filtered = list(filter(lambda x: x.type == wanted_type, l))
return filtered[0]
-class Puk(ConfigurableParameter, metaclass=ClassVarMeta):
+class Puk(DecimalHexParam):
"""Configurable PUK (Pin Unblock Code). String ASCII-encoded digits."""
+ allow_len = 8
+ rpad = 16
keyReference = None
- def validate(self):
- if isinstance(self.input_value, int):
- self.value = '%08d' % self.input_value
- else:
- self.value = self.input_value
- # FIXME: valid length?
- if not self.value.isdecimal():
- raise ValueError('PUK must only contain decimal digits')
- def apply(self, pes: ProfileElementSequence):
- puk = ''.join(['%02x' % (ord(x)) for x in self.value])
- padded_puk = rpad(puk, 16)
+ @classmethod
+ def apply_val(cls, pes: ProfileElementSequence, val):
+ val_bytes = val
mf_pes = pes.pes_by_naa['mf'][0]
pukCodes = obtain_singleton_pe_from_pelist(mf_pes, 'pukCodes')
for pukCode in pukCodes.decoded['pukCodes']:
- if pukCode['keyReference'] == self.keyReference:
- pukCode['pukValue'] = h2b(padded_puk)
+ if pukCode['keyReference'] == cls.keyReference:
+ pukCode['pukValue'] = val_bytes
return
- raise ValueError('cannot find pukCode')
-class Puk1(Puk, keyReference=0x01):
- pass
-class Puk2(Puk, keyReference=0x81):
- pass
+ raise ValueError("input template UPP has unexpected structure:"
+ f" cannot find pukCode with keyReference={cls.keyReference}")
-class Pin(ConfigurableParameter, metaclass=ClassVarMeta):
+class Puk1(Puk):
+ keyReference = 0x01
+
+class Puk2(Puk):
+ keyReference = 0x81
+
+
+class Pin(ConfigurableParameter,metaclass=ClassVarMeta):
"""Configurable PIN (Personal Identification Number). String of digits."""
keyReference = None
def validate(self):
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/39743?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I271e6c030c890778ab7af9ab3bc7997e22018f6a
Gerrit-Change-Number: 39743
Gerrit-PatchSet: 15
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Attention is currently required from: neels.
laforge has posted comments on this change by neels. ( https://gerrit.osmocom.org/c/pysim/+/40094?usp=email )
Change subject: personalization: set example input values
......................................................................
Patch Set 11: Code-Review+2
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40094?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: comment
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I2672fedcbc32cb7a6cb0c233a4a22112bd9aae03
Gerrit-Change-Number: 40094
Gerrit-PatchSet: 11
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Attention: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Comment-Date: Fri, 30 Jan 2026 19:34:06 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
Attention is currently required from: neels.
laforge has posted comments on this change by neels. ( https://gerrit.osmocom.org/c/pysim/+/40093?usp=email )
Change subject: personalization: set some typical parameter names
......................................................................
Patch Set 11: Code-Review+2
--
To view, visit https://gerrit.osmocom.org/c/pysim/+/40093?usp=email
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: comment
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I31f390d634e58c384589c50a33ca45d6f86d4e10
Gerrit-Change-Number: 40093
Gerrit-PatchSet: 11
Gerrit-Owner: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Attention: neels <nhofmeyr(a)sysmocom.de>
Gerrit-Comment-Date: Fri, 30 Jan 2026 19:33:53 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes