Compare commits

..

2 Commits

Author SHA1 Message Date
Philipp Maier
c74d7fbd0d fix
Change-Id: I1af2fd04ba935fc70a2306342eeb247d941c67af
2026-04-15 10:25:06 +02:00
Philipp Maier
d7f4d20471 pySim/transport: fix GET RESPONSE behaviour
The current behavior we implement in the method __send_apdu_T0 is
incomplete. Some details discussed in ETSI TS 102 221,
section 7.3.1.1.4, clause 4 seem to be not fully implemented. We
may also end up sending a GET RESPONSE in other APDU cases than
case 4 (the only case that uses the GET RESPONSE command).

Related: OS#6970
Change-Id: I26f0566af0cdd61dcc97f5f502479dc76adc37cc
2026-04-09 17:55:37 +02:00
16 changed files with 110 additions and 764 deletions

View File

@@ -97,7 +97,7 @@ Please install the following dependencies:
- pyscard - pyscard
- pyserial - pyserial
- pytlv - pytlv
- pyyaml >= 5.4 - pyyaml >= 5.1
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`) - smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
- termcolor - termcolor

View File

@@ -10,11 +10,6 @@
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
setup_venv() {
virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
}
if [ ! -d "./tests/" ] ; then if [ ! -d "./tests/" ] ; then
echo "###############################################" echo "###############################################"
echo "Please call from pySim-prog top directory" echo "Please call from pySim-prog top directory"
@@ -28,7 +23,8 @@ fi
case "$JOB_TYPE" in case "$JOB_TYPE" in
"test") "test")
setup_venv virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
pip install pyshark pip install pyshark
@@ -36,27 +32,23 @@ case "$JOB_TYPE" in
# Execute automatically discovered unit tests first # Execute automatically discovered unit tests first
python -m unittest discover -v -s tests/unittests python -m unittest discover -v -s tests/unittests
# Run pySim-trace test # Run pySim-prog integration tests (requires physical cards)
tests/pySim-trace_test/pySim-trace_test.sh
;;
"card-test") # tests requiring physical cards
setup_venv
pip install -r requirements.txt
# Run pySim-prog integration tests
cd tests/pySim-prog_test/ cd tests/pySim-prog_test/
./pySim-prog_test.sh ./pySim-prog_test.sh
cd ../../ cd ../../
# Run pySim-shell integration tests # Run pySim-trace test
tests/pySim-trace_test/pySim-trace_test.sh
# Run pySim-shell integration tests (requires physical cards)
python3 -m unittest discover -v -s ./tests/pySim-shell_test/ python3 -m unittest discover -v -s ./tests/pySim-shell_test/
# Run pySim-smpp2sim test # Run pySim-smpp2sim test
tests/pySim-smpp2sim_test/pySim-smpp2sim_test.sh tests/pySim-smpp2sim_test/pySim-smpp2sim_test.sh
;; ;;
"distcheck") "distcheck")
setup_venv virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install . pip install .
pip install pyshark pip install pyshark
@@ -69,7 +61,8 @@ case "$JOB_TYPE" in
# Print pylint version # Print pylint version
pip3 freeze | grep pylint pip3 freeze | grep pylint
setup_venv virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install . pip install .
@@ -87,7 +80,8 @@ case "$JOB_TYPE" in
contrib/*.py contrib/*.py
;; ;;
"docs") "docs")
setup_venv virtualenv -p python3 venv --system-site-packages
. venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt

View File

@@ -27,6 +27,7 @@
import hashlib import hashlib
import argparse import argparse
import os import os
import random
import re import re
import sys import sys
import traceback import traceback
@@ -435,7 +436,7 @@ def gen_parameters(opts):
if not re.match('^[0-9a-fA-F]{32}$', ki): if not re.match('^[0-9a-fA-F]{32}$', ki):
raise ValueError('Ki needs to be 128 bits, in hex format') raise ValueError('Ki needs to be 128 bits, in hex format')
else: else:
ki = os.urandom(16).hex() ki = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
# OPC (random) # OPC (random)
if opts.opc is not None: if opts.opc is not None:
@@ -446,7 +447,7 @@ def gen_parameters(opts):
elif opts.op is not None: elif opts.op is not None:
opc = derive_milenage_opc(ki, opts.op) opc = derive_milenage_opc(ki, opts.op)
else: else:
opc = os.urandom(16).hex() opc = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex) pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)

View File

@@ -1079,13 +1079,6 @@ class SecurityDomainKey:
'keyVersionNumber': bytes([self.key_version_number]), 'keyVersionNumber': bytes([self.key_version_number]),
'keyComponents': [k.to_saip_dict() for k in self.key_components]} '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 ProfileElementSD(ProfileElement):
"""Class representing a securityDomain ProfileElement.""" """Class representing a securityDomain ProfileElement."""
type = 'securityDomain' type = 'securityDomain'

View File

@@ -1,120 +0,0 @@
"""Implementation of Personalization of eSIM profiles in SimAlliance/TCA Interoperable Profile:
Run a batch of N personalizations"""
# (C) 2025-2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
#
# Author: nhofmeyr@sysmocom.de
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy
from typing import Generator
from pySim.esim.saip.personalization import ConfigurableParameter
from pySim.esim.saip import param_source
from pySim.esim.saip import ProfileElementSequence
class BatchPersonalization:
"""Produce a series of eSIM profiles from predefined parameters.
Personalization parameters are derived from pysim.esim.saip.param_source.ParamSource.
Usage example:
der_input = open('some_file', 'rb').read()
pes = ProfileElementSequence.from_der(der_input)
p = BatchPersonalization(
n=10,
src_pes=pes,
csv_rows=get_csv_reader())
p.add_param_and_src(
personalization.Iccid(),
param_source.IncDigitSource(
num_digits=18,
first_value=123456789012340001,
last_value=123456789012340010))
# add more parameters here, using ConfigurableParameter and ParamSource subclass instances to define the profile
# ...
# generate all 10 profiles (from n=10 above)
for result_pes in p.generate_profiles():
upp = result_pes.to_der()
store_upp(upp)
"""
class ParamAndSrc:
"""tie a ConfigurableParameter to a source of actual values"""
def __init__(self, param: ConfigurableParameter, src: param_source.ParamSource):
if isinstance(param, type):
self.param_cls = param
else:
self.param_cls = param.__class__
self.src = src
def __init__(self,
n: int,
src_pes: ProfileElementSequence,
params: list[ParamAndSrc]=None,
csv_rows: Generator=None,
):
"""
n: number of eSIM profiles to generate.
src_pes: a decoded eSIM profile as ProfileElementSequence, to serve as template. This is not modified, only
copied.
params: list of ParamAndSrc instances, defining a ConfigurableParameter and corresponding ParamSource to fill in
profile values.
csv_rows: A generator (e.g. iter(list_of_rows)) producing all CSV rows one at a time, starting with a row
containing the column headers. This is compatible with the python csv.reader. Each row gets passed to
ParamSource.get_next(), such that ParamSource implementations can access the row items. See
param_source.CsvSource.
"""
self.n = n
self.params = params or []
self.src_pes = src_pes
self.csv_rows = csv_rows
def add_param_and_src(self, param:ConfigurableParameter, src:param_source.ParamSource):
self.params.append(BatchPersonalization.ParamAndSrc(param, src))
def generate_profiles(self):
# get first row of CSV: column names
csv_columns = None
if self.csv_rows:
try:
csv_columns = next(self.csv_rows)
except StopIteration as e:
raise ValueError('the input CSV file appears to be empty') from e
for i in range(self.n):
csv_row = None
if self.csv_rows and csv_columns:
try:
csv_row_list = next(self.csv_rows)
except StopIteration as e:
raise ValueError(f'not enough rows in the input CSV for eSIM nr {i+1} of {self.n}') from e
csv_row = dict(zip(csv_columns, csv_row_list))
pes = copy.deepcopy(self.src_pes)
for p in self.params:
try:
input_value = p.src.get_next(csv_row=csv_row)
assert input_value is not None
value = p.param_cls.validate_val(input_value)
p.param_cls.apply_val(pes, value)
except Exception as e:
raise ValueError(f'{p.param_cls.get_name()} fed by {p.src.name}: {e}') from e
yield pes

View File

@@ -1,203 +0,0 @@
# Implementation of SimAlliance/TCA Interoperable Profile handling: parameter sources for batch personalization.
#
# (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
#
# Author: nhofmeyr@sysmocom.de
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import random
import re
from osmocom.utils import b2h
class ParamSourceExn(Exception):
pass
class ParamSourceExhaustedExn(ParamSourceExn):
pass
class ParamSourceUndefinedExn(ParamSourceExn):
pass
class ParamSource:
"""abstract parameter source. For usage, see personalization.BatchPersonalization."""
# This name should be short but descriptive, useful for a user interface, like 'random decimal digits'.
name = "none"
numeric_base = None # or 10 or 16
def __init__(self, input_str:str):
"""Subclasses should call super().__init__(input_str) before evaluating self.input_str. Each subclass __init__()
may in turn manipulate self.input_str to apply expansions or decodings."""
self.input_str = input_str
def get_next(self, csv_row:dict=None):
"""Subclasses implement this: return the next value from the parameter source.
When there are no more values from the source, raise a ParamSourceExhaustedExn.
This default implementation is an empty source."""
raise ParamSourceExhaustedExn()
@classmethod
def from_str(cls, input_str:str):
"""compatibility with earlier version of ParamSource. Just use the constructor."""
return cls(input_str)
class ConstantSource(ParamSource):
"""one value for all"""
name = "constant"
def get_next(self, csv_row:dict=None):
return self.input_str
class InputExpandingParamSource(ParamSource):
def __init__(self, input_str:str):
super().__init__(input_str)
self.input_str = self.expand_input_str(self.input_str)
@classmethod
def expand_input_str(cls, input_str:str):
# user convenience syntax '0*32' becomes '00000000000000000000000000000000'
if "*" not in input_str:
return input_str
# re: "XX * 123" with optional spaces
tokens = re.split(r"([^ \t]+)[ \t]*\*[ \t]*([0-9]+)", input_str)
if len(tokens) < 3:
return input_str
parts = []
for unchanged, snippet, repeat_str in zip(tokens[0::3], tokens[1::3], tokens[2::3]):
parts.append(unchanged)
repeat = int(repeat_str)
parts.append(snippet * repeat)
return "".join(parts)
class DecimalRangeSource(InputExpandingParamSource):
"""abstract: decimal numbers with a value range"""
numeric_base = 10
def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
"""Constructor to set up values from a (user entered) string: DecimalRangeSource(input_str).
Constructor to set up values directly: DecimalRangeSource(num_digits=3, first_value=123, last_value=456)
num_digits produces leading zeros when first_value..last_value are shorter.
"""
assert ((input_str is not None and (num_digits, first_value, last_value) == (None, None, None))
or (input_str is None and None not in (num_digits, first_value, last_value)))
if input_str is not None:
super().__init__(input_str)
input_str = self.input_str
if ".." in input_str:
first_str, last_str = input_str.split('..')
first_str = first_str.strip()
last_str = last_str.strip()
else:
first_str = input_str.strip()
last_str = None
num_digits = len(first_str)
first_value = int(first_str)
last_value = int(last_str if last_str is not None else "9" * num_digits)
assert num_digits > 0
assert first_value <= last_value
self.num_digits = num_digits
self.first_value = first_value
self.last_value = last_value
def val_to_digit(self, val:int):
return "%0*d" % (self.num_digits, val) # pylint: disable=consider-using-f-string
class RandomDigitSource(DecimalRangeSource):
"""return a different sequence of random decimal digits each"""
name = "random decimal digits"
def get_next(self, csv_row:dict=None):
val = random.randint(self.first_value, self.last_value) # TODO secure random source?
return self.val_to_digit(val)
class RandomHexDigitSource(InputExpandingParamSource):
"""return a different sequence of random hexadecimal digits each"""
name = "random hexadecimal digits"
numeric_base = 16
def __init__(self, input_str:str):
super().__init__(input_str)
input_str = self.input_str
num_digits = len(input_str.strip())
if num_digits < 1:
raise ValueError("zero number of digits")
# hex digits always come in two
if (num_digits & 1) != 0:
raise ValueError(f"hexadecimal value should have even number of digits, not {num_digits}")
self.num_digits = num_digits
def get_next(self, csv_row:dict=None):
val = random.randbytes(self.num_digits // 2) # TODO secure random source?
return b2h(val)
class IncDigitSource(DecimalRangeSource):
"""incrementing sequence of digits"""
name = "incrementing decimal digits"
def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
"""input_str: the range of values to iterate. Format: 'FIRST..LAST' (e.g. '0001..9999') or
just 'FIRST' (iterates to the maximum value for the given digit width). Leading zeros in
FIRST determine the digit width and are preserved in returned values."""
super().__init__(input_str, num_digits, first_value, last_value)
self.next_val = None
self.reset()
def reset(self):
"""Restart from the first value of the defined range passed to __init__()."""
self.next_val = self.first_value
def get_next(self, csv_row:dict=None):
val = self.next_val
if val is None:
raise ParamSourceExhaustedExn()
returnval = self.val_to_digit(val)
val += 1
if val > self.last_value:
self.next_val = None
else:
self.next_val = val
return returnval
class CsvSource(ParamSource):
"""apply a column from a CSV row, as passed in to ParamSource.get_next(csv_row)"""
name = "from CSV"
def __init__(self, input_str:str):
"""input_str: the CSV column name to read values from.
The caller passes the current CSV row to get_next(), from which CsvSource picks the column matching
this name."""
super().__init__(input_str)
self.csv_column = self.input_str
def get_next(self, csv_row:dict=None):
val = None
if csv_row:
val = csv_row.get(self.csv_column)
if val is None:
raise ParamSourceUndefinedExn(f"no value for CSV column {self.csv_column!r}")
return val

View File

@@ -16,22 +16,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc import abc
import enum
import io import io
import re from typing import List, Tuple
from typing import List, Tuple, Generator, Optional
from osmocom.tlv import camel_to_snake from osmocom.tlv import camel_to_snake
from osmocom.utils import hexstr from pySim.utils import enc_iccid, enc_imsi, h2b, rpad, sanitize_iccid
from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid 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 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))
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'."""
@@ -126,7 +117,6 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
max_len = None max_len = None
allow_len = None # a list of specific lengths allow_len = None # a list of specific lengths
example_input = None example_input = None
default_source = None # a param_source.ParamSource subclass
def __init__(self, input_value=None): def __init__(self, input_value=None):
self.input_value = input_value # the raw input value as given by caller self.input_value = input_value # the raw input value as given by caller
@@ -209,29 +199,6 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
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
@abc.abstractmethod
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'.
Yielded value must be a dict(). Usually, an implementation will return only one key, like
{ "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 { cls.name: 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
@@ -252,13 +219,6 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
return (None, None) return (None, None)
return (min(vals), max(vals)) 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
class DecimalParam(ConfigurableParameter): class DecimalParam(ConfigurableParameter):
"""Decimal digits. The input value may be a string of decimal digits like '012345', or an int. The output of """Decimal digits. The input value may be a string of decimal digits like '012345', or an int. The output of
@@ -289,7 +249,6 @@ class DecimalHexParam(DecimalParam):
@classmethod @classmethod
def validate_val(cls, val): def validate_val(cls, val):
val = super().validate_val(val) val = super().validate_val(val)
assert isinstance(val, str)
val = ''.join('%02x' % ord(x) for x in val) val = ''.join('%02x' % ord(x) for x in val)
if cls.rpad is not None: if cls.rpad is not None:
c = cls.rpad_char c = cls.rpad_char
@@ -297,17 +256,6 @@ 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'
@@ -331,19 +279,10 @@ class IntegerParam(ConfigurableParameter):
raise ValueError(f'Value {val} is out of range, must be [{cls.min_val}..{cls.max_val}]') raise ValueError(f'Value {val} is out of range, must be [{cls.min_val}..{cls.max_val}]')
return val return val
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
for valdict in super().get_values_from_pes(pes):
for key, val in valdict.items():
if isinstance(val, int):
valdict[key] = str(val)
yield valdict
class BinaryParam(ConfigurableParameter): class BinaryParam(ConfigurableParameter):
allow_types = (str, io.BytesIO, bytes, bytearray) allow_types = (str, io.BytesIO, bytes, bytearray)
allow_chars = '0123456789abcdefABCDEF' allow_chars = '0123456789abcdefABCDEF'
strip_chars = ' \t\r\n' strip_chars = ' \t\r\n'
default_source = param_source.RandomHexDigitSource
@classmethod @classmethod
def validate_val(cls, val): def validate_val(cls, val):
@@ -362,82 +301,6 @@ class BinaryParam(ConfigurableParameter):
val = super().validate_val(val) val = super().validate_val(val)
return bytes(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):
"""ConfigurableParameter for named integer enumeration values.
Subclasses must define a nested enum.IntEnum named 'Values' listing all valid names and their
integer codes. apply_val() and get_values_from_pes() are not implemented here and this must
be inherited from another mixin."""
class Values(enum.IntEnum):
pass # subclasses override this
@classmethod
def validate_val(cls, val) -> int:
if isinstance(val, int):
try:
return int(cls.Values(val))
except ValueError:
pass
elif isinstance(val, str):
member = cls.map_name_to_val(val, strict=False)
if member is not None:
return member
valid = ', '.join(m.name for m in cls.Values)
raise ValueError(f"{cls.get_name()}: invalid argument: {val!r}. Valid arguments are: {valid}")
@classmethod
def map_name_to_val(cls, name: str, strict=True) -> int:
"""Return the integer value for a given enum member name. Performs an exact match first,
then falls back to fuzzy matching (case-insensitive, punctuation-insensitive)."""
try:
return int(cls.Values[name])
except KeyError:
pass
clean = cls.clean_name_str(name)
for member in cls.Values:
if cls.clean_name_str(member.name) == clean:
return int(member)
if strict:
valid = ', '.join(m.name for m in cls.Values)
raise ValueError(f"{cls.get_name()}: {name!r} is not a known value. Known values are: {valid}")
return None
@classmethod
def map_val_to_name(cls, val, strict=False) -> str:
"""Return the enum member name for a given integer value."""
try:
return cls.Values(val).name
except ValueError:
if strict:
raise ValueError(f"{cls.get_name()}: {val!r} ({type(val).__name__}) is not a known value.")
return None
@classmethod
def name_normalize(cls, name: str) -> str:
"""Map a (possibly fuzzy) name to its canonical enum member name."""
return cls.Values(cls.map_name_to_val(name)).name
@classmethod
def clean_name_str(cls, val: str) -> str:
"""Strip punctuation and case for fuzzy name comparison.
Treats hyphens and underscores as equivalent (both removed)."""
return re.sub('[^0-9A-Za-z]', '', val).lower()
class Iccid(DecimalParam): class Iccid(DecimalParam):
"""ICCID Parameter. Input: string of decimal digits. """ICCID Parameter. Input: string of decimal digits.
@@ -446,7 +309,6 @@ class Iccid(DecimalParam):
min_len = 18 min_len = 18
max_len = 20 max_len = 20
example_input = '998877665544332211' example_input = '998877665544332211'
default_source = param_source.IncDigitSource
@classmethod @classmethod
def validate_val(cls, val): def validate_val(cls, val):
@@ -460,17 +322,6 @@ 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 { cls.name: iccid }
for pe in pes.get_pes_for_type('mf'):
iccid_f = pe.files.get('ef-iccid', None)
if iccid_f is not None:
yield { cls.name: dec_iccid(b2h(iccid_f.body)) }
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."""
@@ -479,7 +330,6 @@ class Imsi(DecimalParam):
min_len = 6 min_len = 6
max_len = 15 max_len = 15
example_input = '00101' + ('0' * 10) example_input = '00101' + ('0' * 10)
default_source = param_source.IncDigitSource
@classmethod @classmethod
def apply_val(cls, pes: ProfileElementSequence, val): def apply_val(cls, pes: ProfileElementSequence, val):
@@ -492,18 +342,6 @@ 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_f = pe.files.get('ef-imsi', None)
acc_f = pe.files.get('ef-acc', None)
y = {}
if imsi_f:
y[cls.name] = dec_imsi(b2h(imsi_f.body))
if acc_f:
y[cls.name + '-ACC'] = b2h(acc_f.body)
yield y
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
@@ -515,41 +353,22 @@ class SmspTpScAddr(ConfigurableParameter):
max_len = 21 # '+' and 20 digits max_len = 21 # '+' and 20 digits
min_len = 1 min_len = 1
example_input = '+49301234567' example_input = '+49301234567'
default_source = param_source.ConstantSource
@staticmethod @classmethod
def str_to_tuple(addr_str): def validate_val(cls, val):
val = super().validate_val(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
else: else:
digits = addr_str digits = addr_str
international = False international = False
return (international, digits)
@staticmethod
def tuple_to_str(addr_tuple):
international, digits = addr_tuple
if international:
ret = '+'
else:
ret = ''
ret += digits
return ret
@classmethod
def validate_val(cls, val):
val = super().validate_val(val)
addr_tuple = cls.str_to_tuple(str(val))
international, digits = addr_tuple
if len(digits) > 20: if len(digits) > 20:
raise ValueError(f'TP-SC-ADDR must not exceed 20 digits: {digits!r}') raise ValueError(f'TP-SC-ADDR must not exceed 20 digits: {digits!r}')
if not digits.isdecimal(): if not digits.isdecimal():
raise ValueError(f'TP-SC-ADDR must only contain decimal digits: {digits!r}') raise ValueError(f'TP-SC-ADDR must only contain decimal digits: {digits!r}')
return (international, digits)
return addr_tuple
@classmethod @classmethod
def apply_val(cls, pes: ProfileElementSequence, val): def apply_val(cls, pes: ProfileElementSequence, val):
@@ -579,32 +398,6 @@ 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 { cls.name: cls.tuple_to_str((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
@@ -614,40 +407,28 @@ class SdKey(BinaryParam, metaclass=ClassVarMeta):
key_usage_qual = None key_usage_qual = None
@classmethod @classmethod
def apply_val(cls, pes: ProfileElementSequence, val): def _apply_sd(cls, pe: ProfileElement, value):
set_components = [ SecurityDomainKeyComponent(cls.key_type, val) ] assert pe.type == 'securityDomain'
for key in pe.decoded['keyList']:
for pe in pes.pe_list: if key['keyIdentifier'][0] == cls.key_id and key['keyVersionNumber'][0] == cls.kvn:
if pe.type != 'securityDomain': assert len(key['keyComponents']) == 1
continue key['keyComponents'][0]['keyData'] = value
assert isinstance(pe, ProfileElementSD) return
# Could not find matching key to patch, create a new one
key = pe.find_key(key_version_number=cls.kvn, key_id=cls.key_id) key = {
if not key: 'keyUsageQualifier': bytes([cls.key_usage_qual]),
# Could not find matching key to patch, create a new one 'keyIdentifier': bytes([cls.key_id]),
key = SecurityDomainKey( 'keyVersionNumber': bytes([cls.kvn]),
key_version_number=cls.kvn, 'keyComponents': [
key_id=cls.key_id, { 'keyType': bytes([cls.key_type]), 'keyData': value },
key_usage_qualifier=KeyUsageQualifier.build(cls.key_usage_qual), ]
key_components=set_components, }
) pe.decoded['keyList'].append(key)
pe.add_key(key)
else:
key.key_components = set_components
@classmethod @classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence): def apply_val(cls, pes: ProfileElementSequence, value):
for pe in pes.pe_list: for pe in pes.get_pes_for_type('securityDomain'):
if pe.type != 'securityDomain': cls._apply_sd(pe, value)
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, 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
@@ -721,8 +502,7 @@ class Puk(DecimalHexParam):
allow_len = 8 allow_len = 8
rpad = 16 rpad = 16
keyReference = None keyReference = None
example_input = f'0*{allow_len}' example_input = '0' * allow_len
default_source = param_source.RandomDigitSource
@classmethod @classmethod
def apply_val(cls, pes: ProfileElementSequence, val): def apply_val(cls, pes: ProfileElementSequence, val):
@@ -736,14 +516,6 @@ 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.name: cls.decimal_hex_to_str(pukCode['pukValue']) }
class Puk1(Puk): class Puk1(Puk):
name = 'PUK1' name = 'PUK1'
keyReference = 0x01 keyReference = 0x01
@@ -757,8 +529,7 @@ class Pin(DecimalHexParam):
rpad = 16 rpad = 16
min_len = 4 min_len = 4
max_len = 8 max_len = 8
example_input = f'0*{max_len}' example_input = '0' * max_len
default_source = param_source.RandomDigitSource
keyReference = None keyReference = None
@staticmethod @staticmethod
@@ -780,24 +551,9 @@ 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):
"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.name: 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):
name = 'PIN1' name = 'PIN1'
example_input = '0*4' # PIN are usually 4 digits example_input = '0' * 4 # PIN are usually 4 digits
keyReference = 0x01 keyReference = 0x01
class Pin2(Pin1): class Pin2(Pin1):
@@ -816,14 +572,6 @@ 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):
name = 'ADM1' name = 'ADM1'
keyReference = 0x0A keyReference = 0x0A
@@ -848,59 +596,26 @@ 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 class AlgorithmID(DecimalParam, AlgoConfig):
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
if isinstance(val, bytes):
val = b2h(val)
# if it is an int (algorithmID), just pass thru as int
yield { cls.name: val }
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."""
name = "Algorithm"
algo_config_key = 'algorithmID' algo_config_key = 'algorithmID'
example_input = "Milenage" allow_len = 1
default_source = param_source.ConstantSource example_input = 1 # Milenage
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
class Values(enum.IntEnum):
Milenage = 1
TUAK = 2
usim_test = 3 # input 'usim-test' also accepted via fuzzy matching
# EnumParam.validate_val() returns the int values from Values
@classmethod @classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence): def validate_val(cls, val):
# return enum names, not raw values. val = super().validate_val(val)
# use of super(): this intends to call AlgoConfig.get_values_from_pes() so that the cls argument is this cls val = int(val)
# here (AlgorithmID); i.e. AlgoConfig.get_values_from_pes(pes) doesn't work, because AlgoConfig needs to look up valid = (1, 2, 3)
# cls.algo_config_key. if val not in valid:
for d in super(cls, cls).get_values_from_pes(pes): raise ValueError(f'Invalid algorithmID {val!r}, must be one of {valid}')
if cls.name in d: return val
# 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): class K(BinaryParam, AlgoConfig):
"""use validate_val() from BinaryParam, and apply_val() from AlgoConfig""" """use validate_val() from BinaryParam, and apply_val() from AlgoConfig"""
name = 'K' name = 'K'
algo_config_key = 'key' algo_config_key = 'key'
allow_len = (128 // 8, 256 // 8) # length in bytes (from BinaryParam); TUAK also allows 256 bit allow_len = (128 // 8, 256 // 8) # length in bytes (from BinaryParam); TUAK also allows 256 bit
example_input = f'00*{allow_len[0]}' example_input = '00' * allow_len[0]
class Opc(K): class Opc(K):
name = 'OPc' name = 'OPc'
@@ -914,7 +629,6 @@ class MilenageRotationConstants(BinaryParam, AlgoConfig):
algo_config_key = 'rotationConstants' algo_config_key = 'rotationConstants'
allow_len = 5 # length in bytes (from BinaryParam) allow_len = 5 # length in bytes (from BinaryParam)
example_input = '40 00 20 40 60' example_input = '40 00 20 40 60'
default_source = param_source.ConstantSource
@classmethod @classmethod
def validate_val(cls, val): def validate_val(cls, val):
@@ -945,7 +659,6 @@ class MilenageXoringConstants(BinaryParam, AlgoConfig):
' 00000000000000000000000000000002' ' 00000000000000000000000000000002'
' 00000000000000000000000000000004' ' 00000000000000000000000000000004'
' 00000000000000000000000000000008') ' 00000000000000000000000000000008')
default_source = param_source.ConstantSource
class TuakNumberOfKeccak(IntegerParam, AlgoConfig): class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
"""Number of iterations of Keccak-f[1600] permutation as recomended by Section 7.2 of 3GPP TS 35.231""" """Number of iterations of Keccak-f[1600] permutation as recomended by Section 7.2 of 3GPP TS 35.231"""
@@ -954,4 +667,3 @@ class TuakNumberOfKeccak(IntegerParam, AlgoConfig):
min_val = 1 min_val = 1
max_val = 255 max_val = 255
example_input = '1' example_input = '1'
default_source = param_source.ConstantSource

View File

@@ -91,7 +91,6 @@ class UiccSdInstallParams(TLV_IE_Collection, nested=[UiccScp, AcceptExtradAppsAn
# Key Usage: # Key Usage:
# KVN 0x01 .. 0x0F reserved for SCP80 # KVN 0x01 .. 0x0F reserved for SCP80
# KVN 0x81 .. 0x8f reserved for SCP81
# KVN 0x11 reserved for DAP specified in ETSI TS 102 226 # KVN 0x11 reserved for DAP specified in ETSI TS 102 226
# KVN 0x20 .. 0x2F reserved for SCP02 # KVN 0x20 .. 0x2F reserved for SCP02
# KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK # KID 0x01 = ENC; 0x02 = MAC; 0x03 = DEK

View File

@@ -152,8 +152,7 @@ class SimCard(SimCardBase):
return sw return sw
def update_smsp(self, smsp): def update_smsp(self, smsp):
print("using update_smsp") data, sw = self._scc.update_record(EF['SMSP'], 1, rpad(smsp, 84))
data, sw = self._scc.update_record(EF['SMSP'], 1, smsp, leftpad=True)
return sw return sw
def update_ad(self, mnc=None, opmode=None, ofm=None, path=EF['AD']): def update_ad(self, mnc=None, opmode=None, ofm=None, path=EF['AD']):

View File

@@ -307,48 +307,49 @@ class LinkBaseTpdu(LinkBase):
# After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further # After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further
# TPDUs have to be sent in order to complete the task. # TPDUs have to be sent in order to complete the task.
if case == 4 or self.apdu_strict == False: if sw is not None:
# In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data if case == 4 or self.apdu_strict == False:
# available which has to be retrieved using a GET RESPONSE command TPDU. # In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data
# # available which has to be retrieved using a GET RESPONSE command TPDU.
# ETSI TS 102 221, section 7.3.1.1.4 is very cleare about the fact that the GET RESPONSE mechanism #
# shall only apply on case #4 APDUs but unfortunately it is impossible to distinguish between case #3 # ETSI TS 102 221, section 7.3.1.1.4 is very cleare about the fact that the GET RESPONSE mechanism
# and case #4 when the APDU format is not strictly followed. In order to be able to detect case #4 # shall only apply on case #4 APDUs but unfortunately it is impossible to distinguish between case #3
# correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with # and case #4 when the APDU format is not strictly followed. In order to be able to detect case #4
# legacy scripts that use raw APDU strings, we will still loosely apply GET RESPONSE based on what # correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with
# the status word indicates. Unless the user explicitly enables the strict mode (set apdu_strict true) # legacy scripts that use raw APDU strings, we will still loosely apply GET RESPONSE based on what
while True: # the status word indicates. Unless the user explicitly enables the strict mode (set apdu_strict true)
if sw in ['9000', '9100']: while True:
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command) if sw in ['9000', '9100']:
# indicates that either no response data was returnd or all response data has been retrieved # A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
# successfully. We may discontinue the processing at this point. # indicates that either no response data was returnd or all response data has been retrieved
break; # successfully. We may discontinue the processing at this point.
if sw[0:2] in ['61', '9f']: break;
# A status word of 61xx or 9fxx indicates that there is (still) response data available. We if sw[0:2] in ['61', '9f']:
# send a GET RESPONSE command with the length value indicated in the second byte of the status # A status word of 61xx or 9fxx indicates that there is (still) response data available. We
# word. (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4a and 3GPP TS 51.011 9.4.1 and # send a GET RESPONSE command with the length value indicated in the second byte of the status
# ISO/IEC 7816-4, Table 5) # word. (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4a and 3GPP TS 51.011 9.4.1 and
le_gr = sw[2:4] # ISO/IEC 7816-4, Table 5)
elif sw[0:2] in ['62', '63']: le_gr = sw[2:4]
# There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us elif sw[0:2] in ['62', '63']:
# to send a dummy GET RESPONSE command. We send a GET RESPONSE command with a length of 0. # There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1) # to send a dummy GET RESPONSE command. We send a GET RESPONSE command with a length of 0.
le_gr = '00' # (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1)
else: le_gr = '00'
# A status word other then the ones covered by the above logic may indicate an error. In this else:
# case we will discontinue the processing as well. # A status word other then the ones covered by the above logic may indicate an error. In this
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4c) # case we will discontinue the processing as well.
break # (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4c)
tpdu_gr = tpdu[0:2] + 'c00000' + le_gr break
prev_tpdu = tpdu_gr tpdu_gr = tpdu[0:2] + 'c00000' + le_gr
data_gr, sw = self.send_tpdu(tpdu_gr) prev_tpdu = tpdu_gr
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)") data_gr, sw = self.send_tpdu(tpdu_gr)
data += data_gr log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
if sw[0:2] == '6c': data += data_gr
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding if sw[0:2] == '6c':
tpdu_gr = prev_tpdu[0:8] + sw[2:4] # SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
data, sw = self.send_tpdu(tpdu_gr) tpdu_gr = prev_tpdu[0:8] + sw[2:4]
log.debug("T0: repated case #%u TPDU: %s => %s %s", case, tpdu_gr, data or "(no data)", sw or "(no status word)") data, sw = self.send_tpdu(tpdu_gr)
log.debug("T0: repated case #%u TPDU: %s => %s %s", case, tpdu_gr, data or "(no data)", sw or "(no status word)")
return data, sw return data, sw

View File

@@ -251,16 +251,6 @@ class EF_SMSP(LinFixedEF):
"numbering_plan_id": "isdn_e164" }, "numbering_plan_id": "isdn_e164" },
"call_number": "4915790109999" }, "call_number": "4915790109999" },
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 4320 } ), "tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 4320 } ),
( 'e1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9',
{ "alpha_id": "", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True,
"tp_pid": True, "tp_dcs": True, "tp_vp": True },
"tp_dest_addr": { "length": 255, "ton_npi": { "ext": True, "type_of_number": "reserved_for_extension",
"numbering_plan_id": "reserved_for_extension" },
"call_number": "" },
"tp_sc_addr": { "length": 8, "ton_npi": { "ext": True, "type_of_number": "international",
"numbering_plan_id": "isdn_e164" },
"call_number": "4915790109999" },
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 4320 } ),
( '454e6574776f726b73fffffffffffffff1ffffffffffffffffffffffffffffffffffffffffffffffff0000a7', ( '454e6574776f726b73fffffffffffffff1ffffffffffffffffffffffffffffffffffffffffffffffff0000a7',
{ "alpha_id": "ENetworks", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True, { "alpha_id": "ENetworks", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True,
"tp_pid": True, "tp_dcs": True, "tp_vp": False }, "tp_pid": True, "tp_dcs": True, "tp_vp": False },
@@ -341,8 +331,7 @@ class EF_SMSP(LinFixedEF):
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10)))) 'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
DestAddr = Struct('length'/Rebuild(Int8ub, lambda ctx: EF_SMSP.dest_addr_len(ctx)), DestAddr = Struct('length'/Rebuild(Int8ub, lambda ctx: EF_SMSP.dest_addr_len(ctx)),
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10)))) 'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
# (see comment below) self._construct = Struct('alpha_id'/COptional(GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-28)))),
self._construct = Struct('alpha_id'/GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-28))),
'parameter_indicators'/InvertAdapter(BitStruct( 'parameter_indicators'/InvertAdapter(BitStruct(
Const(7, BitsInteger(3)), Const(7, BitsInteger(3)),
'tp_vp'/Flag, 'tp_vp'/Flag,
@@ -356,25 +345,6 @@ class EF_SMSP(LinFixedEF):
'tp_dcs'/Bytes(1), 'tp_dcs'/Bytes(1),
'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte)) 'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte))
# Ensure 'alpha_id' is always present
def encode_record_hex(self, abstract_data: dict, record_nr: int, total_len: int = None) -> str:
# Problem: TS 51.011 Section 10.5.6 describes the 'alpha_id' field as optional. However, this is only true
# at the time when the record length of the file is set up in the file system. A card manufacturer may decide
# to remove the field by setting the record length to 28. Likewise, the card manaufacturer may also decide to
# set the field to a distinct length by setting the record length to a value greater than 28 (e.g. 14 bytes
# 'alpha_id' + 28 bytes). Due to the fixed nature of the record length, this eventually means that in practice
# 'alpha_id' is a mandatory field with a fixed length.
#
# Due to the problematic specification of 'alpha_id' as a pseudo-optional field at the beginning of a
# fixed-size memory, the construct definition in self._construct has been incorrectly implemented and the field
# has been marked as COptional. We may correct the problem by removing COptional. But to maintain compatibility,
# we then have to ensure that in case the field is not provided (None), it is set to an empty string ('').
#
# See also ts_31_102.py, class EF_OCI for a correct example.
if abstract_data['alpha_id'] is None:
abstract_data['alpha_id'] = ''
return super().encode_record_hex(abstract_data, record_nr, total_len)
# TS 51.011 Section 10.5.7 # TS 51.011 Section 10.5.7
class EF_SMSS(TransparentEF): class EF_SMSS(TransparentEF):
class MemCapAdapter(Adapter): class MemCapAdapter(Adapter):

View File

@@ -6,7 +6,7 @@ jsonpath-ng
construct>=2.10.70 construct>=2.10.70
bidict bidict
pyosmocom>=0.0.12 pyosmocom>=0.0.12
pyyaml>=5.4 pyyaml>=5.1
termcolor termcolor
colorlog colorlog
pycryptodomex pycryptodomex

View File

@@ -26,7 +26,7 @@ setup(
"construct >= 2.10.70", "construct >= 2.10.70",
"bidict", "bidict",
"pyosmocom >= 0.0.12", "pyosmocom >= 0.0.12",
"pyyaml >= 5.4", "pyyaml >= 5.1",
"termcolor", "termcolor",
"colorlog", "colorlog",
"pycryptodomex", "pycryptodomex",

View File

@@ -5,7 +5,7 @@ ICCID: 8988219000000117833
IMSI: 001010000000111 IMSI: 001010000000111
GID1: ffffffffffffffff GID1: ffffffffffffffff
GID2: ffffffffffffffff GID2: ffffffffffffffff
SMSP: ffffffffffffffffffffffffffffe1ffffffffffffffffffffffff0581005155f5ffffffffffff000000 SMSP: e1ffffffffffffffffffffffff0581005155f5ffffffffffff000000ffffffffffffffffffffffffffff
SMSC: 0015555 SMSC: 0015555
SPN: Fairwaves SPN: Fairwaves
Show in HPLMN: False Show in HPLMN: False

View File

@@ -5,7 +5,7 @@ ICCID: 89445310150011013678
IMSI: 001010000000102 IMSI: 001010000000102
GID1: Can't read file -- SW match failed! Expected 9000 and got 6a82. GID1: Can't read file -- SW match failed! Expected 9000 and got 6a82.
GID2: Can't read file -- SW match failed! Expected 9000 and got 6a82. GID2: Can't read file -- SW match failed! Expected 9000 and got 6a82.
SMSP: ffffffffffffffffffffffffffffe1ffffffffffffffffffffffff0581005155f5ffffffffffff000000 SMSP: e1ffffffffffffffffffffffff0581005155f5ffffffffffff000000ffffffffffffffffffffffffffff
SMSC: 0015555 SMSC: 0015555
SPN: wavemobile SPN: wavemobile
Show in HPLMN: False Show in HPLMN: False

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Utility to verify the functionality of pySim-smpp2sim.py # Utility to verify the functionality of pySim-trace.py
# #
# (C) 2026 by sysmocom - s.f.m.c. GmbH # (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved # All Rights Reserved