Compare commits

..

36 Commits

Author SHA1 Message Date
Neels Hofmeyr c224990d74 personalization: fix EF_SMSP length, alpha_id padding
The efFileSize needs to be updated and the alpha_id needs to be != None.

Change-Id: Ief6e02517f3e96158a2509d763b88aec4bd5a296
2026-04-22 20:17:23 +02:00
Neels Hofmeyr 3d232ad5ef sdkeys kv40 aes
Change-Id: If5b53c840ebd1f224f9bb4706a602b415194f47b
2026-04-22 20:17:04 +02:00
Neels Hofmeyr b2a2154abd MncLen
Change-Id: I6c600faeab00ffb072acbe94c9a8b2d1397c07d3
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 52875ee71e saip: add numeric_base indicator to ConfigurableParameter
By default, numeric_base = None, to indicate that there are no explicit
limitations on the number space.

For parameters that are definitely decimal, set numeric_base = 10.
For definitely hexadecimal, set numeric_base = 16.

Do the same for ConfigurableParameter as well as ParamSource, so callers
can match them up: if a parameter is numeric_base = 10, then omit
sources that are numeric_base = 16, and vice versa.

Change-Id: Ib0977bbdd9a85167be7eb46dd331fedd529dae01
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 2f867e702a saip SmspTpScAddr.get_values_from_pes: allow empty values
Change-Id: Ibbdd08f96160579238b50699091826883f2e9f5a
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 43dd5e33be SdKey KVN4X ID02: set key_usage_qual=0x48
Related: SYS#7865
Change-Id: Idc5d33a4a003801f60c95fff6931706a9aeb6692
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 5983f387d5 saip: SdKey.__doc__: update SdKey listing
Change-Id: Ib5011b0c7d76b082231744cf09077628dc4e69b7
2026-04-22 20:17:04 +02:00
Neels Hofmeyr b34155fddb esim.saip.personalization: fix TLSPSK keys
Add AES variant of TLSPSK DEK (SCP81 KVN40 key_id=0x02).

Change-Id: I713a008fd26bbfcf437e0f29717b753f058ce76a
2026-04-22 20:17:04 +02:00
Neels Hofmeyr d0e5f1eb1d add comment about not updating existing key_usage_qualifier
Change-Id: Ie23ae5fde17be6b37746784bf1601b4d0874397a
2026-04-22 20:17:04 +02:00
Neels Hofmeyr c5e2ddc987 test_configurable_parameters.py: add tests for new parameters
For:
SmspTpScAddr
MilenageRotation
MilenageXoringConstants
TuakNrOfKeccak

Change-Id: Iecbea14fe31a9ee08d871dcde7f295d26d7bd001
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 46866a42ea saip: SmspTpScAddr: fix get_values_from_pes
Change-Id: I2010305340499c907bb7618c04c61e194db34814
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 1ee69dbcdf ConfigurableParameter: safer val length check
Change-Id: Ibe91722ed1477b00d20ef5e4e7abd9068ff2f3e4
2026-04-22 20:17:04 +02:00
Neels Hofmeyr a66f999cc4 UppAudit: better indicate exception cause
Change-Id: I4d986b89a473a5b12ed56b4710263b034876a33e
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 6b03546f00 remove transitional name mapping
This reverts commit I974cb6c393a2ed2248a6240c2722d157e9235c33

Now, finally, all SdKey classes have a unified logical naming scheme.

Change-Id: Ic185af4a903c2211a5361d023af9e7c6fc57ae78
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 4c90f60168 transitional name mapping
To help existing applications transition to a common naming scheme for
the SdKey classes, offer this intermediate result, where the SdKey
classes' .name are still unchanged as before generating them.

Change-Id: I974cb6c393a2ed2248a6240c2722d157e9235c33
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 02bb0c3e28 generate sdkey classes from a list
Change-Id: Ic92ddea6e1fad8167ea75baf78ffc3eb419838c4
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 1e2725db12 saip SmspTpScAddr: safeguard against decoding error
Reading the TS48 V6.0 eSIM_GTP_SAIP2.1A_NoBERTLV profile results in an
exception [1] in SmspTpScAddr. I have a caller that needs to skip
erratic values instead of raising.

The underlying issue, I presume, is that either the data needs
validation before decode_record_bin(), or decode_record_bin() needs
well-defined error handling.

So far I know only of this IndexError, so, as a workaround, catch that.

[1]
  File "/pysim/pySim/esim/saip/personalization.py", line 617, in get_values_from_pes
    ef_smsp_dec = ef_smsp.decode_record_bin(f_smsp.body, 1)
  File "/pysim/pySim/filesystem.py", line 1047, in decode_record_bin
    return parse_construct(self._construct, raw_bin_data)
  File "/application/venv/lib/python3.13/site-packages/osmocom/construct.py", line 550, in parse_construct
    parsed = c.parse(raw_bin_data, total_len=length, **context)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 404, in parse
    return self.parse_stream(io.BytesIO(data), **contextkw)
           ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 416, in parse_stream
    return self._parsereport(stream, context, "(parsing)")
           ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 428, in _parsereport
    obj = self._parse(stream, context, path)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 2236, in _parse
    subobj = sc._parsereport(stream, context, path)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 428, in _parsereport
    obj = self._parse(stream, context, path)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 2770, in _parse
    return self.subcon._parsereport(stream, context, path)
           ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 428, in _parsereport
    obj = self._parse(stream, context, path)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 2236, in _parse
    subobj = sc._parsereport(stream, context, path)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 428, in _parsereport
    obj = self._parse(stream, context, path)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 2770, in _parse
    return self.subcon._parsereport(stream, context, path)
           ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 428, in _parsereport
    obj = self._parse(stream, context, path)
  File "/application/venv/lib/python3.13/site-packages/construct/core.py", line 820, in _parse
    return self._decode(obj, context, path)
           ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/application/venv/lib/python3.13/site-packages/osmocom/construct.py", line 268, in _decode
    if r[-1] == 'f':
       ~^^^^
  File "/application/venv/lib/python3.13/site-packages/osmocom/utils.py", line 50, in __getitem__
    return hexstr(super().__getitem__(val))
                  ~~~~~~~~~~~~~~~~~~~^^^^^
IndexError: string index out of range

Change-Id: Ic436e206776b81f24de126e8ee0ae8bf5f3e8d7a
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 067c764561 saip/param_source: try to not repeat random values
Change-Id: I4fa743ef5677580f94b9df16a5051d1d178edeb0
2026-04-22 20:17:04 +02:00
Neels Hofmeyr b08e5b8c1f use secrets.SystemRandom as secure random nr source
secrets.SystemRandom is defined as the most secure random source
available on the given operating system.

Change-Id: I8049cd1292674b3ced82b0926569128535af6efe
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 7c8ec2dd77 use random.SystemRandom as random nr source (/dev/urandom)
/dev/urandom is somewhat better than python's PRNG

Change-Id: I6de38c14ac6dd55bc84d53974192509c18d02bfa
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 8225458201 add test_param_src.py
Change-Id: I03087b84030fddae98b965e0075d44e04ec6ba5c
2026-04-22 20:17:04 +02:00
Neels Hofmeyr feaabf6955 param_source: allow plugging a random implementation (for testing)
Change-Id: Idce2b18af70c17844d6f09f7704efc869456ac39
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 84774ef06e personalization: add int as input type for BinaryParameter
Change-Id: I31d8142cb0847a8b291f8dc614d57cb4734f0190
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 90ff3dee4e personalization.ConfigurableParameter: fix BytesIO() input
Change-Id: I0ad160eef9015e76eef10baee7c6b606fe249123
2026-04-22 20:17:04 +02:00
Neels Hofmeyr da0385a56a add test_configurable_parameters.py
Change-Id: Ia55f0d11f8197ca15a948a83a34b3488acf1a0b4
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 8cf59eda25 ConfigurableParameter: do not magically overwrite the 'name' attribute
Change-Id: I6f631444c6addeb7ccc5f6c55b9be3dc83409169
2026-04-22 20:17:04 +02:00
Neels Hofmeyr bcec3a29b1 personalization audit: optionally audit all (unknown) SD keys
By a flag, allow to audit also all Security Domain KVN that we have
*not* created ConfigurableParameter subclasses for.

For example, SCP80 has reserved kvn 0x01..0x0f, but we offer only
Scp80Kvn01, Scp80Kvn02, Scp80Kvn03. So we would not show kvn
0x03..0x0f in an audit.

This patch includes audits of all SD key kvn there may be in the UPP.
This will help to spot SD keys that may already be present in a UPP
template, with unexpected / unusual kvn.

Change-Id: Icaf6f7b589f117868633c0968a99f2f0252cf612
2026-04-22 20:17:04 +02:00
Neels Hofmeyr c8ea5eead7 personalization: implement UppAudit and BatchAudit
Change-Id: Iaab336ca91b483ecdddd5c6c8e08dc475dc6bd0a
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 52c8d81957 comment in uicc.py on Security Domain Keys: add SCP81
Change-Id: Ib0205880f58e78c07688b4637abd5f67ea0570d1
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 33f3e01a22 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
2026-04-22 20:17:04 +02:00
Neels Hofmeyr a921722412 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
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 09d2e20be4 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
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 13a0de20a0 personalization: indicate default ParamSource per ConfigurableParameter
Add default_source class members pointing to ParamSource classes to all
ConfigurableParameter subclasses.

This is useful to automatically set up a default ParamSource for a given
ConfigurableParameter subclass, during user interaction to produce a
batch personalization.

For example, if the user selects a Pin1 parameter, a calling program can
implicitly set this to a RandomDigitSource, which will magically make it
work the way that most users need.

BTW, default_source and default_value can be combined to configure a
matching ParamSource instance:

  my_source = MyParam.default_source.from_str( MyParam.default_value )

Change-Id: Ie58d13bce3fa1aa2547cf3cee918c2f5b30a8b32
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 2bdd22768c personalization: allow reading back multiple values from PES
Change-Id: Iecb68af7c216c6b9dc3add469564416b6f37f7b2
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 0f7931d03c 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
2026-04-22 20:17:04 +02:00
Neels Hofmeyr 4a41503b9c personalization: add param_source.py, add batch.py
Implement pySim.esim.saip.batch.BatchPersonalization,
generating N eSIM profiles from a preset configuration.

Batch parameters can be fed by a constant, incrementing, random or from
CSV rows: add pySim.esim.saip.param_source.* classes to feed such input
to each of the BatchPersonalization's ConfigurableParameter instances.

Related: SYS#6768
Change-Id: I01ae40a06605eb205bfb409189fcd2b3a128855a
2026-04-22 20:17:04 +02:00
26 changed files with 415 additions and 1039 deletions
+13 -19
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
+1 -1
View File
@@ -640,7 +640,7 @@ class SmDppHttpServer:
# look up profile based on matchingID. We simply check if a given file exists for now.. # look up profile based on matchingID. We simply check if a given file exists for now..
path = os.path.join(self.upp_dir, matchingId) + '.der' path = os.path.join(self.upp_dir, matchingId) + '.der'
# prevent directory traversal attack # prevent directory traversal attack
if os.path.commonpath((os.path.realpath(path),self.upp_dir)) != self.upp_dir: if os.path.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir:
raise ApiError('8.2.6', '3.8', 'Refused') raise ApiError('8.2.6', '3.8', 'Refused')
if not os.path.isfile(path) or not os.access(path, os.R_OK): if not os.path.isfile(path) or not os.access(path, os.R_OK):
raise ApiError('8.2.6', '3.8', 'Refused') raise ApiError('8.2.6', '3.8', 'Refused')
+24 -5
View File
@@ -69,8 +69,8 @@ from pySim.ts_102_222 import Ts102222Commands
from pySim.gsm_r import DF_EIRENE from pySim.gsm_r import DF_EIRENE
from pySim.cat import ProactiveCommand from pySim.cat import ProactiveCommand
from pySim.card_key_provider import card_key_provider_argparse_add_args, card_key_provider_init from pySim.card_key_provider import CardKeyProviderCsv, CardKeyProviderPgsql
from pySim.card_key_provider import card_key_provider_get_field, card_key_provider_get from pySim.card_key_provider import card_key_provider_register, card_key_provider_get_field, card_key_provider_get
from pySim.app import init_card from pySim.app import init_card
@@ -1146,6 +1146,18 @@ global_group.add_argument("--skip-card-init", help="Skip all card/profile initia
global_group.add_argument("--verbose", help="Enable verbose logging", global_group.add_argument("--verbose", help="Enable verbose logging",
action='store_true', default=False) action='store_true', default=False)
card_key_group = option_parser.add_argument_group('Card Key Provider Options')
card_key_group.add_argument('--csv', metavar='FILE',
default="~/.osmocom/pysim/card_data.csv",
help='Read card data from CSV file')
card_key_group.add_argument('--pgsql', metavar='FILE',
default="~/.osmocom/pysim/card_data_pgsql.cfg",
help='Read card data from PostgreSQL database (config file)')
card_key_group.add_argument('--csv-column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
help=argparse.SUPPRESS, dest='column_key')
card_key_group.add_argument('--column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
help='per-column AES transport key', dest='column_key')
adm_group = global_group.add_mutually_exclusive_group() adm_group = global_group.add_mutually_exclusive_group()
adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None, adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
help='ADM PIN used for provisioning (overwrites default)') help='ADM PIN used for provisioning (overwrites default)')
@@ -1158,7 +1170,6 @@ option_parser.add_argument("command", nargs='?',
help="A pySim-shell command that would optionally be executed at startup") help="A pySim-shell command that would optionally be executed at startup")
option_parser.add_argument('command_args', nargs=argparse.REMAINDER, option_parser.add_argument('command_args', nargs=argparse.REMAINDER,
help="Optional Arguments for command") help="Optional Arguments for command")
card_key_provider_argparse_add_args(option_parser)
if __name__ == '__main__': if __name__ == '__main__':
startup_errors = False startup_errors = False
@@ -1167,8 +1178,16 @@ if __name__ == '__main__':
# Ensure that we are able to print formatted warnings from the beginning. # Ensure that we are able to print formatted warnings from the beginning.
PySimLogger.setup(print, {logging.WARN: YELLOW}, opts.verbose) PySimLogger.setup(print, {logging.WARN: YELLOW}, opts.verbose)
# Init card key provider for automatic card key retrieval # Register csv-file as card data provider, either from specified CSV
card_key_provider_init(opts) # or from CSV file in home directory
column_keys = {}
for par in opts.column_key:
name, key = par.split(':')
column_keys[name] = key
if os.path.isfile(os.path.expanduser(opts.csv)):
card_key_provider_register(CardKeyProviderCsv(os.path.expanduser(opts.csv), column_keys))
if os.path.isfile(os.path.expanduser(opts.pgsql)):
card_key_provider_register(CardKeyProviderPgsql(os.path.expanduser(opts.pgsql), column_keys))
# Init card reader driver # Init card reader driver
sl = init_reader(opts, proactive_handler = Proact()) sl = init_reader(opts, proactive_handler = Proact())
+4 -7
View File
@@ -26,9 +26,6 @@ from pySim.cdma_ruim import CardProfileRUIM
from pySim.ts_102_221 import CardProfileUICC from pySim.ts_102_221 import CardProfileUICC
from pySim.utils import all_subclasses from pySim.utils import all_subclasses
from pySim.exceptions import SwMatchError from pySim.exceptions import SwMatchError
from pySim.log import PySimLogger
log = PySimLogger.get(__name__)
# we need to import this module so that the SysmocomSJA2 sub-class of # we need to import this module so that the SysmocomSJA2 sub-class of
# CardModel is created, which will add the ATR-based matching and # CardModel is created, which will add the ATR-based matching and
@@ -57,7 +54,7 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
# Wait up to three seconds for a card in reader and try to detect # Wait up to three seconds for a card in reader and try to detect
# the card type. # the card type.
log.info("Waiting for card...") print("Waiting for card...")
sl.wait_for_card(3) sl.wait_for_card(3)
# The user may opt to skip all card initialization. In this case only the # The user may opt to skip all card initialization. In this case only the
@@ -69,7 +66,7 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
generic_card = False generic_card = False
card = card_detect(scc) card = card_detect(scc)
if card is None: if card is None:
log.warning("Could not detect card type - assuming a generic card type...") print("Warning: Could not detect card type - assuming a generic card type...")
card = SimCardBase(scc) card = SimCardBase(scc)
generic_card = True generic_card = True
@@ -79,7 +76,7 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
# just means that pySim was unable to recognize the card profile. This # just means that pySim was unable to recognize the card profile. This
# may happen in particular with unprovisioned cards that do not have # may happen in particular with unprovisioned cards that do not have
# any files on them yet. # any files on them yet.
log.warning("Unsupported card type!") print("Unsupported card type!")
return None, card return None, card
# ETSI TS 102 221, Table 9.3 specifies a default for the PIN key # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
@@ -90,7 +87,7 @@ def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState,
if generic_card and isinstance(profile, CardProfileUICC): if generic_card and isinstance(profile, CardProfileUICC):
card._adm_chv_num = 0x0A card._adm_chv_num = 0x0A
log.info("Card is of type: %s", str(profile)) print("Info: Card is of type: %s" % str(profile))
# FIXME: this shouldn't really be here but somewhere else/more generic. # FIXME: this shouldn't really be here but somewhere else/more generic.
# We cannot do it within pySim/profile.py as that would create circular # We cannot do it within pySim/profile.py as that would create circular
+2 -2
View File
@@ -334,10 +334,10 @@ class ADF_ARAM(CardADF):
apdu_grp.add_argument( apdu_grp.add_argument(
'--apdu-filter', help='APDU filter: multiple groups of 8 hex bytes (4 byte CLA/INS/P1/P2 followed by 4 byte mask)') '--apdu-filter', help='APDU filter: multiple groups of 8 hex bytes (4 byte CLA/INS/P1/P2 followed by 4 byte mask)')
nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group() nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
nfc_grp.add_argument('--nfc-never', action='store_true',
help='NFC event access is not allowed')
nfc_grp.add_argument('--nfc-always', action='store_true', nfc_grp.add_argument('--nfc-always', action='store_true',
help='NFC event access is allowed') help='NFC event access is allowed')
nfc_grp.add_argument('--nfc-never', action='store_true',
help='NFC event access is not allowed')
store_ref_ar_do_parse.add_argument( store_ref_ar_do_parse.add_argument(
'--android-permissions', help='Android UICC Carrier Privilege Permissions (8 hex bytes)') '--android-permissions', help='Android UICC Carrier Privilege Permissions (8 hex bytes)')
+6 -69
View File
@@ -33,12 +33,10 @@ from Cryptodome.Cipher import AES
from osmocom.utils import h2b, b2h from osmocom.utils import h2b, b2h
from pySim.log import PySimLogger from pySim.log import PySimLogger
import os
import abc import abc
import csv import csv
import logging import logging
import yaml import yaml
import argparse
log = PySimLogger.get(__name__) log = PySimLogger.get(__name__)
@@ -132,31 +130,6 @@ class CardKeyFieldCryptor:
cipher = AES.new(h2b(self.transport_keys[field_name.upper()]), AES.MODE_CBC, self.__IV) cipher = AES.new(h2b(self.transport_keys[field_name.upper()]), AES.MODE_CBC, self.__IV)
return b2h(cipher.encrypt(h2b(plaintext_val))) return b2h(cipher.encrypt(h2b(plaintext_val)))
@staticmethod
def argparse_add_args(arg_parser: argparse.ArgumentParser):
arg_parser.add_argument('--column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
help='per-column AES transport key', dest='column_key')
# Depprecated argument, replaced by --column-key (see above)
arg_parser.add_argument('--csv-column-key', metavar='FIELD:AES_KEY_HEX', default=[], action='append',
help=argparse.SUPPRESS, dest='column_key')
@staticmethod
def transport_keys_from_opts(opts: argparse.Namespace) -> dict:
"""
Transport keys are passed via the commandline using the '--column-key' option. Each column requires a
dedicated transport key. This method can be used to extract the column keys parameters from the commandline
options into a dict that can be directly passed to the construtor with the transport_keys argument.
Args:
opts: parsed commandline options (Namespace)
"""
transport_keys = {}
for par in opts.column_key:
name, key = par.split(':')
transport_keys[name] = key
return transport_keys
class CardKeyProvider(abc.ABC): class CardKeyProvider(abc.ABC):
"""Base class, not containing any concrete implementation.""" """Base class, not containing any concrete implementation."""
@@ -175,33 +148,24 @@ class CardKeyProvider(abc.ABC):
fond None shall be returned. fond None shall be returned.
""" """
@staticmethod
def argparse_add_args(arg_parser: argparse.ArgumentParser):
"""
Add the commandline arguments relevant for this card key provider.
Args:
arg_parser : argument parser group
"""
def __str__(self): def __str__(self):
return type(self).__name__ return type(self).__name__
class CardKeyProviderCsv(CardKeyProvider): class CardKeyProviderCsv(CardKeyProvider):
"""Card key provider implementation that allows to query against a specified CSV file.""" """Card key provider implementation that allows to query against a specified CSV file."""
def __init__(self, csv_filename: str, field_cryptor: CardKeyFieldCryptor): def __init__(self, csv_filename: str, transport_keys: dict):
""" """
Args: Args:
csv_filename : file name (path) of CSV file containing card-individual key/data csv_filename : file name (path) of CSV file containing card-individual key/data
field_cryptor : (see class CardKeyFieldCryptor) transport_keys : (see class CardKeyFieldCryptor)
""" """
log.info("Using CSV file as card key data source: %s" % csv_filename) log.info("Using CSV file as card key data source: %s" % csv_filename)
self.csv_file = open(csv_filename, 'r') self.csv_file = open(csv_filename, 'r')
if not self.csv_file: if not self.csv_file:
raise RuntimeError("Could not open CSV file '%s'" % csv_filename) raise RuntimeError("Could not open CSV file '%s'" % csv_filename)
self.csv_filename = csv_filename self.csv_filename = csv_filename
self.crypt = field_cryptor self.crypt = CardKeyFieldCryptor(transport_keys)
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]: def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
self.csv_file.seek(0) self.csv_file.seek(0)
@@ -224,20 +188,14 @@ class CardKeyProviderCsv(CardKeyProvider):
return None return None
return return_dict return return_dict
@staticmethod
def argparse_add_args(arg_parser: argparse.ArgumentParser):
arg_parser.add_argument('--csv', metavar='FILE',
default="~/.osmocom/pysim/card_data.csv",
help='Read card data from CSV file')
class CardKeyProviderPgsql(CardKeyProvider): class CardKeyProviderPgsql(CardKeyProvider):
"""Card key provider implementation that allows to query against a specified PostgreSQL database table.""" """Card key provider implementation that allows to query against a specified PostgreSQL database table."""
def __init__(self, config_filename: str, field_cryptor: CardKeyFieldCryptor): def __init__(self, config_filename: str, transport_keys: dict):
""" """
Args: Args:
config_filename : file name (path) of CSV file containing card-individual key/data config_filename : file name (path) of CSV file containing card-individual key/data
field_cryptor : (see class CardKeyFieldCryptor) transport_keys : (see class CardKeyFieldCryptor)
""" """
import psycopg2 import psycopg2
log.info("Using SQL database as card key data source: %s" % config_filename) log.info("Using SQL database as card key data source: %s" % config_filename)
@@ -254,7 +212,7 @@ class CardKeyProviderPgsql(CardKeyProvider):
host=config.get('host')) host=config.get('host'))
self.tables = config.get('table_names') self.tables = config.get('table_names')
log.info("Card key database tables: %s" % str(self.tables)) log.info("Card key database tables: %s" % str(self.tables))
self.crypt = field_cryptor self.crypt = CardKeyFieldCryptor(transport_keys)
def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]: def get(self, fields: List[str], key: str, value: str) -> Dict[str, str]:
import psycopg2 import psycopg2
@@ -294,11 +252,6 @@ class CardKeyProviderPgsql(CardKeyProvider):
result[k] = self.crypt.decrypt_field(k, result.get(k)) result[k] = self.crypt.decrypt_field(k, result.get(k))
return result return result
@staticmethod
def argparse_add_args(arg_parser: argparse.ArgumentParser):
arg_parser.add_argument('--pgsql', metavar='FILE',
default="~/.osmocom/pysim/card_data_pgsql.cfg",
help='Read card data from PostgreSQL database (config file)')
def card_key_provider_register(provider: CardKeyProvider, provider_list=card_key_providers): def card_key_provider_register(provider: CardKeyProvider, provider_list=card_key_providers):
"""Register a new card key provider. """Register a new card key provider.
@@ -352,19 +305,3 @@ def card_key_provider_get_field(field: str, key: str, value: str, provider_list=
fields = [field] fields = [field]
result = card_key_provider_get(fields, key, value, card_key_providers) result = card_key_provider_get(fields, key, value, card_key_providers)
return result.get(field.upper()) return result.get(field.upper())
def card_key_provider_argparse_add_args(arg_parser: argparse.ArgumentParser):
"""Add card key provider commandline options to the given argument parser"""
card_key_group = arg_parser.add_argument_group('Card Key Provider Options')
CardKeyProviderCsv.argparse_add_args(card_key_group)
CardKeyProviderPgsql.argparse_add_args(card_key_group)
CardKeyFieldCryptor.argparse_add_args(card_key_group)
def card_key_provider_init(opts: argparse.Namespace):
"""Initialize card key provider depending on the user provided commandline options"""
transport_keys = CardKeyFieldCryptor.transport_keys_from_opts(opts)
card_key_field_cryptor = CardKeyFieldCryptor(transport_keys)
if os.path.isfile(os.path.expanduser(opts.csv)):
card_key_provider_register(CardKeyProviderCsv(os.path.expanduser(opts.csv), card_key_field_cryptor))
if os.path.isfile(os.path.expanduser(opts.pgsql)):
card_key_provider_register(CardKeyProviderPgsql(os.path.expanduser(opts.pgsql), card_key_field_cryptor))
+20 -20
View File
@@ -20,25 +20,22 @@
import copy import copy
import pprint import pprint
from typing import Generator, Union from typing import List, Generator
from pySim.esim.saip.personalization import ConfigurableParameter from pySim.esim.saip.personalization import ConfigurableParameter
from pySim.esim.saip import param_source from pySim.esim.saip import param_source
from pySim.esim.saip import ProfileElementSequence, ProfileElementSD from pySim.esim.saip import ProfileElementSequence, ProfileElementSD
from pySim.global_platform import KeyUsageQualifier from pySim.global_platform import KeyUsageQualifier
from osmocom.utils import b2h from osmocom.utils import b2h
# a list of ConfigurableParameter classes and/or ConfigurableParameter class instances
ParamList = list[Union[type[ConfigurableParameter], ConfigurableParameter]]
class BatchPersonalization: class BatchPersonalization:
"""Produce a series of eSIM profiles from predefined parameters. """Produce a series of eSIM profiles from predefined parameters.
Personalization parameters are derived from pysim.esim.saip.param_source.ParamSource. Personalization parameters are derived from pysim.esim.saip.param_source.ParamSource.
Usage example: Usage example:
der_input = open('some_file', 'rb').read() der_input = some_file.open('rb').read()
pes = ProfileElementSequence.from_der(der_input) pes = ProfileElementSequence.from_der(der_input)
p = BatchPersonalization( p = pers.BatchPersonalization(
n=10, n=10,
src_pes=pes, src_pes=pes,
csv_rows=get_csv_reader()) csv_rows=get_csv_reader())
@@ -71,7 +68,7 @@ class BatchPersonalization:
def __init__(self, def __init__(self,
n: int, n: int,
src_pes: ProfileElementSequence, src_pes: ProfileElementSequence,
params: list[ParamAndSrc]=None, params: list[ParamAndSrc]=[],
csv_rows: Generator=None, csv_rows: Generator=None,
): ):
""" """
@@ -134,14 +131,14 @@ class UppAudit(dict):
""" """
@classmethod @classmethod
def from_der(cls, der: bytes, params: ParamList, der_size=False, additional_sd_keys=False): def from_der(cls, der: bytes, params: List, der_size=False, additional_sd_keys=False):
"""return a dict of parameter name and set of selected parameter values found in a DER encoded profile. Note: """return a dict of parameter name and set of selected parameter values found in a DER encoded profile. Note:
some ConfigurableParameter implementations return more than one key-value pair, for example, Imsi returns some ConfigurableParameter implementations return more than one key-value pair, for example, Imsi returns
both 'IMSI' and 'IMSI-ACC' parameters. both 'IMSI' and 'IMSI-ACC' parameters.
e.g. e.g.
UppAudit.from_der(my_der, [Imsi, ]) UppAudit.from_der(my_der, [Imsi, ])
--> {'IMSI': {'001010000000023'}, 'IMSI-ACC': {'5'}} --> {'IMSI': '001010000000023', 'IMSI-ACC': '5'}
(where 'IMSI' == Imsi.name) (where 'IMSI' == Imsi.name)
@@ -186,11 +183,11 @@ class UppAudit(dict):
audit_key = f'SdKey_KVN{key.key_version_number:02x}_ID{key.key_identifier:02x}' audit_key = f'SdKey_KVN{key.key_version_number:02x}_ID{key.key_identifier:02x}'
kuq_bin = KeyUsageQualifier.build(key.key_usage_qualifier).hex() kuq_bin = KeyUsageQualifier.build(key.key_usage_qualifier).hex()
audit_val = f'{key.key_components=!r} key_usage_qualifier=0x{kuq_bin}={key.key_usage_qualifier!r}' audit_val = f'{key.key_components=!r} key_usage_qualifier=0x{kuq_bin}={key.key_usage_qualifier!r}'
upp_audit.add_values({audit_key: audit_val}) upp_audit[audit_key] = set((audit_val, ))
return upp_audit return upp_audit
def get_single_val(self, key, allow_absent=False, absent_val=None): def get_single_val(self, key, validate=True, allow_absent=False, absent_val=None):
""" """
Return the audit's value for the given audit key (like 'IMSI' or 'IMSI-ACC'). Return the audit's value for the given audit key (like 'IMSI' or 'IMSI-ACC').
Any kind of value may occur multiple times in a profile. When all of these agree to the same unambiguous value, Any kind of value may occur multiple times in a profile. When all of these agree to the same unambiguous value,
@@ -230,7 +227,7 @@ class UppAudit(dict):
v = try_single_val(v) v = try_single_val(v)
if isinstance(v, bytes): if isinstance(v, bytes):
v = b2h(v) v = bytes_to_hexstr(v)
if v is None: if v is None:
return 'not present' return 'not present'
return str(v) return str(v)
@@ -240,21 +237,21 @@ class UppAudit(dict):
return UppAudit.audit_val_to_str(self.get(key)) return UppAudit.audit_val_to_str(self.get(key))
def add_values(self, src:dict): def add_values(self, src:dict):
"""Merge a plain dict of values into self, which is a dict of sets. """self and src are both a dict of sets.
For example from For example from
self == { 'a': {123} } self == { 'a': set((123,)) }
and and
src == { 'a': 456, 'b': 789 } src == { 'a': set((456,)), 'b': set((789,)) }
then after this function call: then after this function call:
self == { 'a': {123, 456}, 'b': {789} } self == { 'a': set((123, 456,)), 'b': set((789,)) }
""" """
assert isinstance(src, dict) assert isinstance(src, dict)
for key, srcval in src.items(): for key, srcvalset in src.items():
dstvalset = self.get(key) dstvalset = self.get(key)
if dstvalset is None: if dstvalset is None:
dstvalset = set() dstvalset = set()
self[key] = dstvalset self[key] = dstvalset
dstvalset.add(srcval) dstvalset.add(srcvalset)
def __str__(self): def __str__(self):
return '\n'.join(f'{key}: {self.get_val_str(key)}' for key in sorted(self.keys())) return '\n'.join(f'{key}: {self.get_val_str(key)}' for key in sorted(self.keys()))
@@ -279,7 +276,7 @@ class BatchAudit(list):
BatchAudit itself is a list, callers may use the standard python list API to access the UppAudit instances. BatchAudit itself is a list, callers may use the standard python list API to access the UppAudit instances.
""" """
def __init__(self, params: ParamList): def __init__(self, params:List):
assert params assert params
self.params = params self.params = params
@@ -336,6 +333,9 @@ class BatchAudit(list):
for audit in self: for audit in self:
yield (audit.get_single_val(col, allow_absent=True, absent_val="") for col in columns) yield (audit.get_single_val(col, allow_absent=True, absent_val="") for col in columns)
def bytes_to_hexstr(b:bytes, sep=''):
return sep.join(f'{x:02x}' for x in b)
def esim_profile_introspect(upp): def esim_profile_introspect(upp):
pes = ProfileElementSequence.from_der(upp.read()) pes = ProfileElementSequence.from_der(upp.read())
d = {} d = {}
@@ -343,7 +343,7 @@ def esim_profile_introspect(upp):
def show_bytes_as_hexdump(item): def show_bytes_as_hexdump(item):
if isinstance(item, bytes): if isinstance(item, bytes):
return b2h(item) return bytes_to_hexstr(item)
if isinstance(item, list): if isinstance(item, list):
return list(show_bytes_as_hexdump(i) for i in item) return list(show_bytes_as_hexdump(i) for i in item)
if isinstance(item, tuple): if isinstance(item, tuple):
+27 -19
View File
@@ -129,24 +129,27 @@ class RandomSourceMixin:
class RandomDigitSource(DecimalRangeSource, RandomSourceMixin): class RandomDigitSource(DecimalRangeSource, RandomSourceMixin):
"""return a different sequence of random decimal digits each""" """return a different sequence of random decimal digits each"""
name = "random decimal digits" name = "random decimal digits"
used_keys = set()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.used_keys = set()
def get_next(self, csv_row:dict=None): def get_next(self, csv_row:dict=None):
# try to generate random digits that are always different from previously produced random digits # try to generate random digits that are always different from previously produced random bytes
for _ in range(10): attempts = 10
while True:
val = self.random_impl.randint(self.first_value, self.last_value) val = self.random_impl.randint(self.first_value, self.last_value)
if val not in self.used_keys: if val in RandomDigitSource.used_keys:
attempts -= 1
if attempts:
continue
RandomDigitSource.used_keys.add(val)
break break
self.used_keys.add(val)
return self.val_to_digit(val) return self.val_to_digit(val)
class RandomHexDigitSource(InputExpandingParamSource, RandomSourceMixin): class RandomHexDigitSource(InputExpandingParamSource, RandomSourceMixin):
"""return a different sequence of random hexadecimal digits each""" """return a different sequence of random hexadecimal digits each"""
name = "random hexadecimal digits" name = "random hexadecimal digits"
numeric_base = 16 numeric_base = 16
used_keys = set()
def __init__(self, input_str:str): def __init__(self, input_str:str):
super().__init__(input_str) super().__init__(input_str)
input_str = self.input_str input_str = self.input_str
@@ -158,15 +161,18 @@ class RandomHexDigitSource(InputExpandingParamSource, RandomSourceMixin):
if (num_digits & 1) != 0: if (num_digits & 1) != 0:
raise ValueError(f"hexadecimal value should have even number of digits, not {num_digits}") raise ValueError(f"hexadecimal value should have even number of digits, not {num_digits}")
self.num_digits = num_digits self.num_digits = num_digits
self.used_keys = set()
def get_next(self, csv_row:dict=None): def get_next(self, csv_row:dict=None):
# try to generate random bytes that are always different from previously produced random bytes # try to generate random bytes that are always different from previously produced random bytes
for _ in range(10): attempts = 10
while True:
val = self.random_impl.randbytes(self.num_digits // 2) val = self.random_impl.randbytes(self.num_digits // 2)
if val not in self.used_keys: if val in RandomHexDigitSource.used_keys:
attempts -= 1
if attempts:
continue
RandomHexDigitSource.used_keys.add(val)
break break
self.used_keys.add(val)
return b2h(val) return b2h(val)
@@ -175,9 +181,8 @@ class IncDigitSource(DecimalRangeSource):
name = "incrementing decimal digits" name = "incrementing decimal digits"
def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None): 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 """input_str: the first value to return, a string of an integer number with optional leading zero digits. The
just 'FIRST' (iterates to the maximum value for the given digit width). Leading zeros in leading zero digits are preserved."""
FIRST determine the digit width and are preserved in returned values."""
super().__init__(input_str, num_digits, first_value, last_value) super().__init__(input_str, num_digits, first_value, last_value)
self.next_val = None self.next_val = None
self.reset() self.reset()
@@ -206,9 +211,12 @@ class CsvSource(ParamSource):
name = "from CSV" name = "from CSV"
def __init__(self, input_str:str): def __init__(self, input_str:str):
"""input_str: the CSV column name to read values from. """self.csv_column = input_str:
The caller passes the current CSV row to get_next(), from which CsvSource picks the column matching column name indicating the column to use for this parameter.
this name.""" This name is used in get_next(): the caller passes the current CSV row to get_next(), from which
CsvSource picks the column with the name matching csv_column.
"""
"""Parse input_str into self.num_digits, self.first_value, self.last_value."""
super().__init__(input_str) super().__init__(input_str)
self.csv_column = self.input_str self.csv_column = self.input_str
@@ -216,6 +224,6 @@ class CsvSource(ParamSource):
val = None val = None
if csv_row: if csv_row:
val = csv_row.get(self.csv_column) val = csv_row.get(self.csv_column)
if val is None: if not val:
raise ParamSourceUndefinedExn(f"no value for CSV column {self.csv_column!r}") raise ParamSourceUndefinedExn(f"no value for CSV column {self.csv_column!r}")
return val return val
+133 -56
View File
@@ -16,14 +16,17 @@
# 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 os
import re import re
import pprint
from typing import List, Tuple, Generator, Optional from typing import List, Tuple, Generator, Optional
from construct.core import StreamError
from osmocom.tlv import camel_to_snake from osmocom.tlv import camel_to_snake
from osmocom.utils import hexstr from osmocom.utils import hexstr
from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid from pySim.utils import enc_iccid, dec_iccid, enc_imsi, dec_imsi, h2b, b2h, rpad, sanitize_iccid
from pySim.ts_31_102 import EF_AD
from pySim.ts_51_011 import EF_SMSP from pySim.ts_51_011 import EF_SMSP
from pySim.esim.saip import param_source from pySim.esim.saip import param_source
from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence from pySim.esim.saip import ProfileElement, ProfileElementSD, ProfileElementSequence
@@ -418,69 +421,67 @@ class BinaryParam(ConfigurableParameter):
class EnumParam(ConfigurableParameter): class EnumParam(ConfigurableParameter):
"""ConfigurableParameter for named integer enumeration values. value_map = {
# For example:
Subclasses must define a nested enum.IntEnum named 'Values' listing all valid names and their #'Meaningful label for value 23': 0x23,
integer codes. apply_val() and get_values_from_pes() are not implemented here and this must # Where 0x23 is a valid value to use for apply_val().
be inherited from another mixin.""" }
_value_map_reverse = None
class Values(enum.IntEnum):
pass # subclasses override this
@classmethod @classmethod
def validate_val(cls, val) -> int: def validate_val(cls, val):
if isinstance(val, int): orig_val = val
try: enum_val = None
return int(cls.Values(val)) if isinstance(val, str):
except ValueError: enum_name = val
pass enum_val = cls.map_name_to_val(enum_name)
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) # if the str is not one of the known value_map.keys(), is it maybe one of value_map.keys()?
raise ValueError(f"{cls.get_name()}: invalid argument: {val!r}. Valid arguments are: {valid}") 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 @classmethod
def map_name_to_val(cls, name: str, strict=True) -> int: def map_name_to_val(cls, name:str, strict=True):
"""Return the integer value for a given enum member name. Performs an exact match first, val = cls.value_map.get(name)
then falls back to fuzzy matching (case-insensitive, punctuation-insensitive).""" if val is not None:
try: return val
return int(cls.Values[name])
except KeyError:
pass
clean = cls.clean_name_str(name) clean_name = cls.clean_name_str(name)
for member in cls.Values: for k, v in cls.value_map.items():
if cls.clean_name_str(member.name) == clean: if clean_name == cls.clean_name_str(k):
return int(member) return v
if strict: if strict:
valid = ', '.join(m.name for m in cls.Values) raise ValueError(f"Problem in {cls.get_name()}: {name!r} is not a known value."
raise ValueError(f"{cls.get_name()}: {name!r} is not a known value. Known values are: {valid}") f" Known values are: {cls.value_map.keys()!r}")
return None return None
@classmethod @classmethod
def map_val_to_name(cls, val, strict=False) -> str: def map_val_to_name(cls, val, strict=False) -> str:
"""Return the enum member name for a given integer value.""" if cls._value_map_reverse is None:
try: cls._value_map_reverse = dict((v, k) for k, v in cls.value_map.items())
return cls.Values(val).name
except ValueError: name = cls._value_map_reverse.get(val)
if name:
return name
if strict: if strict:
raise ValueError(f"{cls.get_name()}: {val!r} ({type(val).__name__}) is not a known value.") 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 return None
@classmethod @classmethod
def name_normalize(cls, name: str) -> str: def name_normalize(cls, name:str) -> str:
"""Map a (possibly fuzzy) name to its canonical enum member name.""" return cls.map_val_to_name(cls.map_name_to_val(name))
return cls.Values(cls.map_name_to_val(name)).name
@classmethod @classmethod
def clean_name_str(cls, val: str) -> str: def clean_name_str(cls, val):
"""Strip punctuation and case for fuzzy name comparison. return re.sub('[^0-9A-Za-z-_]', '', val).lower()
Treats hyphens and underscores as equivalent (both removed)."""
return re.sub('[^0-9A-Za-z]', '', val).lower()
class Iccid(DecimalParam): class Iccid(DecimalParam):
@@ -645,9 +646,15 @@ class SmspTpScAddr(ConfigurableParameter):
@classmethod @classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence): def get_values_from_pes(cls, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('usim'): for pe in pes.get_pes_for_type('usim'):
f_smsp = pe.files['ef-smsp'] f_smsp = pe.files.get('ef-smsp', None)
if f_smsp is None:
continue
try:
ef_smsp = EF_SMSP() ef_smsp = EF_SMSP()
ef_smsp_dec = ef_smsp.decode_record_bin(f_smsp.body, 1) ef_smsp_dec = ef_smsp.decode_record_bin(f_smsp.body, 1)
except IndexError:
continue
tp_sc_addr = ef_smsp_dec.get('tp_sc_addr', None) tp_sc_addr = ef_smsp_dec.get('tp_sc_addr', None)
@@ -660,12 +667,78 @@ class SmspTpScAddr(ConfigurableParameter):
yield { cls.name: cls.tuple_to_str((international, digits)) } yield { cls.name: cls.tuple_to_str((international, digits)) }
class MncLen(ConfigurableParameter):
"""MNC length. Must be either 2 or 3. Sets only the MNC length field in EF-AD (Administrative Data)."""
name = 'MNC-LEN'
allow_chars = '23'
strip_chars = ' \t\r\n'
numeric_base = 10
max_len = 1
min_len = 1
example_input = '2'
default_source = param_source.ConstantSource
@classmethod
def validate_val(cls, val):
val = super().validate_val(val)
val = int(val)
if val not in (2, 3):
raise ValueError(f"MNC-LEN must be either 2 or 3, not {val!r}")
return val
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val):
"""val must be an int: either 2 or 3"""
for pe in pes.get_pes_for_type('usim'):
if not hasattr(pe, 'files'):
continue
# decode existing values
f_ad = pe.files['ef-ad']
if not f_ad.body:
continue
try:
ef_ad = EF_AD()
ef_ad_dec = ef_ad.decode_bin(f_ad.body)
except StreamError:
continue
if 'mnc_len' not in ef_ad_dec:
continue
# change mnc_len
ef_ad_dec['mnc_len'] = val
# re-encode into the File body
f_ad.body = ef_ad.encode_bin(ef_ad_dec)
pe.file2pe(f_ad)
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
for naa in ('isim',):# 'isim', 'csim'):
for pe in pes.get_pes_for_type(naa):
if not hasattr(pe, 'files'):
continue
f_ad = pe.files.get('ef-ad', None)
if f_ad is None:
continue
try:
ef_ad = EF_AD()
ef_ad_dec = ef_ad.decode_bin(f_ad.body)
except StreamError:
continue
mnc_len = ef_ad_dec.get('mnc_len', None)
if mnc_len is None:
continue
yield { cls.name: str(mnc_len) }
class SdKey(BinaryParam): class SdKey(BinaryParam):
"""Configurable Security Domain (SD) Key. Value is presented as bytes. """Configurable Security Domain (SD) Key. Value is presented as bytes.
Non-abstract implementations are generated in SdKey.generate_sd_key_classes""" Non-abstract implementations are generated in SdKey.generate_sd_key_classes"""
# these will be set by subclasses # these will be set by subclasses
key_type = None key_type = None
kvn = None kvn = None
reserved_kvn = tuple() # tuple of all reserved kvn for a given SCPxx
key_id = None key_id = None
key_usage_qual = None key_usage_qual = None
@@ -711,6 +784,8 @@ class SdKey(BinaryParam):
yield { cls.name: b2h(kc) } yield { cls.name: b2h(kc) }
NO_OP = (('', {}))
LEN_128 = (16,) LEN_128 = (16,)
LEN_128_192_256 = (16, 24, 32) LEN_128_192_256 = (16, 24, 32)
LEN_128_256 = (16, 32) LEN_128_256 = (16, 32)
@@ -1028,20 +1103,22 @@ class AlgoConfig(ConfigurableParameter):
yield { cls.name: val } yield { cls.name: val }
class AlgorithmID(EnumParam, AlgoConfig): class AlgorithmID(EnumParam, AlgoConfig):
"""use validate_val() from EnumParam, and apply_val() from AlgoConfig. '''use validate_val() from EnumParam, and apply_val() from AlgoConfig.
In get_values_from_pes(), return enum value names, not raw values.""" In get_values_from_pes(), return enum value names, not raw values.'''
name = "Algorithm" name = "Algorithm"
algo_config_key = 'algorithmID'
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
value_map = {
"Milenage" : 1,
"TUAK" : 2,
"usim-test" : 3,
}
example_input = "Milenage" example_input = "Milenage"
default_source = param_source.ConstantSource default_source = param_source.ConstantSource
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn algo_config_key = 'algorithmID'
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 # EnumParam.validate_val() returns the int values from value_map
@classmethod @classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence): def get_values_from_pes(cls, pes: ProfileElementSequence):
+3 -41
View File
@@ -226,28 +226,9 @@ class Icon(BER_TLV_IE, tag=0x94):
_construct = GreedyBytes _construct = GreedyBytes
class ProfileClass(BER_TLV_IE, tag=0x95): class ProfileClass(BER_TLV_IE, tag=0x95):
_construct = Enum(Int8ub, test=0, provisioning=1, operational=2) _construct = Enum(Int8ub, test=0, provisioning=1, operational=2)
class ProfilePolicyRules(BER_TLV_IE, tag=0x99):
_construct = GreedyBytes
class NotificationConfigurationInfo(BER_TLV_IE, tag=0xb6):
_construct = GreedyBytes
# ProfileOwner
class ProfileOwnerPLMN(BER_TLV_IE, tag=0x80):
_construct = PlmnAdapter(Bytes(3))
class ProfileOwnerGID1(BER_TLV_IE, tag=0x81):
_construct = GreedyBytes
class ProfileOwnerGID2(BER_TLV_IE, tag=0x82):
_construct = GreedyBytes
class ProfileOwner(BER_TLV_IE, tag=0xb7, nested=[ProfileOwnerPLMN, ProfileOwnerGID1, ProfileOwnerGID2]):
_construct = GreedyBytes
class SMDPPProprietaryData(BER_TLV_IE, tag=0xb8):
_construct = GreedyBytes
class ProfileInfo(BER_TLV_IE, tag=0xe3, nested=[Iccid, IsdpAid, ProfileState, ProfileNickname, class ProfileInfo(BER_TLV_IE, tag=0xe3, nested=[Iccid, IsdpAid, ProfileState, ProfileNickname,
ServiceProviderName, ProfileName, IconType, Icon, ServiceProviderName, ProfileName, IconType, Icon,
ProfileClass, ProfilePolicyRules, NotificationConfigurationInfo, ProfileClass]): # FIXME: more IEs
ProfileOwner, SMDPPProprietaryData]):
pass pass
class ProfileInfoSeq(BER_TLV_IE, tag=0xa0, nested=[ProfileInfo]): class ProfileInfoSeq(BER_TLV_IE, tag=0xa0, nested=[ProfileInfo]):
pass pass
@@ -463,28 +444,9 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
d = rn.to_dict() d = rn.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['notification_sent_resp'])) self._cmd.poutput_json(flatten_dict_lists(d['notification_sent_resp']))
get_profiles_info_parser = argparse.ArgumentParser() def do_get_profiles_info(self, _opts):
get_profiles_info_parser.add_argument('--all', action='store_true', help='Retrieve all known tags of a profile')
@cmd2.with_argparser(get_profiles_info_parser)
def do_get_profiles_info(self, opts):
"""Perform an ES10c GetProfilesInfo function.""" """Perform an ES10c GetProfilesInfo function."""
if opts.all: pi = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
tags = [nest.tag for nest in ProfileInfo.nested_collection_cls().nested]
u8tags = []
# TODO: rework TagList to support 2 byte tags to not filter it into u8 tags
for tag in tags:
if tag <= 255:
u8tags.append(tag)
elif tag <= 65535:
u8tags.append(tag >> 8)
u8tags.append(tag & 0xff)
# Ignoring 3 byte tags
req = ProfileInfoListReq(children=[TagList(decoded=u8tags)])
else:
req = ProfileInfoListReq()
pi = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, req, ProfileInfoListResp)
d = pi.to_dict() d = pi.to_dict()
self._cmd.poutput_json(flatten_dict_lists(d['profile_info_list_resp'])) self._cmd.poutput_json(flatten_dict_lists(d['profile_info_list_resp']))
+2 -5
View File
@@ -44,7 +44,6 @@ from pySim.utils import sw_match, decomposeATR
from pySim.jsonpath import js_path_modify from pySim.jsonpath import js_path_modify
from pySim.commands import SimCardCommands from pySim.commands import SimCardCommands
from pySim.exceptions import SwMatchError from pySim.exceptions import SwMatchError
from pySim.log import PySimLogger
# int: a single service is associated with this file # int: a single service is associated with this file
# list: any of the listed services requires this file # list: any of the listed services requires this file
@@ -53,8 +52,6 @@ CardFileService = Union[int, List[int], Tuple[int, ...]]
Size = Tuple[int, Optional[int]] Size = Tuple[int, Optional[int]]
log = PySimLogger.get(__name__)
class CardFile: class CardFile:
"""Base class for all objects in the smart card filesystem. """Base class for all objects in the smart card filesystem.
Serve as a common ancestor to all other file types; rarely used directly. Serve as a common ancestor to all other file types; rarely used directly.
@@ -1612,14 +1609,14 @@ class CardModel(abc.ABC):
card_atr = scc.get_atr() card_atr = scc.get_atr()
for atr in cls._atrs: for atr in cls._atrs:
if atr == card_atr: if atr == card_atr:
log.info("Detected CardModel: %s", cls.__name__) print("Detected CardModel:", cls.__name__)
return True return True
# if nothing found try to just compare the Historical Bytes of the ATR # if nothing found try to just compare the Historical Bytes of the ATR
card_atr_hb = decomposeATR(card_atr)['hb'] card_atr_hb = decomposeATR(card_atr)['hb']
for atr in cls._atrs: for atr in cls._atrs:
atr_hb = decomposeATR(atr)['hb'] atr_hb = decomposeATR(atr)['hb']
if atr_hb == card_atr_hb: if atr_hb == card_atr_hb:
log.info("Detected CardModel: %s", cls.__name__) print("Detected CardModel:", cls.__name__)
return True return True
return False return False
+17 -17
View File
@@ -27,9 +27,9 @@ from osmocom.utils import b2h
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
from pySim.utils import parse_command_apdu from pySim.utils import parse_command_apdu
from pySim.secure_channel import SecureChannel from pySim.secure_channel import SecureChannel
from pySim.log import PySimLogger
log = PySimLogger.get(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> bytes: def scp02_key_derivation(constant: bytes, counter: int, base_key: bytes) -> bytes:
assert len(constant) == 2 assert len(constant) == 2
@@ -75,7 +75,7 @@ class Scp02SessionKeys:
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)]))) h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
h = d.decrypt(h) h = d.decrypt(h)
h = e.encrypt(h) h = e.encrypt(h)
log.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h)) logger.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h))
if self.des_icv_enc: if self.des_icv_enc:
self.icv = self.des_icv_enc.encrypt(h) self.icv = self.des_icv_enc.encrypt(h)
else: else:
@@ -89,7 +89,7 @@ class Scp02SessionKeys:
h = b'\x00' * 8 h = b'\x00' * 8
for i in range(q): for i in range(q):
h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)]))) h = e.encrypt(strxor(h, bytes(padded_data[8*i:8*(i+1)])))
log.debug("mac_3des(%s) -> %s", b2h(data), b2h(h)) logger.debug("mac_3des(%s) -> %s", b2h(data), b2h(h))
return h return h
def __init__(self, counter: int, card_keys: 'GpCardKeyset', icv_encrypt=True): def __init__(self, counter: int, card_keys: 'GpCardKeyset', icv_encrypt=True):
@@ -276,10 +276,10 @@ class SCP02(SCP):
return cipher.decrypt(ciphertext) return cipher.decrypt(ciphertext)
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes): def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):
log.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge)) logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
self.host_cryptogram = self.sk.calc_mac_3des(self.sk.counter.to_bytes(2, 'big') + card_challenge + host_challenge) self.host_cryptogram = self.sk.calc_mac_3des(self.sk.counter.to_bytes(2, 'big') + card_challenge + host_challenge)
self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge) self.card_cryptogram = self.sk.calc_mac_3des(self.host_challenge + self.sk.counter.to_bytes(2, 'big') + card_challenge)
log.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram)) logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes: def gen_init_update_apdu(self, host_challenge: bytes = b'\x00'*8) -> bytes:
"""Generate INITIALIZE UPDATE APDU.""" """Generate INITIALIZE UPDATE APDU."""
@@ -291,7 +291,7 @@ class SCP02(SCP):
resp = self.constr_iur.parse(resp_bin) resp = self.constr_iur.parse(resp_bin)
self.card_challenge = resp['card_challenge'] self.card_challenge = resp['card_challenge']
self.sk = Scp02SessionKeys(resp['seq_counter'], self.card_keys) self.sk = Scp02SessionKeys(resp['seq_counter'], self.card_keys)
log.debug(self.sk) logger.debug(self.sk)
self._compute_cryptograms(self.card_challenge, self.host_challenge) self._compute_cryptograms(self.card_challenge, self.host_challenge)
if self.card_cryptogram != resp['card_cryptogram']: if self.card_cryptogram != resp['card_cryptogram']:
raise ValueError("card cryptogram doesn't match") raise ValueError("card cryptogram doesn't match")
@@ -311,7 +311,7 @@ class SCP02(SCP):
def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes: def _wrap_cmd_apdu(self, apdu: bytes, *args, **kwargs) -> bytes:
"""Wrap Command APDU for SCP02: calculate MAC and encrypt.""" """Wrap Command APDU for SCP02: calculate MAC and encrypt."""
log.debug("wrap_cmd_apdu(%s)", b2h(apdu)) logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
if not self.do_cmac: if not self.do_cmac:
return apdu return apdu
@@ -378,7 +378,7 @@ def scp03_key_derivation(constant: bytes, context: bytes, base_key: bytes, l: Op
if l is None: if l is None:
l = len(base_key) * 8 l = len(base_key) * 8
log.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l) logger.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l)
output_len = l // 8 output_len = l // 8
# SCP03 Section 4.1.5 defines a different parameter order than NIST SP 800-108, so we cannot use the # SCP03 Section 4.1.5 defines a different parameter order than NIST SP 800-108, so we cannot use the
# existing Cryptodome.Protocol.KDF.SP800_108_Counter function :( # existing Cryptodome.Protocol.KDF.SP800_108_Counter function :(
@@ -451,7 +451,7 @@ class Scp03SessionKeys:
# This block SHALL be encrypted with S-ENC to produce the ICV for command encryption. # This block SHALL be encrypted with S-ENC to produce the ICV for command encryption.
cipher = AES.new(self.s_enc, AES.MODE_CBC, iv) cipher = AES.new(self.s_enc, AES.MODE_CBC, iv)
icv = cipher.encrypt(data) icv = cipher.encrypt(data)
log.debug("_get_icv(data=%s, is_resp=%s) -> icv=%s", b2h(data), is_response, b2h(icv)) logger.debug("_get_icv(data=%s, is_resp=%s) -> icv=%s", b2h(data), is_response, b2h(icv))
return icv return icv
# TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-wrapping # TODO: Resolve duplication with pySim.esim.bsp.BspAlgoCryptAES128 which provides pad80-wrapping
@@ -489,12 +489,12 @@ class SCP03(SCP):
return cipher.decrypt(ciphertext) return cipher.decrypt(ciphertext)
def _compute_cryptograms(self): def _compute_cryptograms(self):
log.debug("host_challenge(%s), card_challenge(%s)", b2h(self.host_challenge), b2h(self.card_challenge)) logger.debug("host_challenge(%s), card_challenge(%s)", b2h(self.host_challenge), b2h(self.card_challenge))
# Card + Host Authentication Cryptogram: Section 6.2.2.2 + 6.2.2.3 # Card + Host Authentication Cryptogram: Section 6.2.2.2 + 6.2.2.3
context = self.host_challenge + self.card_challenge context = self.host_challenge + self.card_challenge
self.card_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_CARD, context, self.sk.s_mac, l=self.s_mode*8) self.card_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_CARD, context, self.sk.s_mac, l=self.s_mode*8)
self.host_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_HOST, context, self.sk.s_mac, l=self.s_mode*8) self.host_cryptogram = scp03_key_derivation(self.sk.DERIV_CONST_AUTH_CGRAM_HOST, context, self.sk.s_mac, l=self.s_mode*8)
log.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram)) logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes: def gen_init_update_apdu(self, host_challenge: Optional[bytes] = None) -> bytes:
"""Generate INITIALIZE UPDATE APDU.""" """Generate INITIALIZE UPDATE APDU."""
@@ -514,7 +514,7 @@ class SCP03(SCP):
self.i_param = resp['i_param'] self.i_param = resp['i_param']
# derive session keys and compute cryptograms # derive session keys and compute cryptograms
self.sk = Scp03SessionKeys(self.card_keys, self.host_challenge, self.card_challenge) self.sk = Scp03SessionKeys(self.card_keys, self.host_challenge, self.card_challenge)
log.debug(self.sk) logger.debug(self.sk)
self._compute_cryptograms() self._compute_cryptograms()
# verify computed cryptogram matches received cryptogram # verify computed cryptogram matches received cryptogram
if self.card_cryptogram != resp['card_cryptogram']: if self.card_cryptogram != resp['card_cryptogram']:
@@ -529,7 +529,7 @@ class SCP03(SCP):
def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes: def _wrap_cmd_apdu(self, apdu: bytes, skip_cenc: bool = False) -> bytes:
"""Wrap Command APDU for SCP03: calculate MAC and encrypt.""" """Wrap Command APDU for SCP03: calculate MAC and encrypt."""
log.debug("wrap_cmd_apdu(%s)", b2h(apdu)) logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
if not self.do_cmac: if not self.do_cmac:
return apdu return apdu
@@ -584,7 +584,7 @@ class SCP03(SCP):
# status word: in this case only the status word shall be returned in the response. All status words # status word: in this case only the status word shall be returned in the response. All status words
# except '9000' and warning status words (i.e. '62xx' and '63xx') shall be interpreted as error status # except '9000' and warning status words (i.e. '62xx' and '63xx') shall be interpreted as error status
# words. # words.
log.debug("unwrap_rsp_apdu(sw=%s, rsp_apdu=%s)", sw, rsp_apdu) logger.debug("unwrap_rsp_apdu(sw=%s, rsp_apdu=%s)", sw, rsp_apdu)
if not self.do_rmac: if not self.do_rmac:
assert not self.do_renc assert not self.do_renc
return rsp_apdu return rsp_apdu
@@ -600,9 +600,9 @@ class SCP03(SCP):
if self.do_renc: if self.do_renc:
# decrypt response data # decrypt response data
decrypted = self.sk._decrypt(response_data) decrypted = self.sk._decrypt(response_data)
log.debug("decrypted: %s", b2h(decrypted)) logger.debug("decrypted: %s", b2h(decrypted))
# remove padding # remove padding
response_data = unpad80(decrypted) response_data = unpad80(decrypted)
log.debug("response_data: %s", b2h(response_data)) logger.debug("response_data: %s", b2h(response_data))
return response_data return response_data
+1 -2
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']):
+1 -1
View File
@@ -44,7 +44,7 @@ class PySimLogger:
""" """
LOG_FMTSTR = "%(levelname)s: %(message)s" LOG_FMTSTR = "%(levelname)s: %(message)s"
LOG_FMTSTR_VERBOSE = "%(name)s.%(lineno)d -- " + LOG_FMTSTR LOG_FMTSTR_VERBOSE = "%(module)s.%(lineno)d -- " + LOG_FMTSTR
__formatter = logging.Formatter(LOG_FMTSTR) __formatter = logging.Formatter(LOG_FMTSTR)
__formatter_verbose = logging.Formatter(LOG_FMTSTR_VERBOSE) __formatter_verbose = logging.Formatter(LOG_FMTSTR_VERBOSE)
+12 -42
View File
@@ -301,54 +301,24 @@ class LinkBaseTpdu(LinkBase):
prev_tpdu = tpdu prev_tpdu = tpdu
data, sw = self.send_tpdu(tpdu) data, sw = self.send_tpdu(tpdu)
log.debug("T0: case #%u TPDU: %s => %s %s", case, tpdu, data or "(no data)", sw or "(no status word)")
if sw is None:
raise ValueError("no status word received")
# After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further # When we have sent the first APDU, the SW may indicate that there are response bytes
# TPDUs have to be sent in order to complete the task. # available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
if case == 4 or self.apdu_strict == False: # xx is the number of response bytes available.
# In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data # See also:
# available which has to be retrieved using a GET RESPONSE command TPDU. if sw is not None:
# while (sw[0:2] in ['9f', '61', '62', '63']):
# ETSI TS 102 221, section 7.3.1.1.4 is very cleare about the fact that the GET RESPONSE mechanism # SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# shall only apply on case #4 APDUs but unfortunately it is impossible to distinguish between case #3 # SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# and case #4 when the APDU format is not strictly followed. In order to be able to detect case #4 # SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
# correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
# legacy scripts that use raw APDU strings, we will still loosely apply GET RESPONSE based on what
# the status word indicates. Unless the user explicitly enables the strict mode (set apdu_strict true)
while True:
if sw in ['9000', '9100']:
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
# indicates that either no response data was returnd or all response data has been retrieved
# successfully. We may discontinue the processing at this point.
break;
if sw[0:2] in ['61', '9f']:
# A status word of 61xx or 9fxx indicates that there is (still) response data available. We
# send a GET RESPONSE command with the length value indicated in the second byte of the status
# word. (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4a and 3GPP TS 51.011 9.4.1 and
# ISO/IEC 7816-4, Table 5)
le_gr = sw[2:4]
elif sw[0:2] in ['62', '63']:
# There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us
# to send a dummy GET RESPONSE command. We send a GET RESPONSE command with a length of 0.
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1)
le_gr = '00'
else:
# A status word other then the ones covered by the above logic may indicate an error. In this
# case we will discontinue the processing as well.
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4c)
break
tpdu_gr = tpdu[0:2] + 'c00000' + le_gr
prev_tpdu = tpdu_gr prev_tpdu = tpdu_gr
data_gr, sw = self.send_tpdu(tpdu_gr) d, sw = self.send_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 += d
data += data_gr
if sw[0:2] == '6c': if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding # SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
tpdu_gr = prev_tpdu[0:8] + sw[2:4] tpdu_gr = prev_tpdu[0:8] + sw[2:4]
data, sw = self.send_tpdu(tpdu_gr) 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
+8 -51
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):
@@ -1263,11 +1233,9 @@ class CardProfileSIM(CardProfile):
@staticmethod @staticmethod
def decode_select_response(resp_hex: str) -> object: def decode_select_response(resp_hex: str) -> object:
""" # we try to build something that resembles a dict resulting from the TLV decoder
Decode the select response to a dict representation, similar to the one of TS 102.221 (see ts_102_221.py, # of TS 102.221 (FcpTemplate), so that higher-level code only has to deal with one
class FcpTemplate), so that higher-level code only has to deal with one respresentation. See also # format of SELECT response
3GPP TS 51.011, section 9.2.1
"""
resp_bin = h2b(resp_hex) resp_bin = h2b(resp_hex)
struct_of_file_map = { struct_of_file_map = {
0: 'transparent', 0: 'transparent',
@@ -1305,24 +1273,13 @@ class CardProfileSIM(CardProfile):
record_len = resp_bin[14] record_len = resp_bin[14]
ret['file_descriptor']['record_len'] = record_len ret['file_descriptor']['record_len'] = record_len
ret['file_descriptor']['num_of_rec'] = ret['file_size'] // record_len ret['file_descriptor']['num_of_rec'] = ret['file_size'] // record_len
ret['access_conditions'] = b2h(resp_bin[8:11]) ret['access_conditions'] = b2h(resp_bin[8:10])
if resp_bin[11] & 0x01 == 0:
# Life cycle status integer, see also ETSI TS 102 221, table 11.7b
lcsi = resp_bin[11]
if lcsi == 0x00:
ret['life_cycle_status_int'] = 'no_information'
elif lcsi == 0x01:
ret['life_cycle_status_int'] = 'creation'
elif lcsi == 0x03:
ret['life_cycle_status_int'] = 'initialization'
elif lcsi & 0xFD == 0x05:
ret['life_cycle_status_int'] = 'operational_activated' ret['life_cycle_status_int'] = 'operational_activated'
elif lcsi & 0xFD == 0x04: elif resp_bin[11] & 0x04:
ret['life_cycle_status_int'] = 'operational_deactivated' ret['life_cycle_status_int'] = 'operational_deactivated'
elif lcsi & 0xFC == 0x0C:
ret['life_cycle_status_int'] = 'termination'
else: else:
ret['life_cycle_status_int'] = lcsi ret['life_cycle_status_int'] = 'terminated'
return ret return ret
@classmethod @classmethod
-4
View File
@@ -4,7 +4,3 @@ build-backend = "setuptools.build_meta"
[tool.pylint.main] [tool.pylint.main]
ignored-classes = ["twisted.internet.reactor"] ignored-classes = ["twisted.internet.reactor"]
[tool.pylint.TYPECHECK]
# SdKey subclasses are generated dynamically via SdKey.generate_sd_key_classes()
generated-members = ["SdKey[A-Za-z0-9]+"]
+1 -1
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
+1 -1
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
@@ -7,24 +7,10 @@ set apdu_strict true
# No command data field, No response data field present # No command data field, No response data field present
apdu 00700001 --expect-sw 9000 --expect-response-regex '^$' apdu 00700001 --expect-sw 9000 --expect-response-regex '^$'
# Case #1: (verify pin)
# This command returns the number of remaining authentication attempts in the
# form of a status that has the form 63cX, where X is the number of remaining
# attempts. Such a status word can be easily confused with the response to a
# case #4 APDU. This test checks if the transport layer correctly distinguishes
# the between APDU case #1 and APDU case #4.
apdu 0020000A --expect-sw 63c? --expect-response-regex '^$'
# Case #2: (status) # Case #2: (status)
# No command data field, Response data field present # No command data field, Response data field present
apdu 80F2000000 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$' apdu 80F2000000 --expect-sw 9000 --expect-response-regex '^[a-fA-F0-9]+$'
# Case #2: (verify pin)
# (see also above). This test checks if the transport layer is also able to
# distinguish correctly between APDU case #2 (with zero length response) and
# APDU case #4.
apdu 0020000A00 --expect-sw 63c? --expect-response-regex '^$'
# Case #3: (terminal capability) # Case #3: (terminal capability)
# Command data field present, No response data field # Command data field present, No response data field
apdu 80AA000005a903830180 --expect-sw 9000 --expect-response-regex '^$' apdu 80AA000005a903830180 --expect-sw 9000 --expect-response-regex '^$'
@@ -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
+1 -2
View File
@@ -20,8 +20,7 @@ class TestCardKeyProviderCsv(unittest.TestCase):
"KIK3" : "00010204040506070809488B0C0D0E0F"} "KIK3" : "00010204040506070809488B0C0D0E0F"}
csv_file_path = os.path.dirname(os.path.abspath(__file__)) + "/test_card_key_provider.csv" csv_file_path = os.path.dirname(os.path.abspath(__file__)) + "/test_card_key_provider.csv"
card_key_field_cryptor = CardKeyFieldCryptor(column_keys) card_key_provider_register(CardKeyProviderCsv(csv_file_path, column_keys))
card_key_provider_register(CardKeyProviderCsv(csv_file_path, card_key_field_cryptor))
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def test_card_key_provider_get(self): def test_card_key_provider_get(self):
+7 -189
View File
@@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# 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 enum
import io import io
import sys import sys
import unittest import unittest
import io
from importlib import resources from importlib import resources
from osmocom.utils import hexstr from osmocom.utils import hexstr
from pySim.esim.saip import ProfileElementSequence from pySim.esim.saip import ProfileElementSequence
@@ -150,7 +150,7 @@ class ConfigurableParameterTest(unittest.TestCase):
Paramtest(param_cls=p13n.AlgorithmID, Paramtest(param_cls=p13n.AlgorithmID,
val='usim-test', val='usim-test',
expect_clean_val=3, expect_clean_val=3,
expect_val='usim_test'), expect_val='usim-test'),
Paramtest(param_cls=p13n.AlgorithmID, Paramtest(param_cls=p13n.AlgorithmID,
val=1, val=1,
@@ -163,7 +163,7 @@ class ConfigurableParameterTest(unittest.TestCase):
Paramtest(param_cls=p13n.AlgorithmID, Paramtest(param_cls=p13n.AlgorithmID,
val=3, val=3,
expect_clean_val=3, expect_clean_val=3,
expect_val='usim_test'), expect_val='usim-test'),
Paramtest(param_cls=p13n.K, Paramtest(param_cls=p13n.K,
val='01020304050607080910111213141516', val='01020304050607080910111213141516',
@@ -310,11 +310,14 @@ class ConfigurableParameterTest(unittest.TestCase):
p13n.SdKeyScp80Kvn03DesDek, p13n.SdKeyScp80Kvn03DesDek,
#p13n.SdKeyScp80Kvn03DesEnc, #p13n.SdKeyScp80Kvn03DesEnc,
#p13n.SdKeyScp80Kvn03DesMac, #p13n.SdKeyScp80Kvn03DesMac,
p13n.SdKeyScp81Kvn40AesDek, #p13n.SdKeyScp81Kvn40AesDek,
p13n.SdKeyScp81Kvn40DesDek,
#p13n.SdKeyScp81Kvn40Tlspsk, #p13n.SdKeyScp81Kvn40Tlspsk,
#p13n.SdKeyScp81Kvn41AesDek, #p13n.SdKeyScp81Kvn41AesDek,
#p13n.SdKeyScp81Kvn41DesDek,
p13n.SdKeyScp81Kvn41Tlspsk, p13n.SdKeyScp81Kvn41Tlspsk,
#p13n.SdKeyScp81Kvn42AesDek, #p13n.SdKeyScp81Kvn42AesDek,
#p13n.SdKeyScp81Kvn42DesDek,
#p13n.SdKeyScp81Kvn42Tlspsk, #p13n.SdKeyScp81Kvn42Tlspsk,
): ):
@@ -441,191 +444,6 @@ class ConfigurableParameterTest(unittest.TestCase):
raise RuntimeError(f'output differs from expected output at position {at}: "{output[at:at+20]}" != "{xo_str[at:at+20]}"') raise RuntimeError(f'output differs from expected output at position {at}: "{output[at:at+20]}" != "{xo_str[at:at+20]}"')
class TestValidateVal(unittest.TestCase):
"""validate_val() tests for various ConfigurableParameter subclasses."""
def _ok(self, cls, val, expected=None):
result = cls.validate_val(val)
if expected is not None:
self.assertEqual(result, expected)
return result
def _err(self, cls, val):
with self.assertRaises(ValueError):
cls.validate_val(val)
# --- Iccid ---
def test_iccid_18digits_adds_luhn(self):
result = self._ok(p13n.Iccid, '998877665544332211')
self.assertIsInstance(result, str)
self.assertEqual(len(result), 19)
self.assertTrue(result.isdecimal())
def test_iccid_19digits_passthrough(self):
result = self._ok(p13n.Iccid, '9988776655443322110')
self.assertIsInstance(result, str)
self.assertEqual(len(result), 19)
def test_iccid_too_short(self):
self._err(p13n.Iccid, '12345678901234567') # 17 digits
def test_iccid_too_long(self):
self._err(p13n.Iccid, '1' * 21)
def test_iccid_non_digits(self):
self._err(p13n.Iccid, '99887766554433221X')
# --- Imsi ---
def test_imsi_valid_short(self):
self._ok(p13n.Imsi, '001010', '001010')
def test_imsi_valid_long(self):
self._ok(p13n.Imsi, '001010123456789', '001010123456789')
def test_imsi_too_short(self):
self._err(p13n.Imsi, '12345') # 5 digits, min is 6
def test_imsi_too_long(self):
self._err(p13n.Imsi, '1' * 16)
def test_imsi_non_digits(self):
self._err(p13n.Imsi, '00101A123456789')
# --- Pin1 ---
def test_pin1_4digits(self):
# DecimalHexParam encodes each digit as its ASCII byte, then rpad to 8 bytes with 0xff
self._ok(p13n.Pin1, '1234', b'1234\xff\xff\xff\xff')
def test_pin1_8digits(self):
self._ok(p13n.Pin1, '12345678', b'12345678')
def test_pin1_too_short(self):
self._err(p13n.Pin1, '123')
def test_pin1_too_long(self):
self._err(p13n.Pin1, '123456789')
def test_pin1_non_digits(self):
self._err(p13n.Pin1, '123A')
# --- Puk1 ---
def test_puk1_8digits(self):
self._ok(p13n.Puk1, '12345678', b'12345678')
def test_puk1_wrong_length(self):
self._err(p13n.Puk1, '1234567') # 7 digits
self._err(p13n.Puk1, '123456789') # 9 digits
def test_puk1_non_digits(self):
self._err(p13n.Puk1, '1234567X')
# --- K (BinaryParam) ---
def test_k_valid_hex_str(self):
self._ok(p13n.K, '000102030405060708090a0b0c0d0e0f',
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f')
def test_k_valid_bytes(self):
raw = bytes(range(16))
self._ok(p13n.K, raw, raw)
def test_k_wrong_length(self):
self._err(p13n.K, '00' * 15) # 15 bytes, allow_len requires 16 or 32
def test_k_non_hex(self):
self._err(p13n.K, 'gg' * 16)
def test_k_odd_hex_digits(self):
self._err(p13n.K, '0' * 31) # odd number of hex digits
class TestEnumParam(unittest.TestCase):
"""Tests for the EnumParam machinery, using AlgorithmID as the concrete subclass."""
# --- validate_val ---
def test_validate_by_name_exact(self):
self.assertEqual(p13n.AlgorithmID.validate_val('Milenage'), 1)
self.assertEqual(p13n.AlgorithmID.validate_val('TUAK'), 2)
self.assertEqual(p13n.AlgorithmID.validate_val('usim_test'), 3)
def test_validate_by_int(self):
self.assertEqual(p13n.AlgorithmID.validate_val(1), 1)
self.assertEqual(p13n.AlgorithmID.validate_val(2), 2)
self.assertEqual(p13n.AlgorithmID.validate_val(3), 3)
def test_validate_fuzzy_case(self):
self.assertEqual(p13n.AlgorithmID.validate_val('milenage'), 1)
self.assertEqual(p13n.AlgorithmID.validate_val('MILENAGE'), 1)
self.assertEqual(p13n.AlgorithmID.validate_val('tuak'), 2)
def test_validate_fuzzy_hyphen_underscore(self):
# 'usim-test' has a hyphen; enum member is 'usim_test' — must fuzzy-match
self.assertEqual(p13n.AlgorithmID.validate_val('usim-test'), 3)
def test_validate_invalid_name(self):
with self.assertRaises(ValueError):
p13n.AlgorithmID.validate_val('unknown')
def test_validate_invalid_int(self):
with self.assertRaises(ValueError):
p13n.AlgorithmID.validate_val(99)
def test_validate_returns_int(self):
result = p13n.AlgorithmID.validate_val('Milenage')
self.assertIsInstance(result, int)
self.assertNotIsInstance(result, enum.Enum)
# --- map_name_to_val ---
def test_map_name_exact(self):
self.assertEqual(p13n.AlgorithmID.map_name_to_val('Milenage'), 1)
def test_map_name_fuzzy(self):
self.assertEqual(p13n.AlgorithmID.map_name_to_val('milenage'), 1)
self.assertEqual(p13n.AlgorithmID.map_name_to_val('usim-test'), 3)
def test_map_name_strict_raises(self):
with self.assertRaises(ValueError):
p13n.AlgorithmID.map_name_to_val('unknown', strict=True)
def test_map_name_nonstrict_returns_none(self):
self.assertIsNone(p13n.AlgorithmID.map_name_to_val('unknown', strict=False))
# --- map_val_to_name ---
def test_map_val_known(self):
self.assertEqual(p13n.AlgorithmID.map_val_to_name(1), 'Milenage')
self.assertEqual(p13n.AlgorithmID.map_val_to_name(2), 'TUAK')
self.assertEqual(p13n.AlgorithmID.map_val_to_name(3), 'usim_test')
def test_map_val_unknown_nonstrict(self):
self.assertIsNone(p13n.AlgorithmID.map_val_to_name(99))
def test_map_val_unknown_strict(self):
with self.assertRaises(ValueError):
p13n.AlgorithmID.map_val_to_name(99, strict=True)
# --- name_normalize ---
def test_name_normalize(self):
self.assertEqual(p13n.AlgorithmID.name_normalize('Milenage'), 'Milenage')
self.assertEqual(p13n.AlgorithmID.name_normalize('milenage'), 'Milenage')
self.assertEqual(p13n.AlgorithmID.name_normalize('usim-test'), 'usim_test')
# --- clean_name_str ---
def test_clean_name_str(self):
self.assertEqual(p13n.AlgorithmID.clean_name_str('usim-test'), 'usimtest')
self.assertEqual(p13n.AlgorithmID.clean_name_str('usim_test'), 'usimtest')
self.assertEqual(p13n.AlgorithmID.clean_name_str('Milenage'), 'milenage')
self.assertEqual(p13n.AlgorithmID.clean_name_str('foo bar!'), 'foobar')
if __name__ == "__main__": if __name__ == "__main__":
if '-u' in sys.argv: if '-u' in sys.argv:
update_expected_output = True update_expected_output = True
@@ -1,78 +0,0 @@
#!/usr/bin/env python3
# (C) 2026 by sysmocom - s.f.m.c. GmbH
# All Rights Reserved
#
# Author: Philipp Maier <pmaier@sysmocom.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
import os
from pySim.profile import CardProfile
from pySim.ts_51_011 import CardProfileSIM
from pySim.ts_102_221 import CardProfileUICC
class TestDecodeSelectResponse_CardProfile(unittest.TestCase):
def decode_select_response(self, card_Profile: CardProfile, testcases: list[dict]):
for testcase in testcases:
resp_hex = testcase['resp_hex']
decoded = card_Profile.decode_select_response(resp_hex)
if testcase['decoded']:
self.assertEqual(decoded, testcase['decoded'])
else:
print("no testvector to compare against, assuming the following output is correct:")
print("resp_hex:", resp_hex)
print("decoded:", decoded)
def test_CardProfileSIM(self):
testcases = [
# MF
{"resp_hex" : "000000003f000100000000000981020c0400838a838a",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'file_type': 'mf'}}, 'proprietary_info': {'available_memory': 0}, 'file_id': '3f00', 'file_characteristics': '81', 'num_direct_child_df': 2, 'num_direct_child_ef': 12, 'num_chv_unblock_adm_codes': 4}},
# DF.TELECOM
{"resp_hex" : "000000007f100200000000000981000d0400838a838a",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'file_type': 'df'}}, 'proprietary_info': {'available_memory': 0}, 'file_id': '7f10', 'file_characteristics': '81', 'num_direct_child_df': 0, 'num_direct_child_ef': 13, 'num_chv_unblock_adm_codes': 4}},
# EF.MSISDN
{"resp_hex" : "000000346f40040011ffff0102011a",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'file_type': 'working_ef', 'structure': 'linear_fixed'}, 'record_len': 26, 'num_of_rec': 2}, 'proprietary_info': {}, 'file_id': '6f40', 'file_size': 52, 'access_conditions': '11ffff', 'life_cycle_status_int': 'creation'}},
# EF.ICCID
{"resp_hex" : "0000000a2fe204000cffff01020000",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'file_type': 'working_ef', 'structure': 'transparent'}}, 'proprietary_info': {}, 'file_id': '2fe2', 'file_size': 10, 'access_conditions': '0cffff', 'life_cycle_status_int': 'creation'}},
]
self.decode_select_response(CardProfileSIM, testcases)
def test_CardProfileUICC(self):
testcases = [
# MF
{"resp_hex" : "622c8202782183023f00a50c80017183040003a7388701018a01058b032f0601c60c90016083010183010a83010b",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'shareable': True, 'file_type': 'df', 'structure': 'no_info_given'}, 'record_len': None, 'num_of_rec': None}, 'file_identifier': b'?\x00', 'proprietary_information': {'uicc_characteristics': b'q', 'available_memory': 239416, 'supported_filesystem_commands': {'terminal_capability': True}}, 'life_cycle_status_integer': 'operational_activated', 'security_attrib_referenced': {'ef_arr_file_id': b'/\x06', 'ef_arr_record_nr': 1}, 'pin_status_template_do': [{'ps_do': b'`'}, {'key_reference': 1}, {'key_reference': 10}, {'key_reference': 11}]}},
# ADF.USIM
{"resp_hex" : "623d8202782183027fd0840ca0000000871002ff49ff0589a50c80017183040003a7388701018a01058b032f0601c60f90017083010183018183010a83010b",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'shareable': True, 'file_type': 'df', 'structure': 'no_info_given'}, 'record_len': None, 'num_of_rec': None}, 'file_identifier': b'\x7f\xd0', 'df_name': b'\xa0\x00\x00\x00\x87\x10\x02\xffI\xff\x05\x89', 'proprietary_information': {'uicc_characteristics': b'q', 'available_memory': 239416, 'supported_filesystem_commands': {'terminal_capability': True}}, 'life_cycle_status_integer': 'operational_activated', 'security_attrib_referenced': {'ef_arr_file_id': b'/\x06', 'ef_arr_record_nr': 1}, 'pin_status_template_do': [{'ps_do': b'p'}, {'key_reference': 1}, {'key_reference': 129}, {'key_reference': 10}, {'key_reference': 11}]}},
# ADF.ISIM
{"resp_hex" : "623d8202782183027fb0840ca0000000871004ff49ff0589a50c80017183040003a7388701018a01058b032f0601c60f90017083010183018183010a83010b",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'shareable': True, 'file_type': 'df', 'structure': 'no_info_given'}, 'record_len': None, 'num_of_rec': None}, 'file_identifier': b'\x7f\xb0', 'df_name': b'\xa0\x00\x00\x00\x87\x10\x04\xffI\xff\x05\x89', 'proprietary_information': {'uicc_characteristics': b'q', 'available_memory': 239416, 'supported_filesystem_commands': {'terminal_capability': True}}, 'life_cycle_status_integer': 'operational_activated', 'security_attrib_referenced': {'ef_arr_file_id': b'/\x06', 'ef_arr_record_nr': 1}, 'pin_status_template_do': [{'ps_do': b'p'}, {'key_reference': 1}, {'key_reference': 129}, {'key_reference': 10}, {'key_reference': 11}]}},
# EF.IMSI
{"resp_hex" : "62178202412183026f078a01058b036f060a80020009880138",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'shareable': True, 'file_type': 'working_ef', 'structure': 'transparent'}, 'record_len': None, 'num_of_rec': None}, 'file_identifier': b'o\x07', 'life_cycle_status_integer': 'operational_activated', 'security_attrib_referenced': {'ef_arr_file_id': b'o\x06', 'ef_arr_record_nr': 10}, 'file_size': 9, 'short_file_identifier': 7}},
# EF.ECC
{"resp_hex" : "621a82054221000e0283026fb78a01058b036f06088002001c880108",
"decoded" : {'file_descriptor': {'file_descriptor_byte': {'shareable': True, 'file_type': 'working_ef', 'structure': 'linear_fixed'}, 'record_len': 14, 'num_of_rec': 2}, 'file_identifier': b'o\xb7', 'life_cycle_status_integer': 'operational_activated', 'security_attrib_referenced': {'ef_arr_file_id': b'o\x06', 'ef_arr_record_nr': 8}, 'file_size': 28, 'short_file_identifier': 1}},
]
self.decode_select_response(CardProfileUICC, testcases)
if __name__ == "__main__":
unittest.main()
+37 -27
View File
@@ -68,7 +68,7 @@ class ParamSourceTest(unittest.TestCase):
def test_param_source(self): def test_param_source(self):
class Paramtest(D): class ParamSourceTest(D):
mandatory = ( mandatory = (
'param_source', 'param_source',
'n', 'n',
@@ -78,11 +78,6 @@ class ParamSourceTest(unittest.TestCase):
'expect_arg', 'expect_arg',
'csv_rows', 'csv_rows',
) )
param_source: param_source.ParamSource
n: int
expect: object
expect_arg: object
csv_rows: object
def expect_const(t, vals): def expect_const(t, vals):
return tuple(t.expect_arg) == tuple(vals) return tuple(t.expect_arg) == tuple(vals)
@@ -105,59 +100,74 @@ class ParamSourceTest(unittest.TestCase):
return True return True
param_source_tests = [ param_source_tests = [
Paramtest(param_source=param_source.ConstantSource.from_str('123'), ParamSourceTest(param_source=param_source.ConstantSource.from_str('123'),
n=3, n=3,
expect=expect_const, expect=expect_const,
expect_arg=('123', '123', '123')), expect_arg=('123', '123', '123')
Paramtest(param_source=param_source.RandomDigitSource.from_str('12345'), ),
ParamSourceTest(param_source=param_source.RandomDigitSource.from_str('12345'),
n=3, n=3,
expect=expect_random, expect=expect_random,
expect_arg={'digits': decimals, expect_arg={'digits': decimals,
'val_minlen': 5, 'val_minlen': 5,
'val_maxlen': 5}), 'val_maxlen': 5,
Paramtest(param_source=param_source.RandomDigitSource.from_str('1..999'), },
),
ParamSourceTest(param_source=param_source.RandomDigitSource.from_str('1..999'),
n=10, n=10,
expect=expect_random, expect=expect_random,
expect_arg={'digits': decimals, expect_arg={'digits': decimals,
'val_minlen': 1, 'val_minlen': 1,
'val_maxlen': 3}), 'val_maxlen': 3,
Paramtest(param_source=param_source.RandomDigitSource.from_str('001..999'), },
),
ParamSourceTest(param_source=param_source.RandomDigitSource.from_str('001..999'),
n=10, n=10,
expect=expect_random, expect=expect_random,
expect_arg={'digits': decimals, expect_arg={'digits': decimals,
'val_minlen': 3, 'val_minlen': 3,
'val_maxlen': 3}), 'val_maxlen': 3,
Paramtest(param_source=param_source.RandomHexDigitSource.from_str('12345678'), },
),
ParamSourceTest(param_source=param_source.RandomHexDigitSource.from_str('12345678'),
n=3, n=3,
expect=expect_random, expect=expect_random,
expect_arg={'digits': hexadecimals, expect_arg={'digits': hexadecimals,
'val_minlen': 8, 'val_minlen': 8,
'val_maxlen': 8}), 'val_maxlen': 8,
Paramtest(param_source=param_source.RandomHexDigitSource.from_str('0*8'), },
),
ParamSourceTest(param_source=param_source.RandomHexDigitSource.from_str('0*8'),
n=3, n=3,
expect=expect_random, expect=expect_random,
expect_arg={'digits': hexadecimals, expect_arg={'digits': hexadecimals,
'val_minlen': 8, 'val_minlen': 8,
'val_maxlen': 8}), 'val_maxlen': 8,
Paramtest(param_source=param_source.RandomHexDigitSource.from_str('00*4'), },
),
ParamSourceTest(param_source=param_source.RandomHexDigitSource.from_str('00*4'),
n=3, n=3,
expect=expect_random, expect=expect_random,
expect_arg={'digits': hexadecimals, expect_arg={'digits': hexadecimals,
'val_minlen': 8, 'val_minlen': 8,
'val_maxlen': 8}), 'val_maxlen': 8,
Paramtest(param_source=param_source.IncDigitSource.from_str('10001'), },
),
ParamSourceTest(param_source=param_source.IncDigitSource.from_str('10001'),
n=3, n=3,
expect=expect_const, expect=expect_const,
expect_arg=('10001', '10002', '10003')), expect_arg=('10001', '10002', '10003')
Paramtest(param_source=param_source.CsvSource('column_name'), ),
ParamSourceTest(param_source=param_source.CsvSource('column_name'),
n=3, n=3,
expect=expect_const, expect=expect_const,
expect_arg=('first val', 'second val', 'third val'), expect_arg=('first val', 'second val', 'third val'),
csv_rows=( csv_rows=(
{'column_name': 'first val'}, {'column_name': 'first val',},
{'column_name': 'second val'}, {'column_name': 'second val',},
{'column_name': 'third val'}, {'column_name': 'third val',},
)), )
),
] ]
outputs = [] outputs = []
+48 -320
View File
@@ -77,7 +77,7 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 'TUAK':str)
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 'usim-test':str) ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 'usim-test':str)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 1:int) ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 1:int)
clean_val= 1:int clean_val= 1:int
@@ -89,7 +89,7 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 2:int)
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 3:int) ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 3:int)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der K(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1A_NoBERTLV.der K(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -127,42 +127,6 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der Opc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SmspTpScAddr(val= '+1234567':str)
clean_val= (True, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '+1234567'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SmspTpScAddr(val= 1234567:int)
clean_val= (False, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '1234567'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der TuakNumberOfKeccak(val= '123':str)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der TuakNumberOfKeccak(val= 123:int)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der MilenageRotationConstants(val= '0a 0b 0c 01 02':str)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytes)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytearray)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der MilenageXoringConstants(val= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccc 11111111111111111111111111111111 22222222222222222222222222222222':str)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der MilenageXoringConstants(val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
@@ -587,57 +551,25 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp80Kvn03DesDek(val= 11020304050607080910
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes) ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray) ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO) ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= 11020304050607080910111213141516:int) ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '010203040506070809101112131415161718192021222324':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -769,7 +701,7 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 'TUAK':str)
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 'usim-test':str) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 'usim-test':str)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 1:int) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 1:int)
clean_val= 1:int clean_val= 1:int
@@ -781,7 +713,7 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 2:int)
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 3:int) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 3:int)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der K(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der K(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -819,42 +751,6 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der Opc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SmspTpScAddr(val= '+1234567':str)
clean_val= (True, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '+1234567'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SmspTpScAddr(val= 1234567:int)
clean_val= (False, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '1234567'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der TuakNumberOfKeccak(val= '123':str)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der TuakNumberOfKeccak(val= 123:int)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der MilenageRotationConstants(val= '0a 0b 0c 01 02':str)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytes)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytearray)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der MilenageXoringConstants(val= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccc 11111111111111111111111111111111 22222222222222222222222222222222':str)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der MilenageXoringConstants(val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
@@ -1279,57 +1175,25 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp80Kvn03DesDek(val= 110203040506070809
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= 11020304050607080910111213141516:int) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= '010203040506070809101112131415161718192021222324':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -1461,7 +1325,7 @@ ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 'TUAK':str)
ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 'usim-test':str) ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 'usim-test':str)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 1:int) ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 1:int)
clean_val= 1:int clean_val= 1:int
@@ -1473,7 +1337,7 @@ ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 2:int)
ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 3:int) ok: TS48v5_SAIP2.1B_NoBERTLV.der AlgorithmID(val= 3:int)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.1B_NoBERTLV.der K(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1B_NoBERTLV.der K(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -1511,42 +1375,6 @@ ok: TS48v5_SAIP2.1B_NoBERTLV.der Opc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SmspTpScAddr(val= '+1234567':str)
clean_val= (True, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '+1234567'}:{str}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SmspTpScAddr(val= 1234567:int)
clean_val= (False, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '1234567'}:{str}
ok: TS48v5_SAIP2.1B_NoBERTLV.der TuakNumberOfKeccak(val= '123':str)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.1B_NoBERTLV.der TuakNumberOfKeccak(val= 123:int)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.1B_NoBERTLV.der MilenageRotationConstants(val= '0a 0b 0c 01 02':str)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytes)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytearray)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der MilenageXoringConstants(val= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccc 11111111111111111111111111111111 22222222222222222222222222222222':str)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der MilenageXoringConstants(val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
@@ -1971,57 +1799,25 @@ ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp80Kvn03DesDek(val= 11020304050607080910
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes) ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray) ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO) ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= 11020304050607080910111213141516:int) ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '010203040506070809101112131415161718192021222324':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -2153,7 +1949,7 @@ ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 'TUAK':str)
ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 'usim-test':str) ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 'usim-test':str)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 1:int) ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 1:int)
clean_val= 1:int clean_val= 1:int
@@ -2165,7 +1961,7 @@ ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 2:int)
ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 3:int) ok: TS48v5_SAIP2.3_NoBERTLV.der AlgorithmID(val= 3:int)
clean_val= 3:int clean_val= 3:int
read_back_val= {'Algorithm': 'usim_test'}:{str} read_back_val= {'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.3_NoBERTLV.der K(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_NoBERTLV.der K(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -2203,42 +1999,6 @@ ok: TS48v5_SAIP2.3_NoBERTLV.der Opc(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x1
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'OPc': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SmspTpScAddr(val= '+1234567':str)
clean_val= (True, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '+1234567'}:{str}
ok: TS48v5_SAIP2.3_NoBERTLV.der SmspTpScAddr(val= 1234567:int)
clean_val= (False, '1234567'):tuple
read_back_val= {'SMSP-TP-SC-ADDR': '1234567'}:{str}
ok: TS48v5_SAIP2.3_NoBERTLV.der TuakNumberOfKeccak(val= '123':str)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.3_NoBERTLV.der TuakNumberOfKeccak(val= 123:int)
clean_val= 123:int
read_back_val= {'KECCAK-N': '123'}:{str}
ok: TS48v5_SAIP2.3_NoBERTLV.der MilenageRotationConstants(val= '0a 0b 0c 01 02':str)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytes)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der MilenageRotationConstants(val= b'\n\x0b\x0c\x01\x02':bytearray)
clean_val= b'\n\x0b\x0c\x01\x02':bytes
read_back_val= {'MilenageRotation': '0a0b0c0102'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der MilenageXoringConstants(val= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccc 11111111111111111111111111111111 22222222222222222222222222222222':str)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der MilenageXoringConstants(val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes)
clean_val= b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11""""""""""""""""':bytes
read_back_val= {'MilenageXOR': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccc1111111111111111111111111111111122222222222222222222222222222222'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp02Kvn20AesDek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP02-KVN20-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
@@ -2663,57 +2423,25 @@ ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp80Kvn03DesDek(val= 110203040506070809101
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP80-KVN03-DES-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes) ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray) ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO) ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40Dek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '01020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= 11020304050607080910111213141516:int) ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x11\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '11020304050607080910111213141516'}:{hexstr} read_back_val= {'SCP81-KVN40-DEK': '11020304050607080910111213141516'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '010203040506070809101112131415161718192021222324':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '010203040506070809101112131415161718192021222324'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= '0102030405060708091011121314151617181920212223242526272829303132':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytearray)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':BytesIO)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 !"#$%&\'()012':bytes
read_back_val= {'SCP81-KVN40-AES-DEK': '0102030405060708091011121314151617181920212223242526272829303132'}:{hexstr}
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str) ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn41Tlspsk(val= '01020304050607080910111213141516':str)
clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes clean_val= b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes