forked from public/pysim
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b2fc595ce | |||
| 973d6eb2cc | |||
| 597f1e0398 | |||
| 45d37ed959 | |||
| 757c7d048e | |||
| d0e6a1b119 | |||
| 980282cc12 | |||
| 728940efb2 | |||
| cfe2b94f67 | |||
| 861ed0a1d8 | |||
| b576e8fcff | |||
| 38f93d974b | |||
| c5e7e59928 | |||
| 98af3dd2e9 | |||
| e9ff4f3b93 | |||
| ce039d69ba | |||
| aad92f2b73 | |||
| 512aba8b1d | |||
| b5ba274583 | |||
| 4307cffc82 | |||
| bfdfcad22c | |||
| ef0a2fcb37 | |||
| 3974e96933 | |||
| a7c762eb2e | |||
| 710a27d6cf | |||
| 08f40db8a3 | |||
| 4fb393e6ea | |||
| ce5da32a75 | |||
| 9ddd235a2c | |||
| 77eb30a782 | |||
| 2530329ae2 | |||
| f9e4291a43 | |||
| 20538775b2 | |||
| ef58c94dfe | |||
| 810c51c38f | |||
| 66d3b54f92 | |||
| 7d11f91778 | |||
| 58a324126e | |||
| 3cd5c41fb4 | |||
| 593bfa0911 | |||
| 8fa7727a14 | |||
| f1609424de | |||
| 1167b65e2a | |||
| cd4b01f67e | |||
| 393de033d3 | |||
| 5f1c7d603c | |||
| d7072e9263 | |||
| ac593bb14d | |||
| a95622a022 | |||
| 03b58985a5 | |||
| cc71dbf899 | |||
| aafc8d51c3 | |||
| c50f4b4a02 | |||
| 816b31eb07 | |||
| f2567de387 | |||
| 6b5fa38f14 | |||
| 45220e00d5 | |||
| 5828c92c66 | |||
| 5e2fd148f8 | |||
| fc932a2ee9 | |||
| d5aa963caa | |||
| 19245d0d8b | |||
| a786590906 | |||
| ca8fada7b6 | |||
| c995bb1ec2 | |||
| ee06ab987f | |||
| a1d3b8f5e8 | |||
| f7b86e1920 | |||
| 2cfb0972df | |||
| 4215a3bfd3 | |||
| b42d417bbe | |||
| 74ac191ae6 | |||
| add4b991b7 | |||
| 8c81e2cdf9 | |||
| d9d62ee729 | |||
| c7e68e1281 | |||
| 969f9c0e4b | |||
| 2ef9abf23e | |||
| 473f31066c | |||
| b59363b49e | |||
| 115b517c6a | |||
| 99aef1fecf | |||
| caddd1c7a0 | |||
| 11a7a7e3b1 | |||
| 5138208ee6 | |||
| 5b2fabde62 | |||
| 24127e985a | |||
| 09ae327f8b | |||
| d32bce19f6 | |||
| 83bfdc0d3b | |||
| 14ec52a06c | |||
| 209d13e233 | |||
| 3b50e64c8b | |||
| b76cc80ea1 | |||
| 3b87ba3cba | |||
| ea1d5af383 | |||
| 0634f77308 | |||
| a5a5865c7c | |||
| 3752aeb94e | |||
| 914abe3309 | |||
| 84754b6ebb | |||
| c47005d408 | |||
| 2dfaac6e4f | |||
| a615ba5138 | |||
| 8ee10ab1a5 | |||
| f10af30aed | |||
| d8f3c78135 | |||
| 6b9b46a5a4 | |||
| b6b4501e37 | |||
| 54658fa3a9 | |||
| eb04bb1082 | |||
| 453fde5a3a | |||
| 57237b650e | |||
| 1f94791240 |
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
/docs/_*
|
/docs/_*
|
||||||
/docs/generated
|
/docs/generated
|
||||||
|
/docs/filesystem.rst
|
||||||
/.cache
|
/.cache
|
||||||
/.local
|
/.local
|
||||||
/build
|
/build
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Please install the following dependencies:
|
|||||||
- pyscard
|
- pyscard
|
||||||
- pyserial
|
- pyserial
|
||||||
- pytlv
|
- pytlv
|
||||||
- pyyaml >= 5.1
|
- pyyaml >= 5.4
|
||||||
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
|
- smpp.pdu (from `github.com/hologram-io/smpp.pdu`)
|
||||||
- termcolor
|
- termcolor
|
||||||
|
|
||||||
|
|||||||
@@ -285,10 +285,7 @@ if __name__ == '__main__':
|
|||||||
option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False)
|
option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False)
|
||||||
opts = option_parser.parse_args()
|
opts = option_parser.parse_args()
|
||||||
|
|
||||||
PySimLogger.setup(print, {logging.WARN: "\033[33m"})
|
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
|
||||||
if (opts.verbose):
|
|
||||||
PySimLogger.set_verbose(True)
|
|
||||||
PySimLogger.set_level(logging.DEBUG)
|
|
||||||
|
|
||||||
# Open CSV file
|
# Open CSV file
|
||||||
cr = open_csv(opts)
|
cr = open_csv(opts)
|
||||||
|
|||||||
+20
-14
@@ -10,6 +10,11 @@
|
|||||||
|
|
||||||
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"
|
||||||
@@ -23,8 +28,7 @@ fi
|
|||||||
|
|
||||||
case "$JOB_TYPE" in
|
case "$JOB_TYPE" in
|
||||||
"test")
|
"test")
|
||||||
virtualenv -p python3 venv --system-site-packages
|
setup_venv
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install pyshark
|
pip install pyshark
|
||||||
@@ -32,23 +36,27 @@ 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-prog integration tests (requires physical cards)
|
|
||||||
cd tests/pySim-prog_test/
|
|
||||||
./pySim-prog_test.sh
|
|
||||||
cd ../../
|
|
||||||
|
|
||||||
# Run pySim-trace test
|
# Run pySim-trace test
|
||||||
tests/pySim-trace_test/pySim-trace_test.sh
|
tests/pySim-trace_test/pySim-trace_test.sh
|
||||||
|
;;
|
||||||
|
"card-test") # tests requiring physical cards
|
||||||
|
setup_venv
|
||||||
|
|
||||||
# Run pySim-shell integration tests (requires physical cards)
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run pySim-prog integration tests
|
||||||
|
cd tests/pySim-prog_test/
|
||||||
|
./pySim-prog_test.sh
|
||||||
|
cd ../../
|
||||||
|
|
||||||
|
# Run pySim-shell integration tests
|
||||||
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")
|
||||||
virtualenv -p python3 venv --system-site-packages
|
setup_venv
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
pip install .
|
pip install .
|
||||||
pip install pyshark
|
pip install pyshark
|
||||||
@@ -61,8 +69,7 @@ case "$JOB_TYPE" in
|
|||||||
# Print pylint version
|
# Print pylint version
|
||||||
pip3 freeze | grep pylint
|
pip3 freeze | grep pylint
|
||||||
|
|
||||||
virtualenv -p python3 venv --system-site-packages
|
setup_venv
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
pip install .
|
pip install .
|
||||||
|
|
||||||
@@ -80,8 +87,7 @@ case "$JOB_TYPE" in
|
|||||||
contrib/*.py
|
contrib/*.py
|
||||||
;;
|
;;
|
||||||
"docs")
|
"docs")
|
||||||
virtualenv -p python3 venv --system-site-packages
|
setup_venv
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
|||||||
+51
-46
@@ -30,6 +30,48 @@ from pathlib import Path
|
|||||||
|
|
||||||
logger = logging.getLogger(Path(__file__).stem)
|
logger = logging.getLogger(Path(__file__).stem)
|
||||||
|
|
||||||
|
option_parser = argparse.ArgumentParser(description='Tool to send OTA SMS RFM/RAM messages via SMPP',
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
option_parser.add_argument("--host", help="Host/IP of the SMPP server", default="localhost")
|
||||||
|
option_parser.add_argument("--port", help="TCP port of the SMPP server", default=2775, type=int)
|
||||||
|
option_parser.add_argument("--system-id", help="System ID to use to bind to the SMPP server", default="test")
|
||||||
|
option_parser.add_argument("--password", help="Password to use to bind to the SMPP server", default="test")
|
||||||
|
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
|
||||||
|
algo_crypt_choices = []
|
||||||
|
algo_crypt_classes = OtaAlgoCrypt.__subclasses__()
|
||||||
|
for cls in algo_crypt_classes:
|
||||||
|
algo_crypt_choices.append(cls.enum_name)
|
||||||
|
option_parser.add_argument("--algo-crypt", choices=algo_crypt_choices, default='triple_des_cbc2',
|
||||||
|
help="OTA crypt algorithm")
|
||||||
|
algo_auth_choices = []
|
||||||
|
algo_auth_classes = OtaAlgoAuth.__subclasses__()
|
||||||
|
for cls in algo_auth_classes:
|
||||||
|
algo_auth_choices.append(cls.enum_name)
|
||||||
|
option_parser.add_argument("--algo-auth", choices=algo_auth_choices, default='triple_des_cbc2',
|
||||||
|
help="OTA auth algorithm")
|
||||||
|
option_parser.add_argument('--kic', required=True, type=is_hexstr, help='OTA key (KIC)')
|
||||||
|
option_parser.add_argument('--kic-idx', default=1, type=int, help='OTA key index (KIC)')
|
||||||
|
option_parser.add_argument('--kid', required=True, type=is_hexstr, help='OTA key (KID)')
|
||||||
|
option_parser.add_argument('--kid-idx', default=1, type=int, help='OTA key index (KID)')
|
||||||
|
option_parser.add_argument('--cntr', default=0, type=int, help='replay protection counter')
|
||||||
|
option_parser.add_argument('--tar', required=True, type=is_hexstr, help='Toolkit Application Reference')
|
||||||
|
option_parser.add_argument("--cntr-req", choices=CNTR_REQ.decmapping.values(), default='no_counter',
|
||||||
|
help="Counter requirement")
|
||||||
|
option_parser.add_argument('--no-ciphering', action='store_true', default=False, help='Disable ciphering')
|
||||||
|
option_parser.add_argument("--rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
|
||||||
|
help="message check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
|
||||||
|
option_parser.add_argument('--por-in-submit', action='store_true', default=False,
|
||||||
|
help='require PoR to be sent via SMS-SUBMIT')
|
||||||
|
option_parser.add_argument('--por-no-ciphering', action='store_true', default=False, help='Disable ciphering (PoR)')
|
||||||
|
option_parser.add_argument("--por-rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
|
||||||
|
help="PoR check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
|
||||||
|
option_parser.add_argument("--por-req", choices=POR_REQ.decmapping.values(), default='por_required',
|
||||||
|
help="Proof of Receipt requirements")
|
||||||
|
option_parser.add_argument('--src-addr', default='12', type=str, help='SMS source address (MSISDN)')
|
||||||
|
option_parser.add_argument('--dest-addr', default='23', type=str, help='SMS destination address (MSISDN)')
|
||||||
|
option_parser.add_argument('--timeout', default=10, type=int, help='Maximum response waiting time')
|
||||||
|
option_parser.add_argument('-a', '--apdu', action='append', required=True, type=is_hexstr, help='C-APDU to send')
|
||||||
|
|
||||||
class SmppHandler:
|
class SmppHandler:
|
||||||
client = None
|
client = None
|
||||||
|
|
||||||
@@ -141,7 +183,7 @@ class SmppHandler:
|
|||||||
tuple containing the last response data and the last status word as byte strings
|
tuple containing the last response data and the last status word as byte strings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info("C-APDU sending: %s..." % b2h(apdu))
|
logger.info("C-APDU sending: %s...", b2h(apdu))
|
||||||
|
|
||||||
# translate to Secured OTA RFM
|
# translate to Secured OTA RFM
|
||||||
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu)
|
secured = self.ota_dialect.encode_cmd(self.ota_keyset, self.tar, self.spi, apdu=apdu)
|
||||||
@@ -167,65 +209,28 @@ class SmppHandler:
|
|||||||
return h2b(resp), h2b(sw)
|
return h2b(resp), h2b(sw)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
option_parser = argparse.ArgumentParser(description='CSV importer for pySim-shell\'s PostgreSQL Card Key Provider',
|
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
||||||
option_parser.add_argument("--host", help="Host/IP of the SMPP server", default="localhost")
|
|
||||||
option_parser.add_argument("--port", help="TCP port of the SMPP server", default=2775, type=int)
|
|
||||||
option_parser.add_argument("--system-id", help="System ID to use to bind to the SMPP server", default="test")
|
|
||||||
option_parser.add_argument("--password", help="Password to use to bind to the SMPP server", default="test")
|
|
||||||
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
|
|
||||||
algo_crypt_choices = []
|
|
||||||
algo_crypt_classes = OtaAlgoCrypt.__subclasses__()
|
|
||||||
for cls in algo_crypt_classes:
|
|
||||||
algo_crypt_choices.append(cls.enum_name)
|
|
||||||
option_parser.add_argument("--algo-crypt", choices=algo_crypt_choices, default='triple_des_cbc2',
|
|
||||||
help="OTA crypt algorithm")
|
|
||||||
algo_auth_choices = []
|
|
||||||
algo_auth_classes = OtaAlgoAuth.__subclasses__()
|
|
||||||
for cls in algo_auth_classes:
|
|
||||||
algo_auth_choices.append(cls.enum_name)
|
|
||||||
option_parser.add_argument("--algo-auth", choices=algo_auth_choices, default='triple_des_cbc2',
|
|
||||||
help="OTA auth algorithm")
|
|
||||||
option_parser.add_argument('--kic', required=True, type=is_hexstr, help='OTA key (KIC)')
|
|
||||||
option_parser.add_argument('--kic_idx', default=1, type=int, help='OTA key index (KIC)')
|
|
||||||
option_parser.add_argument('--kid', required=True, type=is_hexstr, help='OTA key (KID)')
|
|
||||||
option_parser.add_argument('--kid_idx', default=1, type=int, help='OTA key index (KID)')
|
|
||||||
option_parser.add_argument('--cntr', default=0, type=int, help='replay protection counter')
|
|
||||||
option_parser.add_argument('--tar', required=True, type=is_hexstr, help='Toolkit Application Reference')
|
|
||||||
option_parser.add_argument("--cntr_req", choices=CNTR_REQ.decmapping.values(), default='no_counter',
|
|
||||||
help="Counter requirement")
|
|
||||||
option_parser.add_argument('--ciphering', default=True, type=bool, help='Enable ciphering')
|
|
||||||
option_parser.add_argument("--rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
|
|
||||||
help="message check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
|
|
||||||
option_parser.add_argument('--por-in-submit', default=False, type=bool,
|
|
||||||
help='require PoR to be sent via SMS-SUBMIT')
|
|
||||||
option_parser.add_argument('--por-shall-be-ciphered', default=True, type=bool, help='require encrypted PoR')
|
|
||||||
option_parser.add_argument("--por-rc-cc-ds", choices=RC_CC_DS.decmapping.values(), default='cc',
|
|
||||||
help="PoR check (rc=redundency check, cc=crypt. checksum, ds=digital signature)")
|
|
||||||
option_parser.add_argument("--por_req", choices=POR_REQ.decmapping.values(), default='por_required',
|
|
||||||
help="Proof of Receipt requirements")
|
|
||||||
option_parser.add_argument('--src-addr', default='12', type=str, help='TODO')
|
|
||||||
option_parser.add_argument('--dest-addr', default='23', type=str, help='TODO')
|
|
||||||
option_parser.add_argument('--timeout', default=10, type=int, help='TODO')
|
|
||||||
option_parser.add_argument('-a', '--apdu', action='append', required=True, type=is_hexstr, help='C-APDU to send')
|
|
||||||
opts = option_parser.parse_args()
|
opts = option_parser.parse_args()
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG if opts.verbose else logging.INFO,
|
logging.basicConfig(level=logging.DEBUG if opts.verbose else logging.INFO,
|
||||||
format='%(asctime)s %(levelname)s %(message)s',
|
format='%(asctime)s %(levelname)s %(message)s',
|
||||||
datefmt='%Y-%m-%d %H:%M:%S')
|
datefmt='%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
if opts.kic_idx != opts.kid_idx:
|
||||||
|
logger.warning("KIC index (%s) and KID index (%s) are different (security violation, card should reject message)",
|
||||||
|
opts.kic_idx, opts.kid_idx)
|
||||||
|
|
||||||
ota_keyset = OtaKeyset(algo_crypt=opts.algo_crypt,
|
ota_keyset = OtaKeyset(algo_crypt=opts.algo_crypt,
|
||||||
kic_idx=opts.kic_idx,
|
kic_idx=opts.kic_idx,
|
||||||
kic=h2b(opts.kic),
|
kic=h2b(opts.kic),
|
||||||
algo_auth=opts.algo_auth,
|
algo_auth=opts.algo_auth,
|
||||||
kid_idx=opts.kic_idx,
|
kid_idx=opts.kid_idx,
|
||||||
kid=h2b(opts.kid),
|
kid=h2b(opts.kid),
|
||||||
cntr=opts.cntr)
|
cntr=opts.cntr)
|
||||||
spi = {'counter' : opts.cntr_req,
|
spi = {'counter' : opts.cntr_req,
|
||||||
'ciphering' : opts.ciphering,
|
'ciphering' : not opts.no_ciphering,
|
||||||
'rc_cc_ds': opts.rc_cc_ds,
|
'rc_cc_ds': opts.rc_cc_ds,
|
||||||
'por_in_submit':opts.por_in_submit,
|
'por_in_submit': opts.por_in_submit,
|
||||||
'por_shall_be_ciphered':opts.por_shall_be_ciphered,
|
'por_shall_be_ciphered': not opts.por_no_ciphering,
|
||||||
'por_rc_cc_ds': opts.por_rc_cc_ds,
|
'por_rc_cc_ds': opts.por_rc_cc_ds,
|
||||||
'por': opts.por_req}
|
'por': opts.por_req}
|
||||||
apdu = h2b("".join(opts.apdu))
|
apdu = h2b("".join(opts.apdu))
|
||||||
|
|||||||
@@ -305,16 +305,16 @@ the requested data.
|
|||||||
|
|
||||||
|
|
||||||
ADM PIN
|
ADM PIN
|
||||||
~~~~~~~
|
^^^^^^^
|
||||||
|
|
||||||
The `verify_adm` command will attempt to look up the `ADM1` column
|
The `verify_adm` command will attempt to look up the `ADM1` column
|
||||||
indexed by the ICCID of the SIM/UICC.
|
indexed by the ICCID of the SIM/UICC.
|
||||||
|
|
||||||
|
|
||||||
SCP02 / SCP03
|
SCP02 / SCP03
|
||||||
~~~~~~~~~~~~~
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
SCP02 and SCP03 each use key triplets consisting if ENC, MAC and DEK
|
SCP02 and SCP03 each use key triplets consisting of ENC, MAC and DEK
|
||||||
keys. For more details, see the applicable GlobalPlatform
|
keys. For more details, see the applicable GlobalPlatform
|
||||||
specifications.
|
specifications.
|
||||||
|
|
||||||
|
|||||||
+25
-1
@@ -13,6 +13,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
sys.path.insert(0, os.path.abspath('.')) # for local extensions (pysim_fs_sphinx, ...)
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
@@ -39,7 +40,8 @@ extensions = [
|
|||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinxarg.ext",
|
"sphinxarg.ext",
|
||||||
"sphinx.ext.autosectionlabel",
|
"sphinx.ext.autosectionlabel",
|
||||||
"sphinx.ext.napoleon"
|
"sphinx.ext.napoleon",
|
||||||
|
"pysim_fs_sphinx",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
@@ -64,3 +66,25 @@ html_theme = 'alabaster'
|
|||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
|
||||||
autoclass_content = 'both'
|
autoclass_content = 'both'
|
||||||
|
|
||||||
|
# Mock optional server-side deps of es2p and http_json_api/es9p,
|
||||||
|
# so that autodoc can import and document those modules.
|
||||||
|
autodoc_mock_imports = ['klein', 'twisted']
|
||||||
|
|
||||||
|
# Workaround for duplicate label warnings:
|
||||||
|
# https://github.com/sphinx-doc/sphinx-argparse/issues/14
|
||||||
|
#
|
||||||
|
# sphinxarg.ext generates generic sub-headings ("Named arguments",
|
||||||
|
# "Positional arguments", "Sub-commands", "General options", ...) for every
|
||||||
|
# argparse command/tool. These repeat across many files and trigger tons
|
||||||
|
# of autosectionlabel duplicate-label warnings - suppress them.
|
||||||
|
autosectionlabel_maxdepth = 3
|
||||||
|
suppress_warnings = [
|
||||||
|
'autosectionlabel.filesystem',
|
||||||
|
'autosectionlabel.saip-tool',
|
||||||
|
'autosectionlabel.shell',
|
||||||
|
'autosectionlabel.smpp2sim',
|
||||||
|
'autosectionlabel.smpp-ota-tool',
|
||||||
|
'autosectionlabel.suci-keytool',
|
||||||
|
'autosectionlabel.trace',
|
||||||
|
]
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pySim consists of several parts:
|
|||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
shell
|
shell
|
||||||
|
filesystem
|
||||||
trace
|
trace
|
||||||
legacy
|
legacy
|
||||||
smpp2sim
|
smpp2sim
|
||||||
@@ -48,6 +49,7 @@ pySim consists of several parts:
|
|||||||
sim-rest
|
sim-rest
|
||||||
suci-keytool
|
suci-keytool
|
||||||
saip-tool
|
saip-tool
|
||||||
|
smpp-ota-tool
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|||||||
+1
-1
@@ -205,7 +205,7 @@ Specifically, pySim-read will dump the following:
|
|||||||
|
|
||||||
* DF.GSM
|
* DF.GSM
|
||||||
|
|
||||||
* EF,IMSI
|
* EF.IMSI
|
||||||
* EF.GID1
|
* EF.GID1
|
||||||
* EF.GID2
|
* EF.GID2
|
||||||
* EF.SMSP
|
* EF.SMSP
|
||||||
|
|||||||
@@ -0,0 +1,836 @@
|
|||||||
|
Guide: Managing GP Keys
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Most of today's smartcards follow the GlobalPlatform Card Specification and the included Security Domain model.
|
||||||
|
UICCs and eUCCCs are no exception here.
|
||||||
|
|
||||||
|
The Security Domain acts as an on-card representative of a card authority or administrator. It is used to perform tasks
|
||||||
|
like the installation of applications or the provisioning and rotation of secure channel keys. It also acts as a secure
|
||||||
|
key storage and offers all kinds of cryptographic services to applications that are installed under a specific
|
||||||
|
Security Domain (see also GlobalPlatform Card Specification, section 7).
|
||||||
|
|
||||||
|
In this tutorial, we will show how to work with the key material (keysets) stored inside a Security Domain and how to
|
||||||
|
rotate (replace) existing keys. We will also show how to provision new keys.
|
||||||
|
|
||||||
|
.. warning:: Making changes to keysets requires extreme caution as misconfigured keysets may lock you out permanently.
|
||||||
|
It's also strongly recommended to maintain at least one backup keyset that you can use as fallback in case
|
||||||
|
the primary keyset becomes unusable for some reason.
|
||||||
|
|
||||||
|
|
||||||
|
Selecting a Security Domain
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A typical smartcard, such as an UICC will have one primary Security Domain, called the Issuer Security Domain (ISD).
|
||||||
|
When working with those cards, the ISD will show up in the UICC filesystem tree as `ADF.ISD` and can be selected like
|
||||||
|
any other file.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (00:MF)> select ADF.ISD
|
||||||
|
{
|
||||||
|
"application_id": "a000000003000000",
|
||||||
|
"proprietary_data": {
|
||||||
|
"maximum_length_of_data_field_in_command_message": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
When working with eUICCs, multiple Security Domains are involved. The model is fundamentally different from the classic
|
||||||
|
model with one primary Security Domain (ISD). In the case of eUICCs, an ISD-R (Issuer Security Domain - Root) and an
|
||||||
|
ISD-P (Issuer Security Domain - Profile) exist (see also: GSMA SGP.02, section 2.2.1).
|
||||||
|
|
||||||
|
The ISD-P is established by the ISD-R during the profile installation and serves as a secure container for an eSIM
|
||||||
|
profile. Within the ISD-P the eSIM profile establishes a dedicated Security Domain called `MNO-SD` (see also GSMA
|
||||||
|
SGP.02, section 2.2.4). This `MNO-SD` is comparable to the Issuer Security Domain (ISD) we find on UICCs. The AID of
|
||||||
|
`MNO-SD` is either the default AID for the Issuer Security Domain (see also GlobalPlatform, section H.1.3) or a
|
||||||
|
different value specified by the provider of the eSIM profile.
|
||||||
|
|
||||||
|
Since the AID of the `MNO-SD` is not a fixed value, it is not known by `pySim-shell`. This means there will be no
|
||||||
|
`ADF.ISD` file shown in the file system, but we can simply select the `ADF.ISD-R` first and then select the `MNO-SD`
|
||||||
|
using a raw APDU. In the following example we assume that the default AID (``a000000151000000``) is used The APDU
|
||||||
|
would look like this: ``00a4040408`` + ``a000000151000000`` + ``00``
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (00:MF)> select ADF.ISD-R
|
||||||
|
{
|
||||||
|
"application_id": "a0000005591010ffffffff8900000100",
|
||||||
|
"proprietary_data": {
|
||||||
|
"maximum_length_of_data_field_in_command_message": 255
|
||||||
|
},
|
||||||
|
"isdr_proprietary_application_template": {
|
||||||
|
"supported_version_number": "020300"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pySIM-shell (00:MF/ADF.ISD-R)> apdu 00a4040408a00000015100000000
|
||||||
|
SW: 9000, RESP: 6f108408a000000151000000a5049f6501ff
|
||||||
|
|
||||||
|
After that, the prompt will still show the `ADF.ISD-R`, but we are actually in `ADF.ISD` and the standard GlobalPlatform
|
||||||
|
operations like `establish_scpXX`, `get_data`, and `put_key` should work. By doing this, we simply have tricked
|
||||||
|
`pySim-shell` into making the GlobalPlatform related commands available for some other Security Domain we are not
|
||||||
|
interested in. With the raw APDU we then have swapped out the Security Domain under the hood. The same workaround can
|
||||||
|
be applied to any Security Domain, provided that the AID is known to the user.
|
||||||
|
|
||||||
|
|
||||||
|
Establishing a secure channel
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Before we can make changes to the keysets in the currently selected Security Domain we must first establish a secure
|
||||||
|
channel with that Security Domain. In the following examples we will use `SCP02` (see also GlobalPlatform Card
|
||||||
|
Specification, section E.1.1) and `SCP03` (see also GlobalPlatform Card Specification – Amendment D) to establish the
|
||||||
|
secure channel. `SCP02` is slightly older than `SCP03`. The main difference between the two is that `SCP02` uses 3DES
|
||||||
|
while `SCP03` is based on AES.
|
||||||
|
|
||||||
|
.. warning:: Secure channel protocols like `SCP02` and `SCP03` may manage an error counter to count failed login
|
||||||
|
attempts. This means attempting to establish a secure channel with a wrong keyset multiple times may lock
|
||||||
|
you out permanently. Double check the applied keyset before attempting to establish a secure channel.
|
||||||
|
|
||||||
|
.. warning:: The key values used in the following examples are random key values used for illustration purposes only.
|
||||||
|
Each UICC or eSIM profile is shipped with individual keys, which means that the keys used below will not
|
||||||
|
work with your UICC or eSIM profile. You must replace the key values with the values you have received
|
||||||
|
from your UICC vendor or eSIM profile provider.
|
||||||
|
|
||||||
|
|
||||||
|
Example: `SCP02`
|
||||||
|
----------------
|
||||||
|
|
||||||
|
In the following example, we assume that we want to establish a secure channel with the ISD of a `sysmoUSIM-SJA5` UICC.
|
||||||
|
Along with the card we have received the following keyset:
|
||||||
|
|
||||||
|
+---------+----------------------------------+
|
||||||
|
| Keyname | Keyvalue |
|
||||||
|
+=========+==================================+
|
||||||
|
| ENC/KIC | F09C43EE1A0391665CC9F05AF4E0BD10 |
|
||||||
|
+---------+----------------------------------+
|
||||||
|
| MAC/KID | 01981F4A20999F62AF99988007BAF6CA |
|
||||||
|
+---------+----------------------------------+
|
||||||
|
| DEK/KIK | 8F8AEE5CDCC5D361368BC45673D99195 |
|
||||||
|
+---------+----------------------------------+
|
||||||
|
|
||||||
|
This keyset is tied to the key version number KVN 122 and is configured as a DES keyset. We can use this keyset to
|
||||||
|
establish a secure channel using the SCP02 Secure Channel Protocol.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (00:MF/ADF.ISD)> establish_scp02 --key-enc F09C43EE1A0391665CC9F05AF4E0BD10 --key-mac 01981F4A20999F62AF99988007BAF6CA --key-dek 8F8AEE5CDCC5D361368BC45673D99195 --key-ver 112 --security-level 3
|
||||||
|
Successfully established a SCP02[03] secure channel
|
||||||
|
|
||||||
|
|
||||||
|
Example: `SCP03`
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The establishment of a secure channel via SCP03 works just the same. In the following example we will establish a
|
||||||
|
secure channel to the `MNO-SD` of an eSIM profile. The SCP03 keyset we use is tied to KVN 48 and looks like this:
|
||||||
|
|
||||||
|
+---------+------------------------------------------------------------------+
|
||||||
|
| Keyname | Keyvalue |
|
||||||
|
+=========+==================================================================+
|
||||||
|
| ENC/KIC | 63af517c29ad6ac6fcadfe6ac8a3c8a041d8141c7eb845ef1cba6112a325e430 |
|
||||||
|
+---------+------------------------------------------------------------------+
|
||||||
|
| MAC/KID | 54b9ad6713ae922f54014ed762132e7b59bdcd2a2a6beba98fb9afe6b4df27e1 |
|
||||||
|
+---------+------------------------------------------------------------------+
|
||||||
|
| DEK/KIK | cbb933ba2389da93c86c112739cd96389139f16c6f80f7d16bf3593e407ca893 |
|
||||||
|
+---------+------------------------------------------------------------------+
|
||||||
|
|
||||||
|
We assume that the `MNO-SD` is already selected (see above). We may now establish the SCP03 secure channel:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (00:MF/ADF.ISD-R)> establish_scp03 --key-enc 63af517c29ad6ac6fcadfe6ac8a3c8a041d8141c7eb845ef1cba6112a325e430 --key-mac 54b9ad6713ae922f54014ed762132e7b59bdcd2a2a6beba98fb9afe6b4df27e1 --key-dek cbb933ba2389da93c86c112739cd96389139f16c6f80f7d16bf3593e407ca893 --key-ver 48 --security-level 3
|
||||||
|
Successfully established a SCP03[03] secure channel
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Understanding Keysets
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Before making any changes to keysets, it is recommended to check the status of the currently installed keysets. To do
|
||||||
|
so, we use the `get_data` command to retrieve the `key_information`. This command does not require the establishment of
|
||||||
|
a secure channel. We also cannot read back the key values themselves, but we get a summary of the installed keys
|
||||||
|
together with their KVN numbers, IDs, algorithm and key length values.
|
||||||
|
|
||||||
|
Example: `key_information` from a `sysmoISIM-SJA5`:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> get_data key_information
|
||||||
|
{
|
||||||
|
"key_information": [
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 112,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 112,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 112,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 1,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 1,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 1,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 2,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 2,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 2,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 47,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 47,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 47,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Example: `key_information` from a `sysmoEUICC1-C2T`:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP03[03]:00:MF/ADF.ISD-R)> get_data key_information
|
||||||
|
{
|
||||||
|
"key_information": [
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 50,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 50,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 50,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 64,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 64,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "tls_psk",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
The output from those two examples above may seem lengthy, but in order to move on and to provision own keys
|
||||||
|
successfully, it is important to understand each aspect of it.
|
||||||
|
|
||||||
|
Key Version Number (KVN)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Each key is associated with a Key Version Number (KVN). Multiple keys that share the same KVN belong to the same
|
||||||
|
keyset. In the first example above we can see that four keysets with KVN numbers 112, 1, 2 and 47 are provisioned.
|
||||||
|
In the second example we see two keysets. One with KVN 50 and one with KVN 64.
|
||||||
|
|
||||||
|
The term "Key Version Number" is misleading as this number is not really a version number. It's actually a unique
|
||||||
|
identifier for a specific keyset that also defines with which Secure Channel Protocol a key can be used. This means
|
||||||
|
that the KVN is not just an arbitrary number. The following (incomplete) table gives a hint which KVN numbers may be
|
||||||
|
used with which Secure Channel Protocol.
|
||||||
|
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| KVN range | Secure Channel Protocol |
|
||||||
|
+===========+=======================================================+
|
||||||
|
| 1-15 | reserved for `SCP80` (OTA SMS) |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 17 | reserved for DAP specified in ETSI TS 102 226 |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 32-47 | reserved for `SCP02` |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 48-63 | reserved for `SCP03` |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 64-79 | reserved for `SCP81` (GSMA SGP.02, section 2.2.5.1) |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 112 | Token key (RSA public or DES, also used with `SCP02`) |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 113 | Receipt key (DES) |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 115 | DAP verification key (RS public or DES) |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 116 | reserved for CASD |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 117 | 16-byte DES key for Ciphered Load File Data Block |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
| 255 | reserved for ISD with SCP02 without SCP80 support |
|
||||||
|
+-----------+-------------------------------------------------------+
|
||||||
|
|
||||||
|
With that we can now understand that in the first example, the first and the last keyset is intended to be used with
|
||||||
|
`SCP02` and that the second and the third keyset is intended to be used with `SCP80` (OTA SMS). In the second example we
|
||||||
|
can see that the first keyset is intended to be used with `SCP03`, wheres the second should be usable with `SCP81`.
|
||||||
|
|
||||||
|
|
||||||
|
Key Identifier
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Each keyset consists of a number of keys, where each key has a different Key Identifier. The Key Identifier is usually
|
||||||
|
an incrementing number that starts counting at 1. The Key Identifier is used to distinguish the keys within the keyset.
|
||||||
|
The exact number of keys and their attributes depends on the secure channel protocol for which the keyset is intended
|
||||||
|
for. Each secure channel protocol may have its specific requirements on how many keys of which which type, length or
|
||||||
|
Key Identifier have to be present.
|
||||||
|
|
||||||
|
However, almost all of the classic secure channel protocols (including `SCP02`, `SCP03` and `SCP81`) make use of the
|
||||||
|
following three-key scheme:
|
||||||
|
|
||||||
|
+----------------+---------+---------------------------------------+
|
||||||
|
| Key Identifier | Keyname | Purpose |
|
||||||
|
+================+=========+=======================================+
|
||||||
|
| 1 | ENC/KIC | encryption/decryption |
|
||||||
|
+----------------+---------+---------------------------------------+
|
||||||
|
| 2 | MAC/KID | cryptographic checksumming/signing |
|
||||||
|
+----------------+---------+---------------------------------------+
|
||||||
|
| 3 | DEK/KIK | encryption/decryption of key material |
|
||||||
|
+----------------+---------+---------------------------------------+
|
||||||
|
|
||||||
|
In this case, all three keys share the same length and are used with the same algorithm. The key length is often used
|
||||||
|
to implicitly select sub-types of an algorithm. (e.g. a 16 byte key of type `aes` is associated with `AES128`, where a 32
|
||||||
|
byte key would be associated with `AES256`).
|
||||||
|
|
||||||
|
The second example shows that different schemes are possible. The `SCP80` keyset from the second example uses a scheme
|
||||||
|
that works with two keys:
|
||||||
|
|
||||||
|
+----------------+---------+---------------------------------------+
|
||||||
|
| Key Identifier | Keyname | Purpose |
|
||||||
|
+================+=========+=======================================+
|
||||||
|
| 1 | TLS-PSK | pre-shared key used for TLS |
|
||||||
|
+----------------+---------+---------------------------------------+
|
||||||
|
| 2 | DEK/KIK | encryption/decryption of key material |
|
||||||
|
+----------------+---------+---------------------------------------+
|
||||||
|
|
||||||
|
It should also be noted that the order in which keysets and keys appear is an implementation detail of the UICC/eUICC
|
||||||
|
O/S. The order has no influence on how a keyset is interpreted. Only the Key Version Number (KVN) and the Key Identifier
|
||||||
|
matter.
|
||||||
|
|
||||||
|
|
||||||
|
Rotating a keyset
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Rotating keys is one of the most basic tasks one might want to perform on an UICC/eUICC before using it productively. In
|
||||||
|
the following example we will illustrate how key rotation can be done. When rotating keys, only the key itself may
|
||||||
|
change. For example it is not possible to change the key length or the algorithm used (see also GlobalPlatform Card
|
||||||
|
Specification, section 11.8.2.3.3). Any key of the current Security Domain can be rotated, this also includes the key
|
||||||
|
that was used to establish the secure channel.
|
||||||
|
|
||||||
|
In the following example we assume that the Security Domain is selected and a secure channel is already established. We
|
||||||
|
intend to rotate the keyset with KVN 112. Since this keyset uses triple DES keys with a key length of 16, we must
|
||||||
|
replace it with a keyset with keys of the same nature.
|
||||||
|
|
||||||
|
The new keyset shall look like this:
|
||||||
|
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| Key Identifier | Keyname | Keyvalue |
|
||||||
|
+================+=========+==================================+
|
||||||
|
| 1 | ENC/KIC | 542C37A6043679F2F9F71116418B1CD5 |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| 2 | MAC/KID | 34F11BAC8E5390B57F4E601372339E3C |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| 3 | DEK/KIK | 5524F4BECFE96FB63FC29D6BAAC6058B |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
|
||||||
|
When passing the keys to the `put_key` commandline, we set the Key Identifier of the first key using the `--key-id`
|
||||||
|
parameter. This Key Identifier will be valid for the first key (KIC) we pass. For all consecutive keys, the Key
|
||||||
|
Identifier will be incremented automatically (see also GlobalPlatform Card Specification, section 11.8.2.2). To Ensure
|
||||||
|
that the new KIC, KID and KIK keys get the correct Key Identifiers, it is crucial to maintain order when passing the
|
||||||
|
keys in the `--key-data` arguments. It is also important that each `--key-data` argument is preceded by a `--key-type`
|
||||||
|
argument that sets the algorithm correctly (`des` in this case).
|
||||||
|
|
||||||
|
Finally we have to target the keyset we want to rotate by its KVN. The `--old-key-version-nr` argument is set to 112
|
||||||
|
as this identifies the keyset we want to rotate. The `--key-version-nr` is also set to 112 as we do not want
|
||||||
|
KVN to be changed in this example. Changing the KVN while rotating a keyset is possible. In case the KVN has to change
|
||||||
|
for some reason, the new KVN must be selected carefully to keep the key usable with the associated Secure Channel
|
||||||
|
Protocol.
|
||||||
|
|
||||||
|
The commandline that matches the keyset we had laid out above looks like this:
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> put_key --key-id 1 --key-type des --key-data 542C37A6043679F2F9F71116418B1CD5 --key-type des --key-data 34F11BAC8E5390B57F4E601372339E3C --key-type des --key-data 5524F4BECFE96FB63FC29D6BAAC6058B --old-key-version-nr 112 --key-version-nr 112
|
||||||
|
|
||||||
|
After executing this put_key commandline, the keyset identified by KVN 122 is equipped with new keys. We can use
|
||||||
|
`get_data key_information` to inspect the currently installed keysets. The output should appear unchanged as
|
||||||
|
we only swapped out the keys. All other parameters, identifiers etc. should remain constant.
|
||||||
|
|
||||||
|
.. warning:: It is technically possible to rotate a keyset in a `non atomic` way using one `put_key` commandline for
|
||||||
|
each key. However, in case the targeted keyset is the one used to establish the current secure channel,
|
||||||
|
this method should not be used since, depending on the UICC/eUICC model, half-written key material may
|
||||||
|
interrupt the current secure channel.
|
||||||
|
|
||||||
|
|
||||||
|
Removing a keyset
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In some cases it is necessary to remove a keyset entirely. This can be done with the `delete_key` command. Here it is
|
||||||
|
important to understand that `delete_key` only removes one specific key from a specific keyset. This means that you
|
||||||
|
need to run a separate `delete_key` command for each key inside a keyset.
|
||||||
|
|
||||||
|
In the following example we assume that the Security Domain is selected and a secure channel is already established. We
|
||||||
|
intend to remove the keyset with KVN 112. This keyset consists of three keys.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> delete_key --key-ver 112 --key-id 1
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> delete_key --key-ver 112 --key-id 2
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> delete_key --key-ver 112 --key-id 3
|
||||||
|
|
||||||
|
To verify that the keyset has been deleted properly, we can use the `get_data key_information` command to inspect the
|
||||||
|
current status of the installed keysets. We should see that the key with KVN 112 is no longer present.
|
||||||
|
|
||||||
|
|
||||||
|
Adding a keyset
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In the following we will discuss how to add an entirely new keyset. The procedure is almost identical with the key
|
||||||
|
rotation procedure we have already discussed and it is assumed that all details about the key rotation are understood.
|
||||||
|
In this section we will go into more detail and illustrate how to provision new 3DES, `AES128` and `AES256` keysets.
|
||||||
|
|
||||||
|
It is important to keep in mind that storage space on smartcard is a precious resource. In many cases the amount of
|
||||||
|
keysets that a Security Domain can store is limited. In some situations you may be forced to sacrifice one of your
|
||||||
|
existing keysets in favor of a new keyset.
|
||||||
|
|
||||||
|
The main difference between key rotation and the adding of new keys is that we do not simply replace an existing key.
|
||||||
|
Instead an entirely new key is programmed into the Security Domain. Therefore the `put_key` commandline will have no
|
||||||
|
`--old-key-version-nr` parameter. From the commandline perspective, this is already the only visible difference from a
|
||||||
|
commandline that simply rotates a keyset. Since we are writing an entirely new keyset, we are free to chose the
|
||||||
|
algorithm and the key length within the parameter range permitted by the targeted secure channel protocol. Otherwise
|
||||||
|
the same rules apply.
|
||||||
|
|
||||||
|
For reference, it should be mentioned that it is also possible to add or rotate keyset using multiple `put_key`
|
||||||
|
commandlines. In this case one `put_key` commandline for each key is used. Each commandline will specify `--key-id` and
|
||||||
|
`--key-version-nr` and one `--key-type` and `--key-data` tuple. However, when rotating or adding a keyset step-by-step,
|
||||||
|
the whole process happens in a `non-atomic` way, which is less reliable. Therefore we will favor the `atomic method`
|
||||||
|
|
||||||
|
In the following examples we assume that the Security Domain is selected and a secure channel is already established.
|
||||||
|
|
||||||
|
|
||||||
|
Example: `3DES` key for `SCP02`
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Let's assume we want to provision a new 3DES keyset that we can use for SCP02. The keyset shall look like this:
|
||||||
|
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| Key Identifier | Keyname | Keyvalue |
|
||||||
|
+================+=========+==================================+
|
||||||
|
| 1 | ENC/KIC | 542C37A6043679F2F9F71116418B1CD5 |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| 2 | MAC/KID | 34F11BAC8E5390B57F4E601372339E3C |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| 3 | DEK/KIK | 5524F4BECFE96FB63FC29D6BAAC6058B |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
|
||||||
|
The keyset shall be a associated with the KVN 46. We have made sure before that KVN 46 is still unused and that this
|
||||||
|
KVN number is actually suitable for SCP02 keys. As we are using 3DES, it is obvious that we have to pass 3 keys with 16
|
||||||
|
byte length.
|
||||||
|
|
||||||
|
To program the key, we may use the following commandline. As we can see, this commandline is almost the exact same as
|
||||||
|
the one from the key rotation example where we were rotating a 3DES key. The only difference is that we didn't specify
|
||||||
|
an old KVN number and that we have chosen a different KVN.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> put_key --key-id 1 --key-type des --key-data 542C37A6043679F2F9F71116418B1CD5 --key-type des --key-data 34F11BAC8E5390B57F4E601372339E3C --key-type des --key-data 5524F4BECFE96FB63FC29D6BAAC6058B --key-version-nr 46
|
||||||
|
|
||||||
|
In case of success, the keyset should appear in the `key_information` among the other keysets that are already present.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> get_data key_information
|
||||||
|
{
|
||||||
|
"key_information": [
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 46,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 46,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 46,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "des",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example: `AES128` key for `SCP80`
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
In this example we intend to provision a new `AES128` keyset that we can use with SCP80 (OTA SMS). The keyset shall look
|
||||||
|
like this:
|
||||||
|
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| Key Identifier | Keyname | Keyvalue |
|
||||||
|
+================+=========+==================================+
|
||||||
|
| 1 | ENC/KIC | 542C37A6043679F2F9F71116418B1CD5 |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| 2 | MAC/KID | 34F11BAC8E5390B57F4E601372339E3C |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| 3 | DEK/KIK | 5524F4BECFE96FB63FC29D6BAAC6058B |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
|
||||||
|
In addition to that, we want to associate this key with KVN 3. We have inspected the currently installed keysets before
|
||||||
|
and made sure that KVN 3 is still unused. We are also aware that for SCP80 we may only use KVN values from 1 to 15.
|
||||||
|
|
||||||
|
For `AES128`, we specify the algorithm using the `--key-type aes` parameter. The selection between `AES128` and `AES256` is
|
||||||
|
done implicitly using the key length. Since we want to use `AES128` in this case, all three keys have a length of 16 byte.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> put_key --key-id 1 --key-type aes --key-data 542C37A6043679F2F9F71116418B1CD5 --key-type aes --key-data 34F11BAC8E5390B57F4E601372339E3C --key-type aes --key-data 5524F4BECFE96FB63FC29D6BAAC6058B --key-version-nr 3
|
||||||
|
|
||||||
|
In case of success, the keyset should appear in the `key_information` among the other keysets that are already present.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> get_data key_information
|
||||||
|
{
|
||||||
|
"key_information": [
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 3,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 3,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 3,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example: `AES256` key for `SCP03`
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Let's assume we want to provision a new `AES256` keyset that we can use for SCP03. The keyset shall look like this:
|
||||||
|
|
||||||
|
+----------------+---------+------------------------------------------------------------------+
|
||||||
|
| Key Identifier | Keyname | Keyvalue |
|
||||||
|
+================+=========+==================================================================+
|
||||||
|
| 1 | ENC/KIC | 542C37A6043679F2F9F71116418B1CD5542C37A6043679F2F9F71116418B1CD5 |
|
||||||
|
+----------------+---------+------------------------------------------------------------------+
|
||||||
|
| 2 | MAC/KID | 34F11BAC8E5390B57F4E601372339E3C34F11BAC8E5390B57F4E601372339E3C |
|
||||||
|
+----------------+---------+------------------------------------------------------------------+
|
||||||
|
| 3 | DEK/KIK | 5524F4BECFE96FB63FC29D6BAAC6058B5524F4BECFE96FB63FC29D6BAAC6058B |
|
||||||
|
+----------------+---------+------------------------------------------------------------------+
|
||||||
|
|
||||||
|
In addition to that, we assume that we want to associate this key with KVN 51. This KVN number falls in the range of
|
||||||
|
48 - 63 and is therefore suitable for a key that shall be usable with SCP03. We also made sure before that KVN 51 is
|
||||||
|
still unused.
|
||||||
|
|
||||||
|
With that we can go ahead and make up the following commandline:
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> put_key --key-id 1 --key-type aes --key-data 542C37A6043679F2F9F71116418B1CD5542C37A6043679F2F9F71116418B1CD5 --key-type aes --key-data 34F11BAC8E5390B57F4E601372339E3C34F11BAC8E5390B57F4E601372339E3C --key-type aes --key-data 5524F4BECFE96FB63FC29D6BAAC6058B5524F4BECFE96FB63FC29D6BAAC6058B --key-version-nr 51
|
||||||
|
|
||||||
|
In case of success, we should see the keyset in the `key_information`
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP02[03]:00:MF/ADF.ISD)> get_data key_information
|
||||||
|
{
|
||||||
|
"key_information": [
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 51,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 51,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 3,
|
||||||
|
"key_version_number": 51,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example: `AES128` key for `SCP81`
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
In this example we will show how to provision a new `AES128` keyset for `SCP81`. We will provision this keyset under
|
||||||
|
KVN 64. The keyset we intend to apply shall look like this:
|
||||||
|
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| Key Identifier | Keyname | Keyvalue |
|
||||||
|
+================+=========+==================================+
|
||||||
|
| 1 | TLS-PSK | 000102030405060708090a0b0c0d0e0f |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
| 2 | DEK/KIK | 000102030405060708090a0b0c0d0e0f |
|
||||||
|
+----------------+---------+----------------------------------+
|
||||||
|
|
||||||
|
With that we can put together the following command line:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
put_key --key-id 1 --key-type tls_psk --key-data 000102030405060708090a0b0c0d0e0f --key-type aes --key-data 000102030405060708090a0b0c0d0e0f --key-version-nr 64
|
||||||
|
|
||||||
|
In case of success, the keyset should appear in the `key_information` as follows:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pySIM-shell (SCP03[03]:00:MF/ADF.ISD-R)> get_data key_information
|
||||||
|
{
|
||||||
|
"key_information": [
|
||||||
|
...,
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 2,
|
||||||
|
"key_version_number": 64,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "aes",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key_information_data": {
|
||||||
|
"key_identifier": 1,
|
||||||
|
"key_version_number": 64,
|
||||||
|
"key_types": [
|
||||||
|
{
|
||||||
|
"type": "tls_psk",
|
||||||
|
"length": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
"""
|
||||||
|
Sphinx extension: auto-generate docs/filesystem.rst from the pySim EF class hierarchy.
|
||||||
|
|
||||||
|
Hooked into Sphinx's ``builder-inited`` event so the file is always regenerated
|
||||||
|
from the live Python classes before Sphinx reads any source files.
|
||||||
|
|
||||||
|
The table of root objects to document is in SECTIONS near the top of this file.
|
||||||
|
EXCLUDED lists CardProfile/CardApplication subclasses intentionally omitted from
|
||||||
|
SECTIONS, with reasons. Both tables are read by tests/unittests/test_fs_coverage.py
|
||||||
|
to ensure every class with EF/DF content is accounted for.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
# Ensure pySim is importable when this module is loaded as a Sphinx extension
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
|
from pySim.filesystem import (CardApplication, CardDF, CardMF, CardEF, # noqa: E402
|
||||||
|
TransparentEF, TransRecEF, LinFixedEF, CyclicEF, BerTlvEF)
|
||||||
|
from pySim.profile import CardProfile # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
# Generic EF base classes whose docstrings describe the *type* of file
|
||||||
|
# (Transparent, LinFixed, ...) rather than a specific file's content.
|
||||||
|
# Suppress those boilerplate texts in the per-EF entries; they are only
|
||||||
|
# useful once, at the top of the document or in a dedicated glossary.
|
||||||
|
_EF_BASE_TYPES = frozenset([TransparentEF,
|
||||||
|
TransRecEF,
|
||||||
|
LinFixedEF,
|
||||||
|
CyclicEF,
|
||||||
|
BerTlvEF])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sections: (heading, module, class-name)
|
||||||
|
# The class must be either a CardProfile (uses .files_in_mf) or a CardDF
|
||||||
|
# subclass (uses .children).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SECTIONS = [
|
||||||
|
('MF / TS 102 221 (UICC)',
|
||||||
|
'pySim.ts_102_221', 'CardProfileUICC'),
|
||||||
|
('ADF.USIM / TS 31.102',
|
||||||
|
'pySim.ts_31_102', 'ADF_USIM'),
|
||||||
|
('ADF.ISIM / TS 31.103',
|
||||||
|
'pySim.ts_31_103', 'ADF_ISIM'),
|
||||||
|
('ADF.HPSIM / TS 31.104',
|
||||||
|
'pySim.ts_31_104', 'ADF_HPSIM'),
|
||||||
|
('DF.GSM + DF.TELECOM / TS 51.011 (SIM)',
|
||||||
|
'pySim.ts_51_011', 'CardProfileSIM'),
|
||||||
|
('CDMA / IS-820 (RUIM)',
|
||||||
|
'pySim.cdma_ruim', 'CardProfileRUIM'),
|
||||||
|
('DF.EIRENE / GSM-R',
|
||||||
|
'pySim.gsm_r', 'DF_EIRENE'),
|
||||||
|
('DF.SYSTEM / sysmocom SJA2+SJA5',
|
||||||
|
'pySim.sysmocom_sja2', 'DF_SYSTEM'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Excluded: {(module, class-name)}
|
||||||
|
# CardProfile and CardApplication subclasses that have EF/DF children but are
|
||||||
|
# intentionally absent from SECTIONS. Keeping this list explicit lets
|
||||||
|
# test_fs_coverage.py detect newly added classes that the developer forgot to
|
||||||
|
# add to either table.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
EXCLUDED = {
|
||||||
|
# eUICC profiles inherit files_in_mf verbatim from CardProfileUICC; the
|
||||||
|
# eUICC-specific content lives in ISD-R / ISD-P applications, not in MF.
|
||||||
|
('pySim.euicc', 'CardProfileEuiccSGP02'),
|
||||||
|
('pySim.euicc', 'CardProfileEuiccSGP22'),
|
||||||
|
('pySim.euicc', 'CardProfileEuiccSGP32'),
|
||||||
|
# CardApplication* classes are thin wrappers that embed an ADF_* instance.
|
||||||
|
# The ADF contents are already documented via the corresponding ADF_* entry
|
||||||
|
# in SECTIONS above.
|
||||||
|
('pySim.ts_31_102', 'CardApplicationUSIM'),
|
||||||
|
('pySim.ts_31_102', 'CardApplicationUSIMnonIMSI'),
|
||||||
|
('pySim.ts_31_103', 'CardApplicationISIM'),
|
||||||
|
('pySim.ts_31_104', 'CardApplicationHPSIM'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# RST underline characters ordered by nesting depth
|
||||||
|
_HEADING_CHARS = ['=', '=', '-', '~', '^', '"']
|
||||||
|
# Level 0 uses '=' with overline (page title).
|
||||||
|
# Level 1 uses '=' without overline (major sections).
|
||||||
|
# Levels 2+ use the remaining characters for DFs.
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# RST formatting helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _heading(title: str, level: int) -> str:
|
||||||
|
"""Return an RST heading string. Level 0 gets an overline."""
|
||||||
|
char = _HEADING_CHARS[level]
|
||||||
|
rule = char * len(title)
|
||||||
|
if level == 0:
|
||||||
|
return f'{rule}\n{title}\n{rule}\n\n'
|
||||||
|
return f'{title}\n{rule}\n\n'
|
||||||
|
|
||||||
|
|
||||||
|
def _json_default(obj):
|
||||||
|
"""Fallback serialiser: bytes -> hex, anything else -> repr."""
|
||||||
|
if isinstance(obj, (bytes, bytearray)):
|
||||||
|
return obj.hex()
|
||||||
|
return repr(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _examples_block(cls) -> str:
|
||||||
|
"""Return RST code-block examples (one per vector), or '' if none exist.
|
||||||
|
|
||||||
|
Each example is rendered as a ``json5`` code-block with the hex-encoded
|
||||||
|
binary as a ``// comment`` on the first line, followed by the decoded JSON.
|
||||||
|
``json5`` is used instead of ``json`` so that Pygments does not flag the
|
||||||
|
``//`` comment as a syntax error.
|
||||||
|
"""
|
||||||
|
vectors = []
|
||||||
|
for attr in ('_test_de_encode', '_test_decode'):
|
||||||
|
v = getattr(cls, attr, None)
|
||||||
|
if v:
|
||||||
|
vectors.extend(v)
|
||||||
|
if not vectors:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
lines = ['**Examples**\n\n']
|
||||||
|
|
||||||
|
for t in vectors:
|
||||||
|
# 2-tuple: (encoded, decoded)
|
||||||
|
# 3-tuple: (encoded, record_nr, decoded) — LinFixedEF / CyclicEF
|
||||||
|
if len(t) >= 3:
|
||||||
|
encoded, record_nr, decoded = t[0], t[1], t[2]
|
||||||
|
comment = f'record {record_nr}: {encoded.lower()}'
|
||||||
|
else:
|
||||||
|
encoded, decoded = t[0], t[1]
|
||||||
|
comment = f'file: {encoded.lower()}'
|
||||||
|
|
||||||
|
json_str = json.dumps(decoded, default=_json_default, indent=2)
|
||||||
|
json_indented = textwrap.indent(json_str, ' ')
|
||||||
|
|
||||||
|
lines.append('.. code-block:: json5\n\n')
|
||||||
|
lines.append(f' // {comment}\n')
|
||||||
|
lines.append(json_indented + '\n')
|
||||||
|
lines.append('\n')
|
||||||
|
|
||||||
|
return ''.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _document_ef(ef: CardEF) -> str:
|
||||||
|
"""Return RST for a single EF. Uses ``rubric`` to stay out of the TOC."""
|
||||||
|
cls = type(ef)
|
||||||
|
|
||||||
|
parts = [ef.fully_qualified_path_str()]
|
||||||
|
if ef.fid:
|
||||||
|
parts.append(f'({ef.fid.upper()})')
|
||||||
|
if ef.desc:
|
||||||
|
parts.append(f'\u2014 {ef.desc}') # em-dash
|
||||||
|
title = ' '.join(parts)
|
||||||
|
|
||||||
|
lines = [f'.. rubric:: {title}\n\n']
|
||||||
|
|
||||||
|
# Only show a docstring if it is specific to this class. EFs that are
|
||||||
|
# direct instances of a base type (TransparentEF, LinFixedEF, ...) carry
|
||||||
|
# only the generic "what is a TransparentEF" boilerplate; named subclasses
|
||||||
|
# without their own __doc__ have cls.__dict__['__doc__'] == None. Either
|
||||||
|
# way, suppress the text here - it belongs at the document level, not
|
||||||
|
# repeated for every single EF entry.
|
||||||
|
doc = None if cls in _EF_BASE_TYPES else cls.__dict__.get('__doc__')
|
||||||
|
if doc:
|
||||||
|
lines.append(inspect.cleandoc(doc) + '\n\n')
|
||||||
|
|
||||||
|
examples = _examples_block(cls)
|
||||||
|
if examples:
|
||||||
|
lines.append(examples)
|
||||||
|
|
||||||
|
return ''.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _document_df(df: CardDF, level: int) -> str:
|
||||||
|
"""Return RST for a DF section and all its children, recursively."""
|
||||||
|
parts = [df.fully_qualified_path_str()]
|
||||||
|
if df.fid:
|
||||||
|
parts.append(f'({df.fid.upper()})')
|
||||||
|
if df.desc:
|
||||||
|
parts.append(f'\u2014 {df.desc}') # em-dash
|
||||||
|
title = ' '.join(parts)
|
||||||
|
|
||||||
|
lines = [_heading(title, level)]
|
||||||
|
|
||||||
|
cls = type(df)
|
||||||
|
doc = None if cls in (CardDF, CardMF) else cls.__dict__.get('__doc__')
|
||||||
|
if doc:
|
||||||
|
lines.append(inspect.cleandoc(doc) + '\n\n')
|
||||||
|
|
||||||
|
for child in df.children.values():
|
||||||
|
if isinstance(child, CardDF):
|
||||||
|
lines.append(_document_df(child, level + 1))
|
||||||
|
elif isinstance(child, CardEF):
|
||||||
|
lines.append(_document_ef(child))
|
||||||
|
|
||||||
|
return ''.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Top-level generator
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def generate_filesystem_rst() -> str:
|
||||||
|
"""Walk all registered sections and return the full RST document as a string."""
|
||||||
|
out = [
|
||||||
|
'.. This file is auto-generated by docs/pysim_fs_sphinx.py — do not edit.\n\n',
|
||||||
|
_heading('Card Filesystem Reference', 0),
|
||||||
|
'This page documents all Elementary Files (EFs) and Dedicated Files (DFs) '
|
||||||
|
'implemented in pySim, organised by their location in the card filesystem.\n\n',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Track already-documented classes so that DFs/EFs shared between profiles
|
||||||
|
# (e.g. DF.TELECOM / DF.GSM present in both CardProfileSIM and CardProfileRUIM)
|
||||||
|
# are only emitted once.
|
||||||
|
seen_types: set = set()
|
||||||
|
|
||||||
|
for section_title, module_path, class_name in SECTIONS:
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
cls = getattr(module, class_name)
|
||||||
|
obj = cls()
|
||||||
|
|
||||||
|
if isinstance(obj, CardProfile):
|
||||||
|
files = obj.files_in_mf
|
||||||
|
elif isinstance(obj, CardApplication):
|
||||||
|
files = list(obj.adf.children.values())
|
||||||
|
elif isinstance(obj, CardDF):
|
||||||
|
files = list(obj.children.values())
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter out files whose class was already documented in an earlier section.
|
||||||
|
files = [f for f in files if type(f) not in seen_types]
|
||||||
|
if not files:
|
||||||
|
continue
|
||||||
|
|
||||||
|
out.append(_heading(section_title, 1))
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
seen_types.add(type(f))
|
||||||
|
if isinstance(f, CardDF):
|
||||||
|
out.append(_document_df(f, level=2))
|
||||||
|
elif isinstance(f, CardEF):
|
||||||
|
out.append(_document_ef(f))
|
||||||
|
|
||||||
|
return ''.join(out)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sphinx integration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _on_builder_inited(app):
|
||||||
|
output_path = os.path.join(app.srcdir, 'filesystem.rst')
|
||||||
|
with open(output_path, 'w') as fh:
|
||||||
|
fh.write(generate_filesystem_rst())
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.connect('builder-inited', _on_builder_inited)
|
||||||
|
return {'version': '0.1', 'parallel_read_safe': True}
|
||||||
+1
-1
@@ -67,7 +67,7 @@ Inspecting applications
|
|||||||
|
|
||||||
To inspect the application PE contents of an existing profile package, sub-command `info` with parameter '--apps' can
|
To inspect the application PE contents of an existing profile package, sub-command `info` with parameter '--apps' can
|
||||||
be used. This command lists out all application and their parameters in detail. This allows an application developer
|
be used. This command lists out all application and their parameters in detail. This allows an application developer
|
||||||
to check if the applet insertaion was carried out as expected.
|
to check if the applet insertion was carried out as expected.
|
||||||
|
|
||||||
Example: Listing applications and their parameters
|
Example: Listing applications and their parameters
|
||||||
::
|
::
|
||||||
|
|||||||
+5
-5
@@ -68,7 +68,7 @@ Usage Examples
|
|||||||
|
|
||||||
suci-tutorial
|
suci-tutorial
|
||||||
cap-tutorial
|
cap-tutorial
|
||||||
|
put_key-tutorial
|
||||||
|
|
||||||
Advanced Topics
|
Advanced Topics
|
||||||
---------------
|
---------------
|
||||||
@@ -602,8 +602,8 @@ This allows for easy interactive modification of records.
|
|||||||
If this command fails before the editor is spawned, it means that the current record contents is not decodable,
|
If this command fails before the editor is spawned, it means that the current record contents is not decodable,
|
||||||
and you should use the :ref:`update_record_decoded` or :ref:`update_record` command.
|
and you should use the :ref:`update_record_decoded` or :ref:`update_record` command.
|
||||||
|
|
||||||
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
|
If this command fails after making your modifications in the editor, it means that the new file contents is not
|
||||||
encodable; please check your input and/or us the raw :ref:`update_record` comamdn.
|
encodable; please check your input and/or use the raw :ref:`update_record` command.
|
||||||
|
|
||||||
|
|
||||||
decode_hex
|
decode_hex
|
||||||
@@ -708,8 +708,8 @@ This allows for easy interactive modification of file contents.
|
|||||||
If this command fails before the editor is spawned, it means that the current file contents is not decodable,
|
If this command fails before the editor is spawned, it means that the current file contents is not decodable,
|
||||||
and you should use the :ref:`update_binary_decoded` or :ref:`update_binary` command.
|
and you should use the :ref:`update_binary_decoded` or :ref:`update_binary` command.
|
||||||
|
|
||||||
If this command fails after making your modificatiosn in the editor, it means that the new file contents is not
|
If this command fails after making your modifications in the editor, it means that the new file contents is not
|
||||||
encodable; please check your input and/or us the raw :ref:`update_binary` comamdn.
|
encodable; please check your input and/or use the raw :ref:`update_binary` command.
|
||||||
|
|
||||||
|
|
||||||
decode_hex
|
decode_hex
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
smpp-ota-tool
|
||||||
|
=============
|
||||||
|
|
||||||
|
The `smpp-ota-tool` allows users to send OTA SMS messages containing APDU scripts (RFM, RAM) via an SMPP server. The
|
||||||
|
intended audience are developers who want to test/evaluate the OTA SMS interface of a SIM/UICC/eUICC. `smpp-ota-tool`
|
||||||
|
is intended to be used as a companion tool for :ref:`pySim-smpp2sim`, however it should be usable on any other SMPP
|
||||||
|
server (such as a production SMSC of a live cellular network) as well.
|
||||||
|
|
||||||
|
From the technical perspective `smpp-ota-tool` takes the role of an SMPP ESME. It takes care of the encoding, encryption
|
||||||
|
and checksumming (signing) of the RFM/RAM OTA SMS and eventually submits it to the SMPP server. The program then waits
|
||||||
|
for a response. The response is automatically parsed and printed on stdout. This makes the program also suitable to be
|
||||||
|
called from shell scripts.
|
||||||
|
|
||||||
|
.. note:: In the following we will we will refer to `SIM` as one of the following: `SIM`, `USIM`, `ISIM`, `UICC`,
|
||||||
|
`eUICC`, `eSIM`.
|
||||||
|
|
||||||
|
Applying OTA keys
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Depending on the `SIM` type you will receive one or more sets of keys which you can use to communicate with the `SIM`
|
||||||
|
through a secure channel protocol. When using the OTA SMS method, the SCP80 protocol is used and it therefore crucial
|
||||||
|
to use a keyset that is actually suitable for SCP80.
|
||||||
|
|
||||||
|
A keyset usually consists of three keys:
|
||||||
|
|
||||||
|
#. KIC: the key used for ciphering (encryption/decryption)
|
||||||
|
#. KID: the key used to compute a cryptographic checksum (signing)
|
||||||
|
#. KIK: the key used to encrypt/decrypt key material (key rotation, adding of new keys)
|
||||||
|
|
||||||
|
From the transport security perspective, only KIC and KID are relevant. The KIK (also referenced as "Data Encryption
|
||||||
|
Key", DEK) is only used when keys are rotated or new keys are added (see also ETSI TS 102 226, section 8.2.1.5).
|
||||||
|
|
||||||
|
When the keyset is programmed into the security domain of the `SIM`, it is tied to a specific cryptographic algorithm
|
||||||
|
(3DES, AES128 or AES256) and a so called Key Version Number (KVN). The term "Key Version Number" is misleading, since
|
||||||
|
it is actually not a version number. It is a unique identifier of a certain keyset which also identifies for which
|
||||||
|
secure channel protocol the keyset may be used. Keysets with a KVN from 1-15 (``0x01``-``0x0F``) are suitable for SCP80.
|
||||||
|
This means that it is not only important to know just the KIC/KID/KIK keys. Also the related algorithms and the KVN
|
||||||
|
numbers must be known.
|
||||||
|
|
||||||
|
.. note:: SCP80 keysets typically start counting from 1 upwards. Typical configurations use a set of 3 keysets with
|
||||||
|
KVN numbers 1-3.
|
||||||
|
|
||||||
|
Addressing an Application
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When communicating with a specific application on a `SIM` via SCP80, it is important to address that application with
|
||||||
|
the correct parameters. The following two parameters must be known in advance:
|
||||||
|
|
||||||
|
#. TAR: The Toolkit Application Reference (TAR) number is a three byte value that uniquely addresses an application
|
||||||
|
on the `SIM`. The exact values may vary (see also ETSI TS 101 220, Table D.1).
|
||||||
|
#. MSL: The Minimum Security Level (MSL) is a bit-field that dictates which of the security measures encoded in the
|
||||||
|
SPI are mandatory (see also ETSI TS 102 225, section 5.1.1).
|
||||||
|
|
||||||
|
A practical example
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. note:: This tutorial assumes that pySim-smpp2sim is running on the local machine with its default parameters.
|
||||||
|
See also :ref:`pySim-smpp2sim`.
|
||||||
|
|
||||||
|
Let's assume that an OTA SMS shall be sent to the SIM RFM application of an sysmoISIM-SJA2. What we want to do is to
|
||||||
|
select DF.GSM and to get the select response back.
|
||||||
|
|
||||||
|
We have received the following key material from the `SIM` vendor:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
KIC1: F09C43EE1A0391665CC9F05AF4E0BD10
|
||||||
|
KID1: 01981F4A20999F62AF99988007BAF6CA
|
||||||
|
KIK1: 8F8AEE5CDCC5D361368BC45673D99195
|
||||||
|
KIC2: 01022916E945B656FDE03F806A105FA2
|
||||||
|
KID2: D326CB69F160333CC5BD1495D448EFD6
|
||||||
|
KIK2: 08037E0590DFE049D4975FFB8652F625
|
||||||
|
KIC3: 2B22824D0D27A3A1CEEC512B312082B4
|
||||||
|
KID3: F1697766925A11F4458295590137B672
|
||||||
|
KIK3: C7EE69B2C5A1C8E160DD36A38EB517B3
|
||||||
|
|
||||||
|
Those are three keysets. The enumeration is directly equal to the KVN used. All three keysets are 3DES keys, which
|
||||||
|
means triple_des_cbc2 is the correct algorithm to use.
|
||||||
|
|
||||||
|
.. note:: The key set configuration can be confirmed by retrieving the key configuration using
|
||||||
|
`get_data key_information` from within an SCP02 session on ADF.ISD.
|
||||||
|
|
||||||
|
In this example we intend to address the SIM RFM application on the `SIM`. Which according to the manual has TAR ``B00010``
|
||||||
|
and MSL ``0x06``. When we hold ``0x06`` = ``0b00000110`` against the SPI coding chart (see also ETSI TS 102 225,
|
||||||
|
section 5.1.1). We can deduct that Ciphering and Cryptographic Checksum are mandatory.
|
||||||
|
|
||||||
|
.. note:: The MSL (see also ETSI TS 102 226, section 6.1) is assigned to an application by the `SIM` issuer. It is a
|
||||||
|
custom decision and may vary with different `SIM` types/profiles. In the case of sysmoISIM-SJS1/SJA2/SJA5 the
|
||||||
|
counter requirement has been waived to simplify lab/research type use. In productive environments, `SIM`
|
||||||
|
applications should ideally use an MSL that makes the counter mandatory.
|
||||||
|
|
||||||
|
In order to select DF.GSM (``0x7F20``) and to retrieve the select response, two APDUs are needed. The first APDU is the
|
||||||
|
select command ``A0A40000027F20`` and the second is the related get-response command ``A0C0000016``. Those APDUs will be
|
||||||
|
concatenated and are sent in a single message. The message containing the concatenated APDUs works as a script that
|
||||||
|
is received by the SIM RFM application and then executed. This method poses some limitations that have to be taken into
|
||||||
|
account when making requests like this (see also ETSI TS 102 226, section 5).
|
||||||
|
|
||||||
|
With this information we may now construct a commandline for `smpp-ota-tool.py`. We will pass the KVN as kid_idx and
|
||||||
|
kic_idx (see also ETSI TS 102 225, Table 2, fields `KIc` and `KID`). Both index values should refer to the same
|
||||||
|
keyset/KVN as keysets should not be mixed. (`smpp-ota-tool` still provides separate parameters anyway to allow testing
|
||||||
|
with invalid keyset combinations)
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ PYTHONPATH=./ ./contrib/smpp-ota-tool.py --kic F09C43EE1A0391665CC9F05AF4E0BD10 --kid 01981F4A20999F62AF99988107BAF6CA --kid_idx 1 --kic_idx 1 --algo-crypt triple_des_cbc2 --algo-auth triple_des_cbc2 --tar B00010 --apdu A0A40000027F20 --apdu A0C0000016
|
||||||
|
2026-02-26 17:13:56 INFO Connecting to localhost:2775...
|
||||||
|
2026-02-26 17:13:56 INFO C-APDU sending: a0a40000027f20a0c0000016...
|
||||||
|
2026-02-26 17:13:56 INFO SMS-TPDU sending: 02700000281506191515b00010da1d6cbbd0d11ce4330d844c7408340943e843f67a6d7b0674730881605fd62d...
|
||||||
|
2026-02-26 17:13:56 INFO SMS-TPDU sent, waiting for response...
|
||||||
|
2026-02-26 17:13:56 INFO SMS-TPDU received: 027100002c12b000107ddf58d1780f771638b3975759f4296cf5c31efc87a16a1b61921426baa16da1b5ba1a9951d59a39
|
||||||
|
2026-02-26 17:13:56 INFO SMS-TPDU decoded: (Container(rpl=44, rhl=18, tar=b'\xb0\x00\x10', cntr=b'\x00\x00\x00\x00\x00', pcntr=0, response_status=uEnumIntegerString.new(0, 'por_ok'), cc_rc=b'\x8f\xea\xf5.\xf4\x0e\xc2\x14', secured_data=b'\x02\x90\x00\x00\x00\xff\xff\x7f \x02\x00\x00\x00\x00\x00\t\xb1\x065\x04\x00\x83\x8a\x83\x8a'), Container(number_of_commands=2, last_status_word=u'9000', last_response_data=u'0000ffff7f2002000000000009b106350400838a838a'))
|
||||||
|
2026-02-26 17:13:56 INFO R-APDU received: 0000ffff7f2002000000000009b106350400838a838a 9000
|
||||||
|
0000ffff7f2002000000000009b106350400838a838a 9000
|
||||||
|
2026-02-26 17:13:56 INFO Disconnecting...
|
||||||
|
|
||||||
|
The result we see is the select response of DF.GSM and a status word indicating that the last command has been
|
||||||
|
processed normally.
|
||||||
|
|
||||||
|
As we can see, this mechanism now allows us to perform small administrative tasks remotely. We can read the contents of
|
||||||
|
files remotely or make changes to files. Depending on the changes we make, there may be security issues arising from
|
||||||
|
replay attacks. With the commandline above, the communication is encrypted and protected by a cryptographic checksum,
|
||||||
|
so an adversary can neither read, nor alter the message. However, an adversary could still replay an intercepted
|
||||||
|
message and the `SIM` would happily execute the contained APDUs again.
|
||||||
|
|
||||||
|
To prevent this, we may include a replay protection counter within the message. In this case, the MSL indicates that a
|
||||||
|
replay protection counter is not required. However, to extended the security of our messages, we may chose to use a
|
||||||
|
counter anyway. In the following example, we will encode a counter value of 100. We will instruct the `SIM` to make sure
|
||||||
|
that the value we send is higher than the counter value that is currently stored in the `SIM`.
|
||||||
|
|
||||||
|
To add a replay connection counter we add the commandline arguments `--cntr-req` to set the counter requirement and
|
||||||
|
`--cntr` to pass the counter value.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ PYTHONPATH=./ ./contrib/smpp-ota-tool.py --kic F09C43EE1A0391665CC9F05AF4E0BD10 --kid 01981F4A20999F62AF99988107BAF6CA --kid_idx 1 --kic_idx 1 --algo-crypt triple_des_cbc2 --algo-auth triple_des_cbc2 --tar B00010 --apdu A0A40000027F20 --apdu A0C0000016 --cntr-req counter_must_be_higher --cntr 100
|
||||||
|
2026-02-26 17:16:39 INFO Connecting to localhost:2775...
|
||||||
|
2026-02-26 17:16:39 INFO C-APDU sending: a0a40000027f20a0c0000016...
|
||||||
|
2026-02-26 17:16:39 INFO SMS-TPDU sending: 02700000281516191515b000103a4f599e94f2b5dcfbbda984761b7977df6514c57a580fb4844787c436d2eade...
|
||||||
|
2026-02-26 17:16:39 INFO SMS-TPDU sent, waiting for response...
|
||||||
|
2026-02-26 17:16:39 INFO SMS-TPDU received: 027100002c12b0001049fb0315f6c6401b553867f412cefaf9355b38271178edb342a3bc9cc7e670cdc1f45eea6ffcbb39
|
||||||
|
2026-02-26 17:16:39 INFO SMS-TPDU decoded: (Container(rpl=44, rhl=18, tar=b'\xb0\x00\x10', cntr=b'\x00\x00\x00\x00d', pcntr=0, response_status=uEnumIntegerString.new(0, 'por_ok'), cc_rc=b'\xa9/\xc7\xc9\x00"\xab5', secured_data=b'\x02\x90\x00\x00\x00\xff\xff\x7f \x02\x00\x00\x00\x00\x00\t\xb1\x065\x04\x00\x83\x8a\x83\x8a'), Container(number_of_commands=2, last_status_word=u'9000', last_response_data=u'0000ffff7f2002000000000009b106350400838a838a'))
|
||||||
|
2026-02-26 17:16:39 INFO R-APDU received: 0000ffff7f2002000000000009b106350400838a838a 9000
|
||||||
|
0000ffff7f2002000000000009b106350400838a838a 9000
|
||||||
|
2026-02-26 17:16:39 INFO Disconnecting...
|
||||||
|
|
||||||
|
The `SIM` has accepted the message. The message got processed and the `SIM` has set its internal to 100. As an experiment,
|
||||||
|
we may try to re-use the counter value:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ PYTHONPATH=./ ./contrib/smpp-ota-tool.py --kic F09C43EE1A0391665CC9F05AF4E0BD10 --kid 01981F4A20999F62AF99988107BAF6CA --kid_idx 1 --kic_idx 1 --algo-crypt triple_des_cbc2 --algo-auth triple_des_cbc2 --tar B00010 --apdu A0A40000027F20 --apdu A0C0000016 --cntr-req counter_must_be_higher --cntr 100
|
||||||
|
2026-02-26 17:16:43 INFO Connecting to localhost:2775...
|
||||||
|
2026-02-26 17:16:43 INFO C-APDU sending: a0a40000027f20a0c0000016...
|
||||||
|
2026-02-26 17:16:43 INFO SMS-TPDU sending: 02700000281516191515b000103a4f599e94f2b5dcfbbda984761b7977df6514c57a580fb4844787c436d2eade...
|
||||||
|
2026-02-26 17:16:43 INFO SMS-TPDU sent, waiting for response...
|
||||||
|
2026-02-26 17:16:43 INFO SMS-TPDU received: 027100000b0ab0001000000000000006
|
||||||
|
2026-02-26 17:16:43 INFO SMS-TPDU decoded: (Container(rpl=11, rhl=10, tar=b'\xb0\x00\x10', cntr=b'\x00\x00\x00\x00\x00', pcntr=0, response_status=uEnumIntegerString.new(6, 'undefined_security_error'), cc_rc=b'', secured_data=b''), None)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/home/user/work/git_master/pysim/./contrib/smpp-ota-tool.py", line 238, in <module>
|
||||||
|
resp, sw = smpp_handler.transceive_apdu(apdu, opts.src_addr, opts.dest_addr, opts.timeout)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/user/work/git_master/pysim/./contrib/smpp-ota-tool.py", line 162, in transceive_apdu
|
||||||
|
raise ValueError("Response does not contain any last_response_data, no R-APDU received!")
|
||||||
|
ValueError: Response does not contain any last_response_data, no R-APDU received!
|
||||||
|
2026-02-26 17:16:43 INFO Disconnecting...
|
||||||
|
|
||||||
|
As we can see, the `SIM` has rejected the message with an `undefined_security_error`. The replay-protection-counter
|
||||||
|
ensures that a message can only be sent once.
|
||||||
|
|
||||||
|
.. note:: The replay-protection-counter is implemented as a 5 byte integer value (see also ETSI TS 102 225, Table 3).
|
||||||
|
When the counter has reached its maximum, it will not overflow nor can it be reset.
|
||||||
|
|
||||||
|
smpp-ota-tool syntax
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. argparse::
|
||||||
|
:module: contrib.smpp-ota-tool
|
||||||
|
:func: option_parser
|
||||||
|
:prog: contrib/smpp-ota-tool.py
|
||||||
@@ -55,3 +55,5 @@ And once your external program is sending SMS to the simulated SMSC, it will log
|
|||||||
SMSPPDownload(DeviceIdentities({'source_dev_id': 'network', 'dest_dev_id': 'uicc'}),Address({'ton_npi': 0, 'call_number': '0123456'}),SMS_TPDU({'tpdu': '400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d'}))
|
SMSPPDownload(DeviceIdentities({'source_dev_id': 'network', 'dest_dev_id': 'uicc'}),Address({'ton_npi': 0, 'call_number': '0123456'}),SMS_TPDU({'tpdu': '400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d'}))
|
||||||
INFO root: ENVELOPE: d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
|
INFO root: ENVELOPE: d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
|
||||||
INFO root: SW 9000: 027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c
|
INFO root: SW 9000: 027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c
|
||||||
|
|
||||||
|
.. note:: for sending OTA SMS messages :ref:`smpp-ota-tool` may be used.
|
||||||
|
|||||||
+1
-1
@@ -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.commonprefix((os.path.realpath(path),self.upp_dir)) != self.upp_dir:
|
if os.path.commonpath((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')
|
||||||
|
|||||||
+11
-3
@@ -27,7 +27,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@@ -44,6 +43,11 @@ from pySim.legacy.ts_51_011 import EF
|
|||||||
from pySim.card_handler import *
|
from pySim.card_handler import *
|
||||||
from pySim.utils import *
|
from pySim.utils import *
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
from pySim.log import PySimLogger
|
||||||
|
|
||||||
|
log = PySimLogger.get(Path(__file__).stem)
|
||||||
|
|
||||||
def parse_options():
|
def parse_options():
|
||||||
|
|
||||||
@@ -185,6 +189,7 @@ def parse_options():
|
|||||||
default=False, action="store_true")
|
default=False, action="store_true")
|
||||||
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
|
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
|
||||||
help="Use automatic card handling machine")
|
help="Use automatic card handling machine")
|
||||||
|
parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
|
||||||
|
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
@@ -430,7 +435,7 @@ def gen_parameters(opts):
|
|||||||
if not re.match('^[0-9a-fA-F]{32}$', ki):
|
if not re.match('^[0-9a-fA-F]{32}$', ki):
|
||||||
raise ValueError('Ki needs to be 128 bits, in hex format')
|
raise ValueError('Ki needs to be 128 bits, in hex format')
|
||||||
else:
|
else:
|
||||||
ki = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
|
ki = os.urandom(16).hex()
|
||||||
|
|
||||||
# OPC (random)
|
# OPC (random)
|
||||||
if opts.opc is not None:
|
if opts.opc is not None:
|
||||||
@@ -441,7 +446,7 @@ def gen_parameters(opts):
|
|||||||
elif opts.op is not None:
|
elif opts.op is not None:
|
||||||
opc = derive_milenage_opc(ki, opts.op)
|
opc = derive_milenage_opc(ki, opts.op)
|
||||||
else:
|
else:
|
||||||
opc = ''.join(['%02x' % random.randrange(0, 256) for i in range(16)])
|
opc = os.urandom(16).hex()
|
||||||
|
|
||||||
pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
|
pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
|
||||||
|
|
||||||
@@ -770,6 +775,9 @@ if __name__ == '__main__':
|
|||||||
# Parse options
|
# Parse options
|
||||||
opts = parse_options()
|
opts = parse_options()
|
||||||
|
|
||||||
|
# Setup logger
|
||||||
|
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
|
||||||
|
|
||||||
# Init card reader driver
|
# Init card reader driver
|
||||||
sl = init_reader(opts)
|
sl = init_reader(opts)
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -25,7 +25,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -46,11 +45,17 @@ from pySim.utils import dec_imsi, dec_iccid
|
|||||||
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
|
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
|
||||||
from pySim.ts_51_011 import EF_SMSP
|
from pySim.ts_51_011 import EF_SMSP
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
from pySim.log import PySimLogger
|
||||||
|
|
||||||
|
log = PySimLogger.get(Path(__file__).stem)
|
||||||
|
|
||||||
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
|
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
|
||||||
argparse_add_reader_args(option_parser)
|
argparse_add_reader_args(option_parser)
|
||||||
|
|
||||||
|
|
||||||
def select_app(adf: str, card: SimCard):
|
def select_app(adf: str, card: SimCard):
|
||||||
"""Select application by its AID"""
|
"""Select application by its AID"""
|
||||||
sw = 0
|
sw = 0
|
||||||
@@ -75,6 +80,9 @@ if __name__ == '__main__':
|
|||||||
# Parse options
|
# Parse options
|
||||||
opts = option_parser.parse_args()
|
opts = option_parser.parse_args()
|
||||||
|
|
||||||
|
# Setup logger
|
||||||
|
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
|
||||||
|
|
||||||
# Init card reader driver
|
# Init card reader driver
|
||||||
sl = init_reader(opts)
|
sl = init_reader(opts)
|
||||||
|
|
||||||
|
|||||||
+49
-91
@@ -24,21 +24,21 @@ import traceback
|
|||||||
import re
|
import re
|
||||||
import cmd2
|
import cmd2
|
||||||
from packaging import version
|
from packaging import version
|
||||||
from cmd2 import style
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pySim.log import PySimLogger
|
from pySim.log import PySimLogger
|
||||||
from osmocom.utils import auto_uint8
|
from osmocom.utils import auto_uint8
|
||||||
|
|
||||||
# cmd2 >= 2.3.0 has deprecated the bg/fg in favor of Bg/Fg :(
|
# cmd2 >= 3.0 replaced Fg + style() with Color + stylize()
|
||||||
if version.parse(cmd2.__version__) < version.parse("2.3.0"):
|
if version.parse(cmd2.__version__) >= version.parse("3.0.0"):
|
||||||
from cmd2 import fg, bg # pylint: disable=no-name-in-module
|
from cmd2 import Color, stylize # pylint: disable=no-name-in-module
|
||||||
RED = fg.red
|
RED = Color.RED
|
||||||
YELLOW = fg.yellow
|
YELLOW = Color.YELLOW
|
||||||
LIGHT_RED = fg.bright_red
|
LIGHT_RED = Color.BRIGHT_RED
|
||||||
LIGHT_GREEN = fg.bright_green
|
LIGHT_GREEN = Color.BRIGHT_GREEN
|
||||||
|
def style(text, fg=None, bg=None, bold=False): # pylint: disable=function-redefined
|
||||||
|
return stylize(text, fg) if fg else text
|
||||||
else:
|
else:
|
||||||
from cmd2 import Fg, Bg # pylint: disable=no-name-in-module
|
from cmd2 import style, Fg # pylint: disable=no-name-in-module
|
||||||
RED = Fg.RED
|
RED = Fg.RED
|
||||||
YELLOW = Fg.YELLOW
|
YELLOW = Fg.YELLOW
|
||||||
LIGHT_RED = Fg.LIGHT_RED
|
LIGHT_RED = Fg.LIGHT_RED
|
||||||
@@ -69,50 +69,26 @@ 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 CardKeyProviderCsv, CardKeyProviderPgsql
|
from pySim.card_key_provider import card_key_provider_argparse_add_args, card_key_provider_init
|
||||||
from pySim.card_key_provider import card_key_provider_register, card_key_provider_get_field, card_key_provider_get
|
from pySim.card_key_provider import card_key_provider_get_field, card_key_provider_get
|
||||||
|
|
||||||
from pySim.app import init_card
|
from pySim.app import init_card
|
||||||
|
|
||||||
log = PySimLogger.get(Path(__file__).stem)
|
log = PySimLogger.get(Path(__file__).stem)
|
||||||
|
|
||||||
class Cmd2Compat(cmd2.Cmd):
|
class PysimApp(cmd2.Cmd):
|
||||||
"""Backwards-compatibility wrapper around cmd2.Cmd to support older and newer
|
|
||||||
releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
|
|
||||||
def run_editor(self, file_path: Optional[str] = None) -> None:
|
|
||||||
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
|
|
||||||
return self._run_editor(file_path) # pylint: disable=no-member
|
|
||||||
else:
|
|
||||||
return super().run_editor(file_path) # pylint: disable=no-member
|
|
||||||
|
|
||||||
class Settable2Compat(cmd2.Settable):
|
|
||||||
"""Backwards-compatibility wrapper around cmd2.Settable to support older and newer
|
|
||||||
releases. See https://github.com/python-cmd2/cmd2/blob/master/CHANGELOG.md"""
|
|
||||||
def __init__(self, name, val_type, description, settable_object, **kwargs):
|
|
||||||
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
|
|
||||||
super().__init__(name, val_type, description, **kwargs) # pylint: disable=no-value-for-parameter
|
|
||||||
else:
|
|
||||||
super().__init__(name, val_type, description, settable_object, **kwargs) # pylint: disable=too-many-function-args
|
|
||||||
|
|
||||||
class PysimApp(Cmd2Compat):
|
|
||||||
CUSTOM_CATEGORY = 'pySim Commands'
|
CUSTOM_CATEGORY = 'pySim Commands'
|
||||||
BANNER = """Welcome to pySim-shell!
|
BANNER = """Welcome to pySim-shell!
|
||||||
(C) 2021-2023 by Harald Welte, sysmocom - s.f.m.c. GmbH and contributors
|
(C) 2021-2023 by Harald Welte, sysmocom - s.f.m.c. GmbH and contributors
|
||||||
Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/shell.html """
|
Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/shell.html """
|
||||||
|
|
||||||
def __init__(self, verbose, card, rs, sl, ch, script=None):
|
def __init__(self, verbose, card, rs, sl, ch, script=None):
|
||||||
if version.parse(cmd2.__version__) < version.parse("2.0.0"):
|
|
||||||
kwargs = {'use_ipython': True}
|
|
||||||
else:
|
|
||||||
kwargs = {'include_ipy': True}
|
|
||||||
|
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self._onchange_verbose('verbose', False, self.verbose);
|
|
||||||
|
|
||||||
# pylint: disable=unexpected-keyword-arg
|
|
||||||
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
|
|
||||||
auto_load_commands=False, startup_script=script, **kwargs)
|
|
||||||
PySimLogger.setup(self.poutput, {logging.WARN: YELLOW})
|
PySimLogger.setup(self.poutput, {logging.WARN: YELLOW})
|
||||||
|
self._onchange_verbose('verbose', False, self.verbose)
|
||||||
|
|
||||||
|
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
|
||||||
|
auto_load_commands=False, startup_script=script, include_ipy=True)
|
||||||
self.intro = style(self.BANNER, fg=RED)
|
self.intro = style(self.BANNER, fg=RED)
|
||||||
self.default_category = 'pySim-shell built-in commands'
|
self.default_category = 'pySim-shell built-in commands'
|
||||||
self.card = None
|
self.card = None
|
||||||
@@ -128,19 +104,24 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
self.apdu_trace = False
|
self.apdu_trace = False
|
||||||
self.apdu_strict = False
|
self.apdu_strict = False
|
||||||
|
|
||||||
self.add_settable(Settable2Compat('numeric_path', bool, 'Print File IDs instead of names', self,
|
self.add_settable(cmd2.Settable('numeric_path', bool,
|
||||||
onchange_cb=self._onchange_numeric_path))
|
'Print File IDs instead of names',
|
||||||
self.add_settable(Settable2Compat('conserve_write', bool, 'Read and compare before write', self,
|
self, onchange_cb=self._onchange_numeric_path))
|
||||||
onchange_cb=self._onchange_conserve_write))
|
self.add_settable(cmd2.Settable('conserve_write', bool,
|
||||||
self.add_settable(Settable2Compat('json_pretty_print', bool, 'Pretty-Print JSON output', self))
|
'Read and compare before write',
|
||||||
self.add_settable(Settable2Compat('apdu_trace', bool, 'Trace and display APDUs exchanged with card', self,
|
self, onchange_cb=self._onchange_conserve_write))
|
||||||
onchange_cb=self._onchange_apdu_trace))
|
self.add_settable(cmd2.Settable('json_pretty_print', bool,
|
||||||
self.add_settable(Settable2Compat('apdu_strict', bool,
|
'Pretty-Print JSON output',
|
||||||
'Enforce APDU responses according to ISO/IEC 7816-3, table 12', self,
|
self))
|
||||||
onchange_cb=self._onchange_apdu_strict))
|
self.add_settable(cmd2.Settable('apdu_trace', bool,
|
||||||
self.add_settable(Settable2Compat('verbose', bool,
|
'Trace and display APDUs exchanged with card',
|
||||||
'Enable/disable verbose logging', self,
|
self, onchange_cb=self._onchange_apdu_trace))
|
||||||
onchange_cb=self._onchange_verbose))
|
self.add_settable(cmd2.Settable('apdu_strict', bool,
|
||||||
|
'Strictly apply APDU format according to ISO/IEC 7816-3, table 12',
|
||||||
|
self))
|
||||||
|
self.add_settable(cmd2.Settable('verbose', bool,
|
||||||
|
'Enable/disable verbose logging',
|
||||||
|
self, onchange_cb=self._onchange_verbose))
|
||||||
self.equip(card, rs)
|
self.equip(card, rs)
|
||||||
|
|
||||||
def equip(self, card, rs):
|
def equip(self, card, rs):
|
||||||
@@ -218,13 +199,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
else:
|
else:
|
||||||
self.card._scc._tp.apdu_tracer = None
|
self.card._scc._tp.apdu_tracer = None
|
||||||
|
|
||||||
def _onchange_apdu_strict(self, param_name, old, new):
|
|
||||||
if self.card:
|
|
||||||
if new == True:
|
|
||||||
self.card._scc._tp.apdu_strict = True
|
|
||||||
else:
|
|
||||||
self.card._scc._tp.apdu_strict = False
|
|
||||||
|
|
||||||
def _onchange_verbose(self, param_name, old, new):
|
def _onchange_verbose(self, param_name, old, new):
|
||||||
PySimLogger.set_verbose(new)
|
PySimLogger.set_verbose(new)
|
||||||
if new == True:
|
if new == True:
|
||||||
@@ -281,7 +255,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
|
apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
|
||||||
apdu_cmd_parser.add_argument('--expect-response-regex', help='match response against regex', type=str, default=None)
|
apdu_cmd_parser.add_argument('--expect-response-regex', help='match response against regex', type=str, default=None)
|
||||||
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
|
apdu_cmd_parser.add_argument('--raw', help='Bypass the logical channel (and secure channel)', action='store_true')
|
||||||
apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string')
|
apdu_cmd_parser.add_argument('APDU', type=is_hexstr, help='APDU as hex string (see also: ISO/IEC 7816-3, section 12.1')
|
||||||
|
|
||||||
@cmd2.with_argparser(apdu_cmd_parser)
|
@cmd2.with_argparser(apdu_cmd_parser)
|
||||||
def do_apdu(self, opts):
|
def do_apdu(self, opts):
|
||||||
@@ -290,14 +264,23 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
|||||||
tracked. Depending on the raw APDU sent, pySim-shell may not continue to work as expected if you e.g. select
|
tracked. Depending on the raw APDU sent, pySim-shell may not continue to work as expected if you e.g. select
|
||||||
a different file."""
|
a different file."""
|
||||||
|
|
||||||
|
if not hasattr(self, 'apdu_strict_warning_displayed') and self.apdu_strict is False:
|
||||||
|
self.poutput("Warning: The default for the setable parameter `apdu_strict` will be changed from")
|
||||||
|
self.poutput(" `False` to `True` in future pySim-shell releases. In case you are using")
|
||||||
|
self.poutput(" the `apdu` command from a script that still mixes APDUs with TPDUs, consider")
|
||||||
|
self.poutput(" fixing or adding a `set apdu_strict false` line at the beginning.")
|
||||||
|
self.apdu_strict_warning_displayed = True;
|
||||||
|
|
||||||
# When sending raw APDUs we access the scc object through _scc member of the card object. It should also be
|
# When sending raw APDUs we access the scc object through _scc member of the card object. It should also be
|
||||||
# noted that the apdu command plays an exceptional role since it is the only card accessing command that
|
# noted that the apdu command plays an exceptional role since it is the only card accessing command that
|
||||||
# can be executed without the presence of a runtime state (self.rs) object. However, this also means that
|
# can be executed without the presence of a runtime state (self.rs) object. However, this also means that
|
||||||
# self.lchan is also not present (see method equip).
|
# self.lchan is also not present (see method equip).
|
||||||
|
self.card._scc._tp.apdu_strict = self.apdu_strict
|
||||||
if opts.raw or self.lchan is None:
|
if opts.raw or self.lchan is None:
|
||||||
data, sw = self.card._scc.send_apdu(opts.APDU, apply_lchan = False)
|
data, sw = self.card._scc.send_apdu(opts.APDU, apply_lchan = False)
|
||||||
else:
|
else:
|
||||||
data, sw = self.lchan.scc.send_apdu(opts.APDU, apply_lchan = False)
|
data, sw = self.lchan.scc.send_apdu(opts.APDU, apply_lchan = False)
|
||||||
|
self.card._scc._tp.apdu_strict = True
|
||||||
if data:
|
if data:
|
||||||
self.poutput("SW: %s, RESP: %s" % (sw, data))
|
self.poutput("SW: %s, RESP: %s" % (sw, data))
|
||||||
else:
|
else:
|
||||||
@@ -1145,18 +1128,6 @@ 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)')
|
||||||
@@ -1169,30 +1140,17 @@ 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
|
||||||
opts = option_parser.parse_args()
|
opts = option_parser.parse_args()
|
||||||
|
|
||||||
# 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})
|
PySimLogger.setup(print, {logging.WARN: YELLOW}, opts.verbose)
|
||||||
if opts.verbose:
|
|
||||||
PySimLogger.set_verbose(True)
|
|
||||||
PySimLogger.set_level(logging.DEBUG)
|
|
||||||
else:
|
|
||||||
PySimLogger.set_verbose(False)
|
|
||||||
PySimLogger.set_level(logging.INFO)
|
|
||||||
|
|
||||||
# Register csv-file as card data provider, either from specified CSV
|
# Init card key provider for automatic card key retrieval
|
||||||
# or from CSV file in home directory
|
card_key_provider_init(opts)
|
||||||
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())
|
||||||
|
|||||||
+7
-4
@@ -26,6 +26,9 @@ 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
|
||||||
@@ -54,7 +57,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.
|
||||||
print("Waiting for card...")
|
log.info("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
|
||||||
@@ -66,7 +69,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:
|
||||||
print("Warning: Could not detect card type - assuming a generic card type...")
|
log.warning("Could not detect card type - assuming a generic card type...")
|
||||||
card = SimCardBase(scc)
|
card = SimCardBase(scc)
|
||||||
generic_card = True
|
generic_card = True
|
||||||
|
|
||||||
@@ -76,7 +79,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.
|
||||||
print("Unsupported card type!")
|
log.warning("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
|
||||||
@@ -87,7 +90,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
|
||||||
|
|
||||||
print("Info: Card is of type: %s" % str(profile))
|
log.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
|
||||||
|
|||||||
+10
-10
@@ -72,10 +72,10 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
|
|||||||
if do[0] == 0x01:
|
if do[0] == 0x01:
|
||||||
self.decoded = {'generic_access_rule': 'always'}
|
self.decoded = {'generic_access_rule': 'always'}
|
||||||
return self.decoded
|
return self.decoded
|
||||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
raise ValueError('Invalid 1-byte generic APDU access rule')
|
||||||
else:
|
else:
|
||||||
if len(do) % 8:
|
if len(do) % 8:
|
||||||
return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
|
raise ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
|
||||||
self.decoded = {'apdu_filter': []}
|
self.decoded = {'apdu_filter': []}
|
||||||
offset = 0
|
offset = 0
|
||||||
while offset < len(do):
|
while offset < len(do):
|
||||||
@@ -90,19 +90,19 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
|
|||||||
return b'\x00'
|
return b'\x00'
|
||||||
if self.decoded['generic_access_rule'] == 'always':
|
if self.decoded['generic_access_rule'] == 'always':
|
||||||
return b'\x01'
|
return b'\x01'
|
||||||
return ValueError('Invalid 1-byte generic APDU access rule')
|
raise ValueError('Invalid 1-byte generic APDU access rule')
|
||||||
else:
|
else:
|
||||||
if not 'apdu_filter' in self.decoded:
|
if not 'apdu_filter' in self.decoded:
|
||||||
return ValueError('Invalid APDU AR DO')
|
raise ValueError('Invalid APDU AR DO')
|
||||||
filters = self.decoded['apdu_filter']
|
filters = self.decoded['apdu_filter']
|
||||||
res = b''
|
res = b''
|
||||||
for f in filters:
|
for f in filters:
|
||||||
if not 'header' in f or not 'mask' in f:
|
if not 'header' in f or not 'mask' in f:
|
||||||
return ValueError('APDU filter must contain header and mask')
|
raise ValueError('APDU filter must contain header and mask')
|
||||||
header_b = h2b(f['header'])
|
header_b = h2b(f['header'])
|
||||||
mask_b = h2b(f['mask'])
|
mask_b = h2b(f['mask'])
|
||||||
if len(header_b) != 4 or len(mask_b) != 4:
|
if len(header_b) != 4 or len(mask_b) != 4:
|
||||||
return ValueError('APDU filter header and mask must each be 4 bytes')
|
raise ValueError('APDU filter header and mask must each be 4 bytes')
|
||||||
res += header_b + mask_b
|
res += header_b + mask_b
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ class ADF_ARAM(CardADF):
|
|||||||
cmd_do_enc = cmd_do.to_ie()
|
cmd_do_enc = cmd_do.to_ie()
|
||||||
cmd_do_len = len(cmd_do_enc)
|
cmd_do_len = len(cmd_do_enc)
|
||||||
if cmd_do_len > 255:
|
if cmd_do_len > 255:
|
||||||
return ValueError('DO > 255 bytes not supported yet')
|
raise ValueError('DO > 255 bytes not supported yet')
|
||||||
else:
|
else:
|
||||||
cmd_do_enc = b''
|
cmd_do_enc = b''
|
||||||
cmd_do_len = 0
|
cmd_do_len = 0
|
||||||
@@ -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-always', action='store_true',
|
|
||||||
help='NFC event access is allowed')
|
|
||||||
nfc_grp.add_argument('--nfc-never', action='store_true',
|
nfc_grp.add_argument('--nfc-never', action='store_true',
|
||||||
help='NFC event access is not allowed')
|
help='NFC event access is not allowed')
|
||||||
|
nfc_grp.add_argument('--nfc-always', action='store_true',
|
||||||
|
help='NFC event access is 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)')
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ class ADF_ARAM(CardADF):
|
|||||||
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
ar_do_content += [{'apdu_ar_do': {'generic_access_rule': 'always'}}]
|
||||||
elif opts.apdu_filter:
|
elif opts.apdu_filter:
|
||||||
if len(opts.apdu_filter) % 16:
|
if len(opts.apdu_filter) % 16:
|
||||||
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
|
raise ValueError(f'Invalid non-modulo-16 length of APDU filter: {len(opts.apdu_filter)}')
|
||||||
offset = 0
|
offset = 0
|
||||||
apdu_filter = []
|
apdu_filter = []
|
||||||
while offset < len(opts.apdu_filter):
|
while offset < len(opts.apdu_filter):
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ 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__)
|
||||||
|
|
||||||
@@ -130,6 +132,31 @@ 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."""
|
||||||
|
|
||||||
@@ -148,24 +175,33 @@ 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, transport_keys: dict):
|
def __init__(self, csv_filename: str, field_cryptor: CardKeyFieldCryptor):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
transport_keys : (see class CardKeyFieldCryptor)
|
field_cryptor : (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 = CardKeyFieldCryptor(transport_keys)
|
self.crypt = field_cryptor
|
||||||
|
|
||||||
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)
|
||||||
@@ -188,14 +224,20 @@ 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, transport_keys: dict):
|
def __init__(self, config_filename: str, field_cryptor: CardKeyFieldCryptor):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
transport_keys : (see class CardKeyFieldCryptor)
|
field_cryptor : (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)
|
||||||
@@ -212,7 +254,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 = CardKeyFieldCryptor(transport_keys)
|
self.crypt = field_cryptor
|
||||||
|
|
||||||
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
|
||||||
@@ -252,6 +294,11 @@ 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.
|
||||||
@@ -305,3 +352,19 @@ 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))
|
||||||
|
|||||||
+2
-2
@@ -128,10 +128,10 @@ class EF_AD(TransparentEF):
|
|||||||
cell_test = 0x04
|
cell_test = 0x04
|
||||||
|
|
||||||
def __init__(self, fid='6f43', sfid=None, name='EF.AD',
|
def __init__(self, fid='6f43', sfid=None, name='EF.AD',
|
||||||
desc='Service Provider Name', size=(3, None), **kwargs):
|
desc='Administrative Data', size=(3, None), **kwargs):
|
||||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||||
self._construct = Struct(
|
self._construct = Struct(
|
||||||
# Byte 1: Display Condition
|
# Byte 1: MS operation mode
|
||||||
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
|
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
|
||||||
# Bytes 2-3: Additional information
|
# Bytes 2-3: Additional information
|
||||||
'additional_info'/Bytes(2),
|
'additional_info'/Bytes(2),
|
||||||
|
|||||||
+36
-12
@@ -19,7 +19,7 @@ import abc
|
|||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
import base64
|
import base64
|
||||||
from twisted.web.server import Request
|
from twisted.web.server import Request
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ class JsonHttpApiFunction(abc.ABC):
|
|||||||
# receives from the a requesting client. The same applies vice versa to class variables that have an "output_"
|
# receives from the a requesting client. The same applies vice versa to class variables that have an "output_"
|
||||||
# prefix.
|
# prefix.
|
||||||
|
|
||||||
# path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder')
|
# path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder', see also method rewrite_url).
|
||||||
path = None
|
path = None
|
||||||
|
|
||||||
# dictionary of input parameters. key is parameter name, value is ApiParam class
|
# dictionary of input parameters. key is parameter name, value is ApiParam class
|
||||||
@@ -336,6 +336,22 @@ class JsonHttpApiFunction(abc.ABC):
|
|||||||
output[p] = p_class.decode(v)
|
output[p] = p_class.decode(v)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def rewrite_url(self, data: dict, url: str) -> Tuple[dict, str]:
|
||||||
|
"""
|
||||||
|
Rewrite a static URL using information passed in the data dict. This method may be overloaded by a derived
|
||||||
|
class to allow fully dynamic URLs. The input parameters required for the URL rewriting may be passed using
|
||||||
|
data parameter. In case those parameters are additional parameters that are not intended to be passed to
|
||||||
|
the encode_client method later, they must be removed explcitly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: (see JsonHttpApiClient and JsonHttpApiServer)
|
||||||
|
url: statically generated URL string (see comment in JsonHttpApiClient)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This implementation is a placeholder in which we do not perform any URL rewriting. We just pass through data
|
||||||
|
# and url unmodified.
|
||||||
|
return data, url
|
||||||
|
|
||||||
class JsonHttpApiClient():
|
class JsonHttpApiClient():
|
||||||
def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str],
|
def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str],
|
||||||
session: requests.Session):
|
session: requests.Session):
|
||||||
@@ -352,8 +368,16 @@ class JsonHttpApiClient():
|
|||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]:
|
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]:
|
||||||
"""Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as
|
"""
|
||||||
json-serializable dict. Output data is returned as json-deserialized dict."""
|
Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as
|
||||||
|
json-serializable fields. `data` may also contain additional parameters required for URL rewriting (see
|
||||||
|
rewrite_url in class JsonHttpApiFunction). Output data is returned as json-deserialized dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Input data required to perform the request.
|
||||||
|
func_call_id: Function Call Identifier, if present a header field is generated automatically.
|
||||||
|
timeout: Maximum amount of time to wait for the request to complete.
|
||||||
|
"""
|
||||||
|
|
||||||
# In case a function caller ID is supplied, use it together with the stored function requestor ID to generate
|
# In case a function caller ID is supplied, use it together with the stored function requestor ID to generate
|
||||||
# and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header
|
# and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header
|
||||||
@@ -362,6 +386,11 @@ class JsonHttpApiClient():
|
|||||||
data = {'header' : {'functionRequesterIdentifier': self.func_req_id,
|
data = {'header' : {'functionRequesterIdentifier': self.func_req_id,
|
||||||
'functionCallIdentifier': func_call_id}} | data
|
'functionCallIdentifier': func_call_id}} | data
|
||||||
|
|
||||||
|
# The URL used for the HTTP request (see below) normally consists of the initially given url_prefix
|
||||||
|
# concatenated with the path defined by the JsonHttpApiFunction definition. This static URL path may be
|
||||||
|
# rewritten by rewrite_url method defined in the JsonHttpApiFunction.
|
||||||
|
data, url = self.api_func.rewrite_url(data, self.url_prefix + self.api_func.path)
|
||||||
|
|
||||||
# Encode the message (the presence of mandatory fields is checked during encoding)
|
# Encode the message (the presence of mandatory fields is checked during encoding)
|
||||||
encoded = json.dumps(self.api_func.encode_client(data))
|
encoded = json.dumps(self.api_func.encode_client(data))
|
||||||
|
|
||||||
@@ -373,7 +402,6 @@ class JsonHttpApiClient():
|
|||||||
req_headers.update(self.api_func.extra_http_req_headers)
|
req_headers.update(self.api_func.extra_http_req_headers)
|
||||||
|
|
||||||
# Perform HTTP request
|
# Perform HTTP request
|
||||||
url = self.url_prefix + self.api_func.path
|
|
||||||
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
|
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
|
||||||
response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
|
response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
|
||||||
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
|
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))
|
||||||
@@ -383,18 +411,14 @@ class JsonHttpApiClient():
|
|||||||
# SGP.22, section 6.5.1)
|
# SGP.22, section 6.5.1)
|
||||||
if response.status_code != self.api_func.expected_http_status:
|
if response.status_code != self.api_func.expected_http_status:
|
||||||
raise HttpStatusError(response)
|
raise HttpStatusError(response)
|
||||||
|
if response.content and not response.headers.get('Content-Type').startswith(req_headers['Content-Type']):
|
||||||
|
raise HttpHeaderError(response)
|
||||||
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
|
if not response.headers.get('X-Admin-Protocol', 'gsma/rsp/v2.unknown').startswith('gsma/rsp/v2.'):
|
||||||
raise HttpHeaderError(response)
|
raise HttpHeaderError(response)
|
||||||
|
|
||||||
# Decode response and return the result back to the caller
|
# Decode response and return the result back to the caller
|
||||||
if response.content:
|
if response.content:
|
||||||
if response.headers.get('Content-Type').startswith('application/json'):
|
output = self.api_func.decode_client(response.json())
|
||||||
output = self.api_func.decode_client(response.json())
|
|
||||||
elif response.headers.get('Content-Type').startswith('text/plain;charset=UTF-8'):
|
|
||||||
output = { 'data': response.content.decode('utf-8') }
|
|
||||||
else:
|
|
||||||
raise HttpHeaderError(f'unimplemented response Content-Type: {response.headers=!r}')
|
|
||||||
|
|
||||||
# In case the response contains a header, check it to make sure that the API call was executed successfully
|
# In case the response contains a header, check it to make sure that the API call was executed successfully
|
||||||
# (the presence of the header field is checked by the decode_client method)
|
# (the presence of the header field is checked by the decode_client method)
|
||||||
if 'header' in output:
|
if 'header' in output:
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ class File:
|
|||||||
self.df_name = None
|
self.df_name = None
|
||||||
self.fill_pattern = None
|
self.fill_pattern = None
|
||||||
self.fill_pattern_repeat = False
|
self.fill_pattern_repeat = False
|
||||||
|
self.pstdo = None # pinStatusTemplateDO, mandatory for DF/ADF
|
||||||
|
self.lcsi = None # optional life cycle status indicator
|
||||||
# apply some defaults from profile
|
# apply some defaults from profile
|
||||||
if self.template:
|
if self.template:
|
||||||
self.from_template(self.template)
|
self.from_template(self.template)
|
||||||
@@ -278,6 +280,8 @@ class File:
|
|||||||
elif self.file_type in ['MF', 'DF', 'ADF']:
|
elif self.file_type in ['MF', 'DF', 'ADF']:
|
||||||
fdb_dec['file_type'] = 'df'
|
fdb_dec['file_type'] = 'df'
|
||||||
fdb_dec['structure'] = 'no_info_given'
|
fdb_dec['structure'] = 'no_info_given'
|
||||||
|
# pinStatusTemplateDO is mandatory for DF/ADF
|
||||||
|
fileDescriptor['pinStatusTemplateDO'] = self.pstdo
|
||||||
# build file descriptor based on above input data
|
# build file descriptor based on above input data
|
||||||
fd_dict = {}
|
fd_dict = {}
|
||||||
if len(fdb_dec):
|
if len(fdb_dec):
|
||||||
@@ -304,6 +308,8 @@ class File:
|
|||||||
# desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles
|
# desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles
|
||||||
# downloaded to a V2.2 or earlier eUICC.
|
# downloaded to a V2.2 or earlier eUICC.
|
||||||
fileDescriptor['proprietaryEFInfo'] = pefi
|
fileDescriptor['proprietaryEFInfo'] = pefi
|
||||||
|
if self.lcsi:
|
||||||
|
fileDescriptor['lcsi'] = self.lcsi
|
||||||
logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor))
|
logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor))
|
||||||
return fileDescriptor
|
return fileDescriptor
|
||||||
|
|
||||||
@@ -323,6 +329,8 @@ class File:
|
|||||||
if efFileSize:
|
if efFileSize:
|
||||||
self._file_size = self._decode_file_size(efFileSize)
|
self._file_size = self._decode_file_size(efFileSize)
|
||||||
|
|
||||||
|
self.pstdo = fileDescriptor.get('pinStatusTemplateDO', None)
|
||||||
|
self.lcsi = fileDescriptor.get('lcsi', None)
|
||||||
pefi = fileDescriptor.get('proprietaryEFInfo', {})
|
pefi = fileDescriptor.get('proprietaryEFInfo', {})
|
||||||
securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None)
|
securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None)
|
||||||
if securityAttributesReferenced:
|
if securityAttributesReferenced:
|
||||||
@@ -433,7 +441,7 @@ class File:
|
|||||||
elif k == 'fillFileContent':
|
elif k == 'fillFileContent':
|
||||||
stream.write(v)
|
stream.write(v)
|
||||||
else:
|
else:
|
||||||
return ValueError("Unknown key '%s' in tuple list" % k)
|
raise ValueError("Unknown key '%s' in tuple list" % k)
|
||||||
return stream.getvalue()
|
return stream.getvalue()
|
||||||
|
|
||||||
def file_content_to_tuples(self, optimize:bool = False) -> List[Tuple]:
|
def file_content_to_tuples(self, optimize:bool = False) -> List[Tuple]:
|
||||||
|
|||||||
+35
-32
@@ -20,22 +20,25 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import pprint
|
import pprint
|
||||||
from typing import List, Generator
|
from typing import Generator, Union
|
||||||
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 = some_file.open('rb').read()
|
der_input = open('some_file', 'rb').read()
|
||||||
pes = ProfileElementSequence.from_der(der_input)
|
pes = ProfileElementSequence.from_der(der_input)
|
||||||
p = pers.BatchPersonalization(
|
p = BatchPersonalization(
|
||||||
n=10,
|
n=10,
|
||||||
src_pes=pes,
|
src_pes=pes,
|
||||||
csv_rows=get_csv_reader())
|
csv_rows=get_csv_reader())
|
||||||
@@ -57,9 +60,12 @@ class BatchPersonalization:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
class ParamAndSrc:
|
class ParamAndSrc:
|
||||||
'tie a ConfigurableParameter to a source of actual values'
|
"""tie a ConfigurableParameter to a source of actual values"""
|
||||||
def __init__(self, param: ConfigurableParameter, src: param_source.ParamSource):
|
def __init__(self, param: ConfigurableParameter, src: param_source.ParamSource):
|
||||||
self.param = param
|
if isinstance(param, type):
|
||||||
|
self.param_cls = param
|
||||||
|
else:
|
||||||
|
self.param_cls = param.__class__
|
||||||
self.src = src
|
self.src = src
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
@@ -74,10 +80,10 @@ class BatchPersonalization:
|
|||||||
copied.
|
copied.
|
||||||
params: list of ParamAndSrc instances, defining a ConfigurableParameter and corresponding ParamSource to fill in
|
params: list of ParamAndSrc instances, defining a ConfigurableParameter and corresponding ParamSource to fill in
|
||||||
profile values.
|
profile values.
|
||||||
csv_rows: A list or generator producing all CSV rows one at a time, starting with a row containing the column
|
csv_rows: A generator (e.g. iter(list_of_rows)) producing all CSV rows one at a time, starting with a row
|
||||||
headers. This is compatible with the python csv.reader. Each row gets passed to
|
containing the column headers. This is compatible with the python csv.reader. Each row gets passed to
|
||||||
ParamSource.get_next(), such that ParamSource implementations can access the row items.
|
ParamSource.get_next(), such that ParamSource implementations can access the row items. See
|
||||||
See param_source.CsvSource.
|
param_source.CsvSource.
|
||||||
"""
|
"""
|
||||||
self.n = n
|
self.n = n
|
||||||
self.params = params or []
|
self.params = params or []
|
||||||
@@ -85,7 +91,7 @@ class BatchPersonalization:
|
|||||||
self.csv_rows = csv_rows
|
self.csv_rows = csv_rows
|
||||||
|
|
||||||
def add_param_and_src(self, param:ConfigurableParameter, src:param_source.ParamSource):
|
def add_param_and_src(self, param:ConfigurableParameter, src:param_source.ParamSource):
|
||||||
self.params.append(BatchPersonalization.ParamAndSrc(param=param, src=src))
|
self.params.append(BatchPersonalization.ParamAndSrc(param, src))
|
||||||
|
|
||||||
def generate_profiles(self):
|
def generate_profiles(self):
|
||||||
# get first row of CSV: column names
|
# get first row of CSV: column names
|
||||||
@@ -112,10 +118,10 @@ class BatchPersonalization:
|
|||||||
try:
|
try:
|
||||||
input_value = p.src.get_next(csv_row=csv_row)
|
input_value = p.src.get_next(csv_row=csv_row)
|
||||||
assert input_value is not None
|
assert input_value is not None
|
||||||
value = p.param.__class__.validate_val(input_value)
|
value = p.param_cls.validate_val(input_value)
|
||||||
p.param.__class__.apply_val(pes, value)
|
p.param_cls.apply_val(pes, value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f'{p.param.name} fed by {p.src.name}: {e}') from e
|
raise ValueError(f'{p.param_cls.get_name()} fed by {p.src.name}: {e}') from e
|
||||||
|
|
||||||
yield pes
|
yield pes
|
||||||
|
|
||||||
@@ -128,14 +134,14 @@ class UppAudit(dict):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_der(cls, der: bytes, params: List, der_size=False, additional_sd_keys=False):
|
def from_der(cls, der: bytes, params: ParamList, 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)
|
||||||
|
|
||||||
@@ -151,7 +157,7 @@ class UppAudit(dict):
|
|||||||
Scp80Kvn03. So we would not show kvn 0x04..0x0f in an audit. additional_sd_keys=True includes audits of all SD
|
Scp80Kvn03. So we would not show kvn 0x04..0x0f in an audit. additional_sd_keys=True includes audits of all SD
|
||||||
key KVN there may be in the UPP. This helps to spot SD keys that may already be present in a UPP template, with
|
key KVN there may be in the UPP. This helps to spot SD keys that may already be present in a UPP template, with
|
||||||
unexpected / unusual kvn.
|
unexpected / unusual kvn.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# make an instance of this class
|
# make an instance of this class
|
||||||
upp_audit = cls()
|
upp_audit = cls()
|
||||||
@@ -180,11 +186,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[audit_key] = set((audit_val, ))
|
upp_audit.add_values({audit_key: audit_val})
|
||||||
|
|
||||||
return upp_audit
|
return upp_audit
|
||||||
|
|
||||||
def get_single_val(self, key, validate=True, allow_absent=False, absent_val=None):
|
def get_single_val(self, key, 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,
|
||||||
@@ -224,7 +230,7 @@ class UppAudit(dict):
|
|||||||
|
|
||||||
v = try_single_val(v)
|
v = try_single_val(v)
|
||||||
if isinstance(v, bytes):
|
if isinstance(v, bytes):
|
||||||
v = bytes_to_hexstr(v)
|
v = b2h(v)
|
||||||
if v is None:
|
if v is None:
|
||||||
return 'not present'
|
return 'not present'
|
||||||
return str(v)
|
return str(v)
|
||||||
@@ -234,21 +240,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):
|
||||||
"""self and src are both a dict of sets.
|
"""Merge a plain dict of values into self, which is a dict of sets.
|
||||||
For example from
|
For example from
|
||||||
self == { 'a': set((123,)) }
|
self == { 'a': {123} }
|
||||||
and
|
and
|
||||||
src == { 'a': set((456,)), 'b': set((789,)) }
|
src == { 'a': 456, 'b': 789 }
|
||||||
then after this function call:
|
then after this function call:
|
||||||
self == { 'a': set((123, 456,)), 'b': set((789,)) }
|
self == { 'a': {123, 456}, 'b': {789} }
|
||||||
"""
|
"""
|
||||||
assert isinstance(src, dict)
|
assert isinstance(src, dict)
|
||||||
for key, srcvalset in src.items():
|
for key, srcval 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(srcvalset)
|
dstvalset.add(srcval)
|
||||||
|
|
||||||
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()))
|
||||||
@@ -273,7 +279,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:List):
|
def __init__(self, params: ParamList):
|
||||||
assert params
|
assert params
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|
||||||
@@ -317,7 +323,7 @@ class BatchAudit(list):
|
|||||||
return batch_audit
|
return batch_audit
|
||||||
|
|
||||||
def to_csv_rows(self, headers=True, sort_key=None):
|
def to_csv_rows(self, headers=True, sort_key=None):
|
||||||
'''generator that yields all audits' values as rows, useful feed to a csv.writer.'''
|
"""generator that yields all audits' values as rows, useful feed to a csv.writer."""
|
||||||
columns = set()
|
columns = set()
|
||||||
for audit in self:
|
for audit in self:
|
||||||
columns.update(audit.keys())
|
columns.update(audit.keys())
|
||||||
@@ -330,9 +336,6 @@ 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 = {}
|
||||||
@@ -340,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 bytes_to_hexstr(item)
|
return b2h(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):
|
||||||
|
|||||||
@@ -37,13 +37,10 @@ class ParamSource:
|
|||||||
name = "none"
|
name = "none"
|
||||||
numeric_base = None # or 10 or 16
|
numeric_base = None # or 10 or 16
|
||||||
|
|
||||||
@classmethod
|
def __init__(self, input_str:str):
|
||||||
def from_str(cls, s:str):
|
"""Subclasses should call super().__init__(input_str) before evaluating self.input_str. Each subclass __init__()
|
||||||
"""Subclasses implement this:
|
may in turn manipulate self.input_str to apply expansions or decodings."""
|
||||||
if a parameter source defines some string input magic, override this function.
|
self.input_str = input_str
|
||||||
For example, a RandomDigitSource derives the number of digits from the string length,
|
|
||||||
so the user can enter '0000' to get a four digit random number."""
|
|
||||||
return cls(s)
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
def get_next(self, csv_row:dict=None):
|
||||||
"""Subclasses implement this: return the next value from the parameter source.
|
"""Subclasses implement this: return the next value from the parameter source.
|
||||||
@@ -51,146 +48,143 @@ class ParamSource:
|
|||||||
This default implementation is an empty source."""
|
This default implementation is an empty source."""
|
||||||
raise ParamSourceExhaustedExn()
|
raise ParamSourceExhaustedExn()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_str(cls, input_str:str):
|
||||||
|
"""compatibility with earlier version of ParamSource. Just use the constructor."""
|
||||||
|
return cls(input_str)
|
||||||
|
|
||||||
class ConstantSource(ParamSource):
|
class ConstantSource(ParamSource):
|
||||||
"""one value for all"""
|
"""one value for all"""
|
||||||
name = "constant"
|
name = "constant"
|
||||||
|
|
||||||
def __init__(self, val:str):
|
|
||||||
self.val = val
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
def get_next(self, csv_row:dict=None):
|
||||||
return self.val
|
return self.input_str
|
||||||
|
|
||||||
class InputExpandingParamSource(ParamSource):
|
class InputExpandingParamSource(ParamSource):
|
||||||
|
|
||||||
|
def __init__(self, input_str:str):
|
||||||
|
super().__init__(input_str)
|
||||||
|
self.input_str = self.expand_input_str(self.input_str)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def expand_str(cls, s:str):
|
def expand_input_str(cls, input_str:str):
|
||||||
# user convenience syntax '0*32' becomes '00000000000000000000000000000000'
|
# user convenience syntax '0*32' becomes '00000000000000000000000000000000'
|
||||||
if "*" not in s:
|
if "*" not in input_str:
|
||||||
return s
|
return input_str
|
||||||
tokens = re.split(r"([^ \t]+)[ \t]*\*[ \t]*([0-9]+)", s)
|
# re: "XX * 123" with optional spaces
|
||||||
|
tokens = re.split(r"([^ \t]+)[ \t]*\*[ \t]*([0-9]+)", input_str)
|
||||||
if len(tokens) < 3:
|
if len(tokens) < 3:
|
||||||
return s
|
return input_str
|
||||||
parts = []
|
parts = []
|
||||||
for unchanged, snippet, repeat_str in zip(tokens[0::3], tokens[1::3], tokens[2::3]):
|
for unchanged, snippet, repeat_str in zip(tokens[0::3], tokens[1::3], tokens[2::3]):
|
||||||
parts.append(unchanged)
|
parts.append(unchanged)
|
||||||
repeat = int(repeat_str)
|
repeat = int(repeat_str)
|
||||||
parts.append(snippet * repeat)
|
parts.append(snippet * repeat)
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
@classmethod
|
return "".join(parts)
|
||||||
def from_str(cls, s:str):
|
|
||||||
return cls(cls.expand_str(s))
|
|
||||||
|
|
||||||
class DecimalRangeSource(InputExpandingParamSource):
|
class DecimalRangeSource(InputExpandingParamSource):
|
||||||
"""abstract: decimal numbers with a value range"""
|
"""abstract: decimal numbers with a value range"""
|
||||||
|
|
||||||
numeric_base = 10
|
numeric_base = 10
|
||||||
|
|
||||||
def __init__(self, num_digits, first_value, last_value):
|
def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
|
||||||
"""
|
"""Constructor to set up values from a (user entered) string: DecimalRangeSource(input_str).
|
||||||
See also from_str().
|
Constructor to set up values directly: DecimalRangeSource(num_digits=3, first_value=123, last_value=456)
|
||||||
|
|
||||||
All arguments are integer values, and are converted to int if necessary, so a string of an integer is fine.
|
num_digits produces leading zeros when first_value..last_value are shorter.
|
||||||
num_digits: fixed number of digits (possibly with leading zeros) to generate.
|
|
||||||
first_value, last_value: the decimal range in which to provide digits.
|
|
||||||
"""
|
"""
|
||||||
num_digits = int(num_digits)
|
assert ((input_str is not None and (num_digits, first_value, last_value) == (None, None, None))
|
||||||
first_value = int(first_value)
|
or (input_str is None and None not in (num_digits, first_value, last_value)))
|
||||||
last_value = int(last_value)
|
|
||||||
|
if input_str is not None:
|
||||||
|
super().__init__(input_str)
|
||||||
|
|
||||||
|
input_str = self.input_str
|
||||||
|
|
||||||
|
if ".." in input_str:
|
||||||
|
first_str, last_str = input_str.split('..')
|
||||||
|
first_str = first_str.strip()
|
||||||
|
last_str = last_str.strip()
|
||||||
|
else:
|
||||||
|
first_str = input_str.strip()
|
||||||
|
last_str = None
|
||||||
|
|
||||||
|
num_digits = len(first_str)
|
||||||
|
first_value = int(first_str)
|
||||||
|
last_value = int(last_str if last_str is not None else "9" * num_digits)
|
||||||
|
|
||||||
assert num_digits > 0
|
assert num_digits > 0
|
||||||
assert first_value <= last_value
|
assert first_value <= last_value
|
||||||
self.num_digits = num_digits
|
self.num_digits = num_digits
|
||||||
self.val_first_last = (first_value, last_value)
|
self.first_value = first_value
|
||||||
|
self.last_value = last_value
|
||||||
|
|
||||||
def val_to_digit(self, val:int):
|
def val_to_digit(self, val:int):
|
||||||
return "%0*d" % (self.num_digits, val) # pylint: disable=consider-using-f-string
|
return "%0*d" % (self.num_digits, val) # pylint: disable=consider-using-f-string
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_str(cls, s:str):
|
|
||||||
s = cls.expand_str(s)
|
|
||||||
|
|
||||||
if ".." in s:
|
|
||||||
first_str, last_str = s.split('..')
|
|
||||||
first_str = first_str.strip()
|
|
||||||
last_str = last_str.strip()
|
|
||||||
else:
|
|
||||||
first_str = s.strip()
|
|
||||||
last_str = None
|
|
||||||
|
|
||||||
first_value = int(first_str)
|
|
||||||
last_value = int(last_str) if last_str is not None else "9" * len(first_str)
|
|
||||||
return cls(num_digits=len(first_str), first_value=first_value, last_value=last_value)
|
|
||||||
|
|
||||||
class RandomSourceMixin:
|
class RandomSourceMixin:
|
||||||
random_impl = secrets.SystemRandom()
|
random_impl = secrets.SystemRandom()
|
||||||
|
|
||||||
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 bytes
|
# try to generate random digits that are always different from previously produced random digits
|
||||||
attempts = 10
|
for _ in range(10):
|
||||||
while True:
|
val = self.random_impl.randint(self.first_value, self.last_value)
|
||||||
val = self.random_impl.randint(*self.val_first_last)
|
if val not in self.used_keys:
|
||||||
if val in RandomDigitSource.used_keys:
|
break
|
||||||
attempts -= 1
|
self.used_keys.add(val)
|
||||||
if attempts:
|
|
||||||
continue
|
|
||||||
RandomDigitSource.used_keys.add(val)
|
|
||||||
break
|
|
||||||
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):
|
||||||
|
super().__init__(input_str)
|
||||||
|
input_str = self.input_str
|
||||||
|
|
||||||
def __init__(self, num_digits):
|
num_digits = len(input_str.strip())
|
||||||
"""see from_str()"""
|
|
||||||
num_digits = int(num_digits)
|
|
||||||
if num_digits < 1:
|
if num_digits < 1:
|
||||||
raise ValueError("zero number of digits")
|
raise ValueError("zero number of digits")
|
||||||
# hex digits always come in two
|
# hex digits always come in two
|
||||||
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
|
||||||
attempts = 10
|
for _ in range(10):
|
||||||
while True:
|
|
||||||
val = self.random_impl.randbytes(self.num_digits // 2)
|
val = self.random_impl.randbytes(self.num_digits // 2)
|
||||||
if val in RandomHexDigitSource.used_keys:
|
if val not in self.used_keys:
|
||||||
attempts -= 1
|
break
|
||||||
if attempts:
|
self.used_keys.add(val)
|
||||||
continue
|
|
||||||
RandomHexDigitSource.used_keys.add(val)
|
|
||||||
break
|
|
||||||
|
|
||||||
return b2h(val)
|
return b2h(val)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_str(cls, s:str):
|
|
||||||
s = cls.expand_str(s)
|
|
||||||
return cls(num_digits=len(s.strip()))
|
|
||||||
|
|
||||||
class IncDigitSource(DecimalRangeSource):
|
class IncDigitSource(DecimalRangeSource):
|
||||||
"""incrementing sequence of digits"""
|
"""incrementing sequence of digits"""
|
||||||
name = "incrementing decimal digits"
|
name = "incrementing decimal digits"
|
||||||
|
|
||||||
def __init__(self, num_digits, first_value, last_value):
|
def __init__(self, input_str:str=None, num_digits:int=None, first_value:int=None, last_value:int=None):
|
||||||
super().__init__(num_digits, first_value, last_value)
|
"""input_str: the range of values to iterate. Format: 'FIRST..LAST' (e.g. '0001..9999') or
|
||||||
|
just 'FIRST' (iterates to the maximum value for the given digit width). Leading zeros in
|
||||||
|
FIRST determine the digit width and are preserved in returned values."""
|
||||||
|
super().__init__(input_str, num_digits, first_value, last_value)
|
||||||
self.next_val = None
|
self.next_val = None
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Restart from the first value of the defined range passed to __init__()."""
|
"""Restart from the first value of the defined range passed to __init__()."""
|
||||||
self.next_val = self.val_first_last[0]
|
self.next_val = self.first_value
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
def get_next(self, csv_row:dict=None):
|
||||||
val = self.next_val
|
val = self.next_val
|
||||||
@@ -200,7 +194,7 @@ class IncDigitSource(DecimalRangeSource):
|
|||||||
returnval = self.val_to_digit(val)
|
returnval = self.val_to_digit(val)
|
||||||
|
|
||||||
val += 1
|
val += 1
|
||||||
if val > self.val_first_last[1]:
|
if val > self.last_value:
|
||||||
self.next_val = None
|
self.next_val = None
|
||||||
else:
|
else:
|
||||||
self.next_val = val
|
self.next_val = val
|
||||||
@@ -211,18 +205,17 @@ class CsvSource(ParamSource):
|
|||||||
"""apply a column from a CSV row, as passed in to ParamSource.get_next(csv_row)"""
|
"""apply a column from a CSV row, as passed in to ParamSource.get_next(csv_row)"""
|
||||||
name = "from CSV"
|
name = "from CSV"
|
||||||
|
|
||||||
def __init__(self, csv_column):
|
def __init__(self, input_str:str):
|
||||||
"""
|
"""input_str: the CSV column name to read values from.
|
||||||
csv_column: column name indicating the column to use for this parameter.
|
The caller passes the current CSV row to get_next(), from which CsvSource picks the column matching
|
||||||
This name is used in get_next(): the caller passes the current CSV row to get_next(), from which
|
this name."""
|
||||||
CsvSource picks the column with the name matching csv_column.
|
super().__init__(input_str)
|
||||||
"""
|
self.csv_column = self.input_str
|
||||||
self.csv_column = csv_column
|
|
||||||
|
|
||||||
def get_next(self, csv_row:dict=None):
|
def get_next(self, csv_row:dict=None):
|
||||||
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 not val:
|
if val is None:
|
||||||
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
|
||||||
|
|||||||
@@ -16,10 +16,9 @@
|
|||||||
# 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 osmocom.tlv import camel_to_snake
|
from osmocom.tlv import camel_to_snake
|
||||||
@@ -55,22 +54,6 @@ class ClassVarMeta(abc.ABCMeta):
|
|||||||
setattr(x, k, v)
|
setattr(x, k, v)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def file_tuples_content_as_bytes(l: List[Tuple]) -> Optional[bytes]:
|
|
||||||
"""linearize a list of fillFileContent / fillFileOffset tuples into a stream of bytes."""
|
|
||||||
stream = io.BytesIO()
|
|
||||||
for k, v in l:
|
|
||||||
if k == 'doNotCreate':
|
|
||||||
return None
|
|
||||||
if k == 'fileDescriptor':
|
|
||||||
pass
|
|
||||||
elif k == 'fillFileOffset':
|
|
||||||
stream.seek(v, os.SEEK_CUR)
|
|
||||||
elif k == 'fillFileContent':
|
|
||||||
stream.write(v)
|
|
||||||
else:
|
|
||||||
return ValueError("Unknown key '%s' in tuple list" % k)
|
|
||||||
return stream.getvalue()
|
|
||||||
|
|
||||||
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
||||||
r"""Base class representing a part of the eSIM profile that is configurable during the
|
r"""Base class representing a part of the eSIM profile that is configurable during the
|
||||||
personalization process (with dynamic data from elsewhere).
|
personalization process (with dynamic data from elsewhere).
|
||||||
@@ -253,7 +236,7 @@ class ConfigurableParameter(abc.ABC, metaclass=ClassVarMeta):
|
|||||||
if val is None:
|
if val is None:
|
||||||
val = v
|
val = v
|
||||||
elif val != v:
|
elif val != v:
|
||||||
raise ValueError(f'get_value_from_pes(): got distinct values: {val!r} != {v!r}')
|
raise ValueError(f'get_value_from_pes(): got distinct values: {val!r} != {v!r}')
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -344,6 +327,7 @@ class DecimalHexParam(DecimalParam):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val):
|
||||||
val = super().validate_val(val)
|
val = super().validate_val(val)
|
||||||
|
assert isinstance(val, str)
|
||||||
val = ''.join('%02x' % ord(x) for x in val)
|
val = ''.join('%02x' % ord(x) for x in val)
|
||||||
if cls.rpad is not None:
|
if cls.rpad is not None:
|
||||||
c = cls.rpad_char
|
c = cls.rpad_char
|
||||||
@@ -353,7 +337,7 @@ class DecimalHexParam(DecimalParam):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def decimal_hex_to_str(cls, val):
|
def decimal_hex_to_str(cls, val):
|
||||||
'useful for get_values_from_pes() implementations of subclasses'
|
"""useful for get_values_from_pes() implementations of subclasses"""
|
||||||
if isinstance(val, bytes):
|
if isinstance(val, bytes):
|
||||||
val = b2h(val)
|
val = b2h(val)
|
||||||
assert isinstance(val, hexstr)
|
assert isinstance(val, hexstr)
|
||||||
@@ -434,67 +418,69 @@ class BinaryParam(ConfigurableParameter):
|
|||||||
|
|
||||||
|
|
||||||
class EnumParam(ConfigurableParameter):
|
class EnumParam(ConfigurableParameter):
|
||||||
value_map = {
|
"""ConfigurableParameter for named integer enumeration values.
|
||||||
# For example:
|
|
||||||
#'Meaningful label for value 23': 0x23,
|
Subclasses must define a nested enum.IntEnum named 'Values' listing all valid names and their
|
||||||
# Where 0x23 is a valid value to use for apply_val().
|
integer codes. apply_val() and get_values_from_pes() are not implemented here and this must
|
||||||
}
|
be inherited from another mixin."""
|
||||||
_value_map_reverse = None
|
|
||||||
|
class Values(enum.IntEnum):
|
||||||
|
pass # subclasses override this
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val) -> int:
|
||||||
orig_val = val
|
if isinstance(val, int):
|
||||||
enum_val = None
|
try:
|
||||||
if isinstance(val, str):
|
return int(cls.Values(val))
|
||||||
enum_name = val
|
except ValueError:
|
||||||
enum_val = cls.map_name_to_val(enum_name)
|
pass
|
||||||
|
elif isinstance(val, str):
|
||||||
|
member = cls.map_name_to_val(val, strict=False)
|
||||||
|
if member is not None:
|
||||||
|
return member
|
||||||
|
|
||||||
# if the str is not one of the known value_map.keys(), is it maybe one of value_map.keys()?
|
valid = ', '.join(m.name for m in cls.Values)
|
||||||
if enum_val is None and val in cls.value_map.values():
|
raise ValueError(f"{cls.get_name()}: invalid argument: {val!r}. Valid arguments are: {valid}")
|
||||||
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):
|
def map_name_to_val(cls, name: str, strict=True) -> int:
|
||||||
val = cls.value_map.get(name)
|
"""Return the integer value for a given enum member name. Performs an exact match first,
|
||||||
if val is not None:
|
then falls back to fuzzy matching (case-insensitive, punctuation-insensitive)."""
|
||||||
return val
|
try:
|
||||||
|
return int(cls.Values[name])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
clean_name = cls.clean_name_str(name)
|
clean = cls.clean_name_str(name)
|
||||||
for k, v in cls.value_map.items():
|
for member in cls.Values:
|
||||||
if clean_name == cls.clean_name_str(k):
|
if cls.clean_name_str(member.name) == clean:
|
||||||
return v
|
return int(member)
|
||||||
|
|
||||||
if strict:
|
if strict:
|
||||||
raise ValueError(f"Problem in {cls.get_name()}: {name!r} is not a known value."
|
valid = ', '.join(m.name for m in cls.Values)
|
||||||
f" Known values are: {cls.value_map.keys()!r}")
|
raise ValueError(f"{cls.get_name()}: {name!r} is not a known value. Known values are: {valid}")
|
||||||
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:
|
||||||
if cls._value_map_reverse is None:
|
"""Return the enum member name for a given integer value."""
|
||||||
cls._value_map_reverse = dict((v, k) for k, v in cls.value_map.items())
|
try:
|
||||||
|
return cls.Values(val).name
|
||||||
name = cls._value_map_reverse.get(val)
|
except ValueError:
|
||||||
if name:
|
if strict:
|
||||||
return name
|
raise ValueError(f"{cls.get_name()}: {val!r} ({type(val).__name__}) is not a known value.")
|
||||||
if strict:
|
return None
|
||||||
raise ValueError(f"Problem in {cls.get_name()}: {val!r} ({type(val)}) is not a known value."
|
|
||||||
f" Known values are: {cls.value_map.values()!r}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def name_normalize(cls, name:str) -> str:
|
def name_normalize(cls, name: str) -> str:
|
||||||
return cls.map_val_to_name(cls.map_name_to_val(name))
|
"""Map a (possibly fuzzy) name to its canonical enum member name."""
|
||||||
|
return cls.Values(cls.map_name_to_val(name)).name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean_name_str(cls, val):
|
def clean_name_str(cls, val: str) -> str:
|
||||||
return re.sub('[^0-9A-Za-z-_]', '', val).lower()
|
"""Strip punctuation and case for fuzzy name comparison.
|
||||||
|
Treats hyphens and underscores as equivalent (both removed)."""
|
||||||
|
return re.sub('[^0-9A-Za-z]', '', val).lower()
|
||||||
|
|
||||||
|
|
||||||
class Iccid(DecimalParam):
|
class Iccid(DecimalParam):
|
||||||
@@ -632,24 +618,36 @@ class SmspTpScAddr(ConfigurableParameter):
|
|||||||
ef_smsp_dec['tp_sc_addr']['ton_npi']['type_of_number'] = 'international' if international else 'unknown'
|
ef_smsp_dec['tp_sc_addr']['ton_npi']['type_of_number'] = 'international' if international else 'unknown'
|
||||||
# ensure the parameter_indicators.tp_sc_addr is True
|
# ensure the parameter_indicators.tp_sc_addr is True
|
||||||
ef_smsp_dec['parameter_indicators']['tp_sc_addr'] = True
|
ef_smsp_dec['parameter_indicators']['tp_sc_addr'] = True
|
||||||
# re-encode into the File body
|
|
||||||
f_smsp.body = ef_smsp.encode_record_bin(ef_smsp_dec, 1)
|
# alpha_id padding: to make room for a human readable SMSC name that can be provisioned to the profile later
|
||||||
|
# on, alpha_id needs to be empty but padded 0xff to some length.
|
||||||
|
# - alpha_id is optional, setting alpha_id = '' ensures the IE is present.
|
||||||
|
# - the length of the file is 28+Y where Y is the length of the alpha_id -- here the intended length of our padding
|
||||||
|
# (see 3GPP TS 31.102 4.2.27 EF.SMSP). So if we want a maximum length of alpha_id = 14, we set the total
|
||||||
|
# file size to 28+14 = 42.
|
||||||
|
# - this file size has to go in two places: encode_record_bin() needs to know the length to encode the right
|
||||||
|
# length of fillFileContent.
|
||||||
|
# - the f_smsp needs to show the right file size in the PES, as in
|
||||||
|
# 'ef-smsp': [('fileDescriptor', {'efFileSize': '2a', ...
|
||||||
|
# (where 2a == 42)
|
||||||
|
# - To generate the right amount of fillFileContent, pass total_len=42 to encode_record_bin().
|
||||||
|
# - To show the right size in the PES, set f_smsp.rec_len = 42
|
||||||
|
ef_smsp_dec['alpha_id'] = ''
|
||||||
|
f_smsp.rec_len = 42
|
||||||
|
|
||||||
|
# re-encode into the File body.
|
||||||
|
#
|
||||||
#print("SMSP (new): %s" % f_smsp.body)
|
#print("SMSP (new): %s" % f_smsp.body)
|
||||||
# re-generate the pe.decoded member from the File instance
|
# re-generate the pe.decoded member from the File instance
|
||||||
|
f_smsp.body = ef_smsp.encode_record_bin(ef_smsp_dec, 1, total_len=f_smsp.rec_len)
|
||||||
pe.file2pe(f_smsp)
|
pe.file2pe(f_smsp)
|
||||||
|
|
||||||
@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.get('ef-smsp', None)
|
f_smsp = pe.files['ef-smsp']
|
||||||
if f_smsp is None:
|
ef_smsp = EF_SMSP()
|
||||||
continue
|
ef_smsp_dec = ef_smsp.decode_record_bin(f_smsp.body, 1)
|
||||||
|
|
||||||
try:
|
|
||||||
ef_smsp = EF_SMSP()
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -668,7 +666,6 @@ class SdKey(BinaryParam):
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -714,8 +711,6 @@ 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)
|
||||||
@@ -955,7 +950,7 @@ class Pin(DecimalHexParam):
|
|||||||
|
|
||||||
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
for pinCode in pinCodes.decoded['pinCodes'][1]:
|
||||||
if pinCode['keyReference'] == cls.keyReference:
|
if pinCode['keyReference'] == cls.keyReference:
|
||||||
yield { cls.name: cls.decimal_hex_to_str(pinCode['pinValue']) }
|
yield { cls.name: cls.decimal_hex_to_str(pinCode['pinValue']) }
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
||||||
@@ -1033,22 +1028,20 @@ 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
|
||||||
|
|
||||||
algo_config_key = 'algorithmID'
|
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
|
||||||
|
class Values(enum.IntEnum):
|
||||||
|
Milenage = 1
|
||||||
|
TUAK = 2
|
||||||
|
usim_test = 3 # input 'usim-test' also accepted via fuzzy matching
|
||||||
|
|
||||||
# EnumParam.validate_val() returns the int values from value_map
|
# EnumParam.validate_val() returns the int values from Values
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
def get_values_from_pes(cls, pes: ProfileElementSequence):
|
||||||
|
|||||||
+41
-3
@@ -226,9 +226,28 @@ 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]): # FIXME: more IEs
|
ProfileClass, ProfilePolicyRules, NotificationConfigurationInfo,
|
||||||
|
ProfileOwner, SMDPPProprietaryData]):
|
||||||
pass
|
pass
|
||||||
class ProfileInfoSeq(BER_TLV_IE, tag=0xa0, nested=[ProfileInfo]):
|
class ProfileInfoSeq(BER_TLV_IE, tag=0xa0, nested=[ProfileInfo]):
|
||||||
pass
|
pass
|
||||||
@@ -444,9 +463,28 @@ 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']))
|
||||||
|
|
||||||
def do_get_profiles_info(self, _opts):
|
get_profiles_info_parser = argparse.ArgumentParser()
|
||||||
|
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."""
|
||||||
pi = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, ProfileInfoListReq(), ProfileInfoListResp)
|
if opts.all:
|
||||||
|
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']))
|
||||||
|
|
||||||
|
|||||||
+101
-20
@@ -30,6 +30,7 @@ import tempfile
|
|||||||
import json
|
import json
|
||||||
import abc
|
import abc
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
|
|
||||||
import cmd2
|
import cmd2
|
||||||
from cmd2 import CommandSet, with_default_category
|
from cmd2 import CommandSet, with_default_category
|
||||||
@@ -43,6 +44,7 @@ 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
|
||||||
@@ -51,6 +53,8 @@ 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.
|
||||||
@@ -552,6 +556,85 @@ class CardADF(CardDF):
|
|||||||
return lchan.selected_file.application.export(as_json, lchan)
|
return lchan.selected_file.application.export(as_json, lchan)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonEditor:
|
||||||
|
"""Context manager for editing a JSON-encoded EF value in an external editor.
|
||||||
|
|
||||||
|
Writes the current JSON value (plus encode/decode examples as //-comments)
|
||||||
|
to a temporary file, opens the user's editor, then reads the result back
|
||||||
|
(stripping comment lines) and returns it as the context variable::
|
||||||
|
|
||||||
|
with JsonEditor(self._cmd, orig_json, ef) as edited_json:
|
||||||
|
if edited_json != orig_json:
|
||||||
|
...write back...
|
||||||
|
"""
|
||||||
|
def __init__(self, cmd, orig_json, ef):
|
||||||
|
self._cmd = cmd
|
||||||
|
self._orig_json = orig_json
|
||||||
|
self._ef = ef
|
||||||
|
self._file = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _strip_comments(text: str) -> str:
|
||||||
|
"""Strip //-comment lines from text before JSON parsing."""
|
||||||
|
# TODO: also strip inline comments?
|
||||||
|
return '\n'.join(line for line in text.splitlines() if not line.lstrip().startswith('//'))
|
||||||
|
|
||||||
|
def _append_examples_as_comments(self, text_file) -> None:
|
||||||
|
"""Append encode/decode test vectors as //-comment lines to an open file.
|
||||||
|
The examples are taken from _test_de_encode and _test_decode class
|
||||||
|
attributes (same source as the auto-generated filesystem documentation).
|
||||||
|
The comment block is intentionally ignored on read-back by _strip_comments."""
|
||||||
|
vectors = []
|
||||||
|
for attr in ('_test_de_encode', '_test_decode'):
|
||||||
|
v = getattr(type(self._ef), attr, None)
|
||||||
|
if v:
|
||||||
|
vectors.extend(v)
|
||||||
|
if not vectors:
|
||||||
|
return
|
||||||
|
ef = self._ef
|
||||||
|
parts = [ef.fully_qualified_path_str()]
|
||||||
|
if ef.fid:
|
||||||
|
parts.append(f'({ef.fid.upper()})')
|
||||||
|
if ef.desc:
|
||||||
|
parts.append(f'- {ef.desc}')
|
||||||
|
text_file.write(f'\n\n// {" ".join(parts)}\n')
|
||||||
|
text_file.write('// Examples (ignored on save):\n')
|
||||||
|
for t in vectors:
|
||||||
|
if len(t) >= 3:
|
||||||
|
encoded, record_nr, decoded = t[0], t[1], t[2]
|
||||||
|
text_file.write(f'// record {record_nr}: {encoded}\n')
|
||||||
|
else:
|
||||||
|
encoded, decoded = t[0], t[1]
|
||||||
|
text_file.write(f'// file: {encoded}\n')
|
||||||
|
for line in json.dumps(decoded, indent=4, cls=JsonEncoder).splitlines():
|
||||||
|
text_file.write(f'// {line}\n')
|
||||||
|
|
||||||
|
def __enter__(self) -> object:
|
||||||
|
"""Write JSON + examples to a temp file, run the editor, return parsed result.
|
||||||
|
|
||||||
|
On JSONDecodeError the user is offered the option to re-open the file
|
||||||
|
and fix the mistake interactively. The temp file is removed by __exit__()
|
||||||
|
on success, or when the user declines to retry."""
|
||||||
|
self._file = tempfile.NamedTemporaryFile(prefix='pysim_', suffix='.json',
|
||||||
|
mode='w', delete=False)
|
||||||
|
json.dump(self._orig_json, self._file, indent=4, cls=JsonEncoder)
|
||||||
|
self._append_examples_as_comments(self._file)
|
||||||
|
self._file.close()
|
||||||
|
while True:
|
||||||
|
self._cmd.run_editor(self._file.name)
|
||||||
|
try:
|
||||||
|
with open(self._file.name, 'r') as f:
|
||||||
|
return json.loads(self._strip_comments(f.read()))
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self._cmd.perror(f'Invalid JSON: {e}')
|
||||||
|
answer = self._cmd.read_input('Re-open file for editing? [y]es/[n]o: ')
|
||||||
|
if answer not in ('y', 'yes'):
|
||||||
|
return self._orig_json
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
os.unlink(self._file.name)
|
||||||
|
|
||||||
|
|
||||||
class CardEF(CardFile):
|
class CardEF(CardFile):
|
||||||
"""EF (Entry File) in the smart card filesystem"""
|
"""EF (Entry File) in the smart card filesystem"""
|
||||||
|
|
||||||
@@ -657,15 +740,8 @@ class TransparentEF(CardEF):
|
|||||||
def do_edit_binary_decoded(self, _opts):
|
def do_edit_binary_decoded(self, _opts):
|
||||||
"""Edit the JSON representation of the EF contents in an editor."""
|
"""Edit the JSON representation of the EF contents in an editor."""
|
||||||
(orig_json, _sw) = self._cmd.lchan.read_binary_dec()
|
(orig_json, _sw) = self._cmd.lchan.read_binary_dec()
|
||||||
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
|
ef = self._cmd.lchan.selected_file
|
||||||
filename = '%s/file' % dirname
|
with JsonEditor(self._cmd, orig_json, ef) as edited_json:
|
||||||
# write existing data as JSON to file
|
|
||||||
with open(filename, 'w') as text_file:
|
|
||||||
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
|
|
||||||
# run a text editor
|
|
||||||
self._cmd.run_editor(filename)
|
|
||||||
with open(filename, 'r') as text_file:
|
|
||||||
edited_json = json.load(text_file)
|
|
||||||
if edited_json == orig_json:
|
if edited_json == orig_json:
|
||||||
self._cmd.poutput("Data not modified, skipping write")
|
self._cmd.poutput("Data not modified, skipping write")
|
||||||
else:
|
else:
|
||||||
@@ -787,6 +863,8 @@ class TransparentEF(CardEF):
|
|||||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||||
t.from_dict(abstract_data)
|
t.from_dict(abstract_data)
|
||||||
return t.to_tlv()
|
return t.to_tlv()
|
||||||
|
if 'raw' in abstract_data:
|
||||||
|
return h2b(abstract_data['raw'])
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"%s encoder not yet implemented. Patches welcome." % self)
|
"%s encoder not yet implemented. Patches welcome." % self)
|
||||||
|
|
||||||
@@ -816,6 +894,8 @@ class TransparentEF(CardEF):
|
|||||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||||
t.from_dict(abstract_data)
|
t.from_dict(abstract_data)
|
||||||
return b2h(t.to_tlv())
|
return b2h(t.to_tlv())
|
||||||
|
if 'raw' in abstract_data:
|
||||||
|
return abstract_data['raw']
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"%s encoder not yet implemented. Patches welcome." % self)
|
"%s encoder not yet implemented. Patches welcome." % self)
|
||||||
|
|
||||||
@@ -959,15 +1039,8 @@ class LinFixedEF(CardEF):
|
|||||||
def do_edit_record_decoded(self, opts):
|
def do_edit_record_decoded(self, opts):
|
||||||
"""Edit the JSON representation of one record in an editor."""
|
"""Edit the JSON representation of one record in an editor."""
|
||||||
(orig_json, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
(orig_json, _sw) = self._cmd.lchan.read_record_dec(opts.RECORD_NR)
|
||||||
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
|
ef = self._cmd.lchan.selected_file
|
||||||
filename = '%s/file' % dirname
|
with JsonEditor(self._cmd, orig_json, ef) as edited_json:
|
||||||
# write existing data as JSON to file
|
|
||||||
with open(filename, 'w') as text_file:
|
|
||||||
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
|
|
||||||
# run a text editor
|
|
||||||
self._cmd.run_editor(filename)
|
|
||||||
with open(filename, 'r') as text_file:
|
|
||||||
edited_json = json.load(text_file)
|
|
||||||
if edited_json == orig_json:
|
if edited_json == orig_json:
|
||||||
self._cmd.poutput("Data not modified, skipping write")
|
self._cmd.poutput("Data not modified, skipping write")
|
||||||
else:
|
else:
|
||||||
@@ -1097,6 +1170,8 @@ class LinFixedEF(CardEF):
|
|||||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||||
t.from_dict(abstract_data)
|
t.from_dict(abstract_data)
|
||||||
return b2h(t.to_tlv())
|
return b2h(t.to_tlv())
|
||||||
|
if 'raw' in abstract_data:
|
||||||
|
return abstract_data['raw']
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"%s encoder not yet implemented. Patches welcome." % self)
|
"%s encoder not yet implemented. Patches welcome." % self)
|
||||||
|
|
||||||
@@ -1126,6 +1201,8 @@ class LinFixedEF(CardEF):
|
|||||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||||
t.from_dict(abstract_data)
|
t.from_dict(abstract_data)
|
||||||
return t.to_tlv()
|
return t.to_tlv()
|
||||||
|
if 'raw' in abstract_data:
|
||||||
|
return h2b(abstract_data['raw'])
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"%s encoder not yet implemented. Patches welcome." % self)
|
"%s encoder not yet implemented. Patches welcome." % self)
|
||||||
|
|
||||||
@@ -1317,6 +1394,8 @@ class TransRecEF(TransparentEF):
|
|||||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||||
t.from_dict(abstract_data)
|
t.from_dict(abstract_data)
|
||||||
return b2h(t.to_tlv())
|
return b2h(t.to_tlv())
|
||||||
|
if 'raw' in abstract_data:
|
||||||
|
return abstract_data['raw']
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"%s encoder not yet implemented. Patches welcome." % self)
|
"%s encoder not yet implemented. Patches welcome." % self)
|
||||||
|
|
||||||
@@ -1346,6 +1425,8 @@ class TransRecEF(TransparentEF):
|
|||||||
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
t = self._tlv() if inspect.isclass(self._tlv) else self._tlv
|
||||||
t.from_dict(abstract_data)
|
t.from_dict(abstract_data)
|
||||||
return t.to_tlv()
|
return t.to_tlv()
|
||||||
|
if 'raw' in abstract_data:
|
||||||
|
return h2b(abstract_data['raw'])
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"%s encoder not yet implemented. Patches welcome." % self)
|
"%s encoder not yet implemented. Patches welcome." % self)
|
||||||
|
|
||||||
@@ -1543,14 +1624,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:
|
||||||
print("Detected CardModel:", cls.__name__)
|
log.info("Detected CardModel: %s", 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:
|
||||||
print("Detected CardModel:", cls.__name__)
|
log.info("Detected CardModel: %s", cls.__name__)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class ListOfSupportedOptions(BER_TLV_IE, tag=0x81):
|
|||||||
class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
|
class SupportedKeysForScp03(BER_TLV_IE, tag=0x82):
|
||||||
_construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
|
_construct = FlagsEnum(Byte, aes128=0x01, aes192=0x02, aes256=0x04)
|
||||||
class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
|
class SupportedTlsCipherSuitesForScp81(BER_TLV_IE, tag=0x83):
|
||||||
_consuruct = GreedyRange(Int16ub)
|
_construct = GreedyRange(Int16ub)
|
||||||
class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
|
class ScpInformation(BER_TLV_IE, tag=0xa0, nested=[ScpType, ListOfSupportedOptions, SupportedKeysForScp03,
|
||||||
SupportedTlsCipherSuitesForScp81]):
|
SupportedTlsCipherSuitesForScp81]):
|
||||||
pass
|
pass
|
||||||
@@ -319,7 +319,7 @@ class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
|
|||||||
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
|
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
|
||||||
class ApplicationAID(BER_TLV_IE, tag=0x4f):
|
class ApplicationAID(BER_TLV_IE, tag=0x4f):
|
||||||
_construct = GreedyBytes
|
_construct = GreedyBytes
|
||||||
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
|
class ApplicationTemplate(BER_TLV_IE, tag=0x61, nested=[ApplicationAID]):
|
||||||
pass
|
pass
|
||||||
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
|
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
|
||||||
pass
|
pass
|
||||||
@@ -562,14 +562,14 @@ class ADF_SD(CardADF):
|
|||||||
|
|
||||||
@cmd2.with_argparser(store_data_parser)
|
@cmd2.with_argparser(store_data_parser)
|
||||||
def do_store_data(self, opts):
|
def do_store_data(self, opts):
|
||||||
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
|
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
|
||||||
See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
|
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
||||||
response_permitted = opts.response == 'may_be_returned'
|
response_permitted = opts.response == 'may_be_returned'
|
||||||
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
|
self.store_data(h2b(opts.DATA), opts.data_structure, opts.encryption, response_permitted)
|
||||||
|
|
||||||
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
|
def store_data(self, data: bytes, structure:str = 'none', encryption:str = 'none', response_permitted: bool = False) -> bytes:
|
||||||
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
|
"""Perform the GlobalPlatform STORE DATA command in order to store some card-specific data.
|
||||||
See GlobalPlatform CardSpecification v2.3Section 11.11 for details."""
|
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
||||||
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
|
max_cmd_len = self._cmd.lchan.scc.max_cmd_len
|
||||||
# Table 11-89 of GP Card Specification v2.3
|
# Table 11-89 of GP Card Specification v2.3
|
||||||
remainder = data
|
remainder = data
|
||||||
@@ -585,7 +585,7 @@ class ADF_SD(CardADF):
|
|||||||
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
|
data, _sw = self._cmd.lchan.scc.send_apdu_checksw(hdr + b2h(chunk) + "00")
|
||||||
block_nr += 1
|
block_nr += 1
|
||||||
response += data
|
response += data
|
||||||
return data
|
return h2b(response)
|
||||||
|
|
||||||
put_key_parser = argparse.ArgumentParser()
|
put_key_parser = argparse.ArgumentParser()
|
||||||
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
|
put_key_parser.add_argument('--old-key-version-nr', type=auto_uint8, default=0, help='Old Key Version Number')
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from osmocom.construct import *
|
from osmocom.construct import *
|
||||||
from osmocom.utils import *
|
from osmocom.utils import *
|
||||||
from osmocom.tlv import *
|
from osmocom.tlv import *
|
||||||
@@ -46,7 +48,9 @@ class InstallParams(TLV_IE_Collection, nested=[AppSpecificParams, SystemSpecific
|
|||||||
# GPD_SPE_013, table 11-49
|
# GPD_SPE_013, table 11-49
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def gen_install_parameters(non_volatile_memory_quota:int, volatile_memory_quota:int, stk_parameter:str):
|
def gen_install_parameters(non_volatile_memory_quota: Optional[int] = None,
|
||||||
|
volatile_memory_quota: Optional[int] = None,
|
||||||
|
stk_parameter: Optional[str] = None):
|
||||||
|
|
||||||
# GPD_SPE_013, table 11-49
|
# GPD_SPE_013, table 11-49
|
||||||
|
|
||||||
@@ -54,19 +58,17 @@ def gen_install_parameters(non_volatile_memory_quota:int, volatile_memory_quota:
|
|||||||
install_params = InstallParams()
|
install_params = InstallParams()
|
||||||
install_params_dict = [{'app_specific_params': None}]
|
install_params_dict = [{'app_specific_params': None}]
|
||||||
|
|
||||||
#Conditional
|
# Collect system specific parameters (optional)
|
||||||
if non_volatile_memory_quota and volatile_memory_quota and stk_parameter:
|
system_specific_params = []
|
||||||
system_specific_params = []
|
if non_volatile_memory_quota is not None:
|
||||||
#Optional
|
system_specific_params.append({'non_volatile_memory_quota': non_volatile_memory_quota})
|
||||||
if non_volatile_memory_quota:
|
if volatile_memory_quota is not None:
|
||||||
system_specific_params += [{'non_volatile_memory_quota': non_volatile_memory_quota}]
|
system_specific_params.append({'volatile_memory_quota': volatile_memory_quota})
|
||||||
#Optional
|
if stk_parameter is not None:
|
||||||
if volatile_memory_quota:
|
system_specific_params.append({'stk_parameter': stk_parameter})
|
||||||
system_specific_params += [{'volatile_memory_quota': volatile_memory_quota}]
|
# Add system specific parameters to the install parameters, if any
|
||||||
#Optional
|
if system_specific_params:
|
||||||
if stk_parameter:
|
install_params_dict.append({'system_specific_params': system_specific_params})
|
||||||
system_specific_params += [{'stk_parameter': stk_parameter}]
|
|
||||||
install_params_dict += [{'system_specific_params': system_specific_params}]
|
|
||||||
|
|
||||||
install_params.from_dict(install_params_dict)
|
install_params.from_dict(install_params_dict)
|
||||||
return b2h(install_params.to_bytes())
|
return b2h(install_params.to_bytes())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
log = PySimLogger.get(__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)
|
||||||
logger.debug("mac_1des(%s,icv=%s) -> %s", b2h(data), b2h(icv), b2h(h))
|
log.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)])))
|
||||||
logger.debug("mac_3des(%s) -> %s", b2h(data), b2h(h))
|
log.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):
|
||||||
@@ -266,18 +266,20 @@ class SCP02(SCP):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
def dek_encrypt(self, plaintext:bytes) -> bytes:
|
||||||
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB)
|
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
||||||
|
cipher = DES3.new(self.sk.data_enc, DES.MODE_ECB)
|
||||||
return cipher.encrypt(plaintext)
|
return cipher.encrypt(plaintext)
|
||||||
|
|
||||||
def dek_decrypt(self, ciphertext:bytes) -> bytes:
|
def dek_decrypt(self, ciphertext:bytes) -> bytes:
|
||||||
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB)
|
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
||||||
|
cipher = DES3.new(self.sk.data_enc, DES.MODE_ECB)
|
||||||
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):
|
||||||
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(host_challenge), b2h(card_challenge))
|
log.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)
|
||||||
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
|
log.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."""
|
||||||
@@ -289,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)
|
||||||
logger.debug(self.sk)
|
log.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")
|
||||||
@@ -309,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."""
|
||||||
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
log.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
||||||
|
|
||||||
if not self.do_cmac:
|
if not self.do_cmac:
|
||||||
return apdu
|
return apdu
|
||||||
@@ -376,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
|
||||||
|
|
||||||
logger.debug("scp03_kdf(constant=%s, context=%s, base_key=%s, l=%u)", b2h(constant), b2h(context), b2h(base_key), l)
|
log.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 :(
|
||||||
@@ -436,7 +438,7 @@ class Scp03SessionKeys:
|
|||||||
"""Obtain the ICV value computed as described in 6.2.6.
|
"""Obtain the ICV value computed as described in 6.2.6.
|
||||||
This method has two modes:
|
This method has two modes:
|
||||||
* is_response=False for computing the ICV for C-ENC. Will pre-increment the counter.
|
* is_response=False for computing the ICV for C-ENC. Will pre-increment the counter.
|
||||||
* is_response=False for computing the ICV for R-DEC."""
|
* is_response=True for computing the ICV for R-DEC."""
|
||||||
if not is_response:
|
if not is_response:
|
||||||
self.block_nr += 1
|
self.block_nr += 1
|
||||||
# The binary value of this number SHALL be left padded with zeroes to form a full block.
|
# The binary value of this number SHALL be left padded with zeroes to form a full block.
|
||||||
@@ -449,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)
|
||||||
logger.debug("_get_icv(data=%s, is_resp=%s) -> icv=%s", b2h(data), is_response, b2h(icv))
|
log.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
|
||||||
@@ -487,12 +489,12 @@ class SCP03(SCP):
|
|||||||
return cipher.decrypt(ciphertext)
|
return cipher.decrypt(ciphertext)
|
||||||
|
|
||||||
def _compute_cryptograms(self):
|
def _compute_cryptograms(self):
|
||||||
logger.debug("host_challenge(%s), card_challenge(%s)", b2h(self.host_challenge), b2h(self.card_challenge))
|
log.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)
|
||||||
logger.debug("host_cryptogram(%s), card_cryptogram(%s)", b2h(self.host_cryptogram), b2h(self.card_cryptogram))
|
log.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."""
|
||||||
@@ -512,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)
|
||||||
logger.debug(self.sk)
|
log.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']:
|
||||||
@@ -527,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."""
|
||||||
logger.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
log.debug("wrap_cmd_apdu(%s)", b2h(apdu))
|
||||||
|
|
||||||
if not self.do_cmac:
|
if not self.do_cmac:
|
||||||
return apdu
|
return apdu
|
||||||
@@ -582,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.
|
||||||
logger.debug("unwrap_rsp_apdu(sw=%s, rsp_apdu=%s)", sw, rsp_apdu)
|
log.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
|
||||||
@@ -598,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)
|
||||||
logger.debug("decrypted: %s", b2h(decrypted))
|
log.debug("decrypted: %s", b2h(decrypted))
|
||||||
# remove padding
|
# remove padding
|
||||||
response_data = unpad80(decrypted)
|
response_data = unpad80(decrypted)
|
||||||
logger.debug("response_data: %s", b2h(response_data))
|
log.debug("response_data: %s", b2h(response_data))
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ class SimCard(SimCardBase):
|
|||||||
return sw
|
return sw
|
||||||
|
|
||||||
def update_smsp(self, smsp):
|
def update_smsp(self, smsp):
|
||||||
data, sw = self._scc.update_record(EF['SMSP'], 1, rpad(smsp, 84))
|
print("using update_smsp")
|
||||||
|
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']):
|
||||||
|
|||||||
+22
-4
@@ -24,7 +24,15 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from cmd2 import style
|
import cmd2
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
if version.parse(cmd2.__version__) >= version.parse("3.0.0"):
|
||||||
|
from cmd2 import stylize as _stylize # pylint: disable=no-name-in-module
|
||||||
|
def _style(text, fg=None): # pylint: disable=function-redefined
|
||||||
|
return _stylize(text, fg) if fg else text
|
||||||
|
else: # cmd2>=2.6.2
|
||||||
|
from cmd2 import style as _style # pylint: disable=no-name-in-module
|
||||||
|
|
||||||
class _PySimLogHandler(logging.Handler):
|
class _PySimLogHandler(logging.Handler):
|
||||||
def __init__(self, log_callback):
|
def __init__(self, log_callback):
|
||||||
@@ -44,7 +52,7 @@ class PySimLogger:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
LOG_FMTSTR = "%(levelname)s: %(message)s"
|
LOG_FMTSTR = "%(levelname)s: %(message)s"
|
||||||
LOG_FMTSTR_VERBOSE = "%(module)s.%(lineno)d -- " + LOG_FMTSTR
|
LOG_FMTSTR_VERBOSE = "%(name)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)
|
||||||
|
|
||||||
@@ -63,7 +71,7 @@ class PySimLogger:
|
|||||||
raise RuntimeError('static class, do not instantiate')
|
raise RuntimeError('static class, do not instantiate')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup(print_callback = None, colors:dict = {}):
|
def setup(print_callback = None, colors:dict = {}, verbose_debug:bool = False):
|
||||||
"""
|
"""
|
||||||
Set a print callback function and color scheme. This function call is optional. In case this method is not
|
Set a print callback function and color scheme. This function call is optional. In case this method is not
|
||||||
called, default settings apply.
|
called, default settings apply.
|
||||||
@@ -72,10 +80,20 @@ class PySimLogger:
|
|||||||
have the following format: print_callback(message:str)
|
have the following format: print_callback(message:str)
|
||||||
colors : An optional dict through which certain log levels can be assigned a color.
|
colors : An optional dict through which certain log levels can be assigned a color.
|
||||||
(e.g. {logging.WARN: YELLOW})
|
(e.g. {logging.WARN: YELLOW})
|
||||||
|
verbose_debug: Enable verbose logging and set the loglevel DEBUG when set to true. Otherwise the
|
||||||
|
non-verbose logging is used and the loglevel is set to INFO. This setting can be changed
|
||||||
|
using the set_verbose and set_level methods at any time.
|
||||||
"""
|
"""
|
||||||
PySimLogger.print_callback = print_callback
|
PySimLogger.print_callback = print_callback
|
||||||
PySimLogger.colors = colors
|
PySimLogger.colors = colors
|
||||||
|
|
||||||
|
if (verbose_debug):
|
||||||
|
PySimLogger.set_verbose(True)
|
||||||
|
PySimLogger.set_level(logging.DEBUG)
|
||||||
|
else:
|
||||||
|
PySimLogger.set_verbose(False)
|
||||||
|
PySimLogger.set_level(logging.INFO)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_verbose(verbose:bool = False):
|
def set_verbose(verbose:bool = False):
|
||||||
"""
|
"""
|
||||||
@@ -111,7 +129,7 @@ class PySimLogger:
|
|||||||
if isinstance(color, str):
|
if isinstance(color, str):
|
||||||
PySimLogger.print_callback(color + formatted_message + "\033[0m")
|
PySimLogger.print_callback(color + formatted_message + "\033[0m")
|
||||||
else:
|
else:
|
||||||
PySimLogger.print_callback(style(formatted_message, fg = color))
|
PySimLogger.print_callback(_style(formatted_message, fg = color))
|
||||||
else:
|
else:
|
||||||
PySimLogger.print_callback(formatted_message)
|
PySimLogger.print_callback(formatted_message)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -221,12 +221,12 @@ class OtaAlgoCrypt(OtaAlgo, abc.ABC):
|
|||||||
for subc in cls.__subclasses__():
|
for subc in cls.__subclasses__():
|
||||||
if subc.enum_name == otak.algo_crypt:
|
if subc.enum_name == otak.algo_crypt:
|
||||||
return subc(otak)
|
return subc(otak)
|
||||||
raise ValueError('No implementation for crypt algorithm %s' % otak.algo_auth)
|
raise ValueError('No implementation for crypt algorithm %s' % otak.algo_crypt)
|
||||||
|
|
||||||
class OtaAlgoAuth(OtaAlgo, abc.ABC):
|
class OtaAlgoAuth(OtaAlgo, abc.ABC):
|
||||||
def __init__(self, otak: OtaKeyset):
|
def __init__(self, otak: OtaKeyset):
|
||||||
if self.enum_name != otak.algo_auth:
|
if self.enum_name != otak.algo_auth:
|
||||||
raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_crypt))
|
raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_auth))
|
||||||
super().__init__(otak)
|
super().__init__(otak)
|
||||||
|
|
||||||
def sign(self, data:bytes) -> bytes:
|
def sign(self, data:bytes) -> bytes:
|
||||||
|
|||||||
+8
-2
@@ -169,8 +169,14 @@ class SMS_TPDU(abc.ABC):
|
|||||||
|
|
||||||
class SMS_DELIVER(SMS_TPDU):
|
class SMS_DELIVER(SMS_TPDU):
|
||||||
"""Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction."""
|
"""Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction."""
|
||||||
flags_construct = BitStruct('tp_rp'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag, 'tp_sri'/Flag,
|
flags_construct = BitStruct('tp_rp'/Flag,
|
||||||
Padding(1), 'tp_mms'/Flag, 'tp_mti'/BitsInteger(2))
|
'tp_udhi'/Flag,
|
||||||
|
'tp_sri'/Flag,
|
||||||
|
Padding(1),
|
||||||
|
'tp_lp'/Flag,
|
||||||
|
'tp_mms'/Flag,
|
||||||
|
'tp_mti'/BitsInteger(2))
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs['tp_mti'] = 0
|
kwargs['tp_mti'] = 0
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|||||||
+47
-17
@@ -90,7 +90,7 @@ class LinkBase(abc.ABC):
|
|||||||
self.sw_interpreter = sw_interpreter
|
self.sw_interpreter = sw_interpreter
|
||||||
self.apdu_tracer = apdu_tracer
|
self.apdu_tracer = apdu_tracer
|
||||||
self.proactive_handler = proactive_handler
|
self.proactive_handler = proactive_handler
|
||||||
self.apdu_strict = False
|
self.apdu_strict = True
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -301,24 +301,54 @@ 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")
|
||||||
|
|
||||||
# When we have sent the first APDU, the SW may indicate that there are response bytes
|
# After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further
|
||||||
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
|
# TPDUs have to be sent in order to complete the task.
|
||||||
# xx is the number of response bytes available.
|
if case == 4 or self.apdu_strict == False:
|
||||||
# See also:
|
# In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data
|
||||||
if sw is not None:
|
# available which has to be retrieved using a GET RESPONSE command TPDU.
|
||||||
while (sw[0:2] in ['9f', '61', '62', '63']):
|
#
|
||||||
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
|
# ETSI TS 102 221, section 7.3.1.1.4 is very cleare about the fact that the GET RESPONSE mechanism
|
||||||
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
|
# shall only apply on case #4 APDUs but unfortunately it is impossible to distinguish between case #3
|
||||||
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
|
# and case #4 when the APDU format is not strictly followed. In order to be able to detect case #4
|
||||||
tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
|
# correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with
|
||||||
|
# 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
|
||||||
d, sw = self.send_tpdu(tpdu_gr)
|
data_gr, sw = self.send_tpdu(tpdu_gr)
|
||||||
data += d
|
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
|
||||||
if sw[0:2] == '6c':
|
data += data_gr
|
||||||
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
if sw[0:2] == '6c':
|
||||||
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
|
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
||||||
data, sw = self.send_tpdu(tpdu_gr)
|
tpdu_gr = prev_tpdu[0:8] + sw[2:4]
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
+12
-9
@@ -26,6 +26,7 @@ from smartcard.CardRequest import CardRequest
|
|||||||
from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException
|
from smartcard.Exceptions import NoCardException, CardRequestTimeoutException, CardConnectionException
|
||||||
from smartcard.System import readers
|
from smartcard.System import readers
|
||||||
from smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnection
|
from smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnection
|
||||||
|
from smartcard.ATR import ATR
|
||||||
|
|
||||||
from osmocom.utils import h2i, i2h, Hexstr
|
from osmocom.utils import h2i, i2h, Hexstr
|
||||||
|
|
||||||
@@ -80,23 +81,25 @@ class PcscSimLink(LinkBaseTpdu):
|
|||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
try:
|
try:
|
||||||
# To avoid leakage of resources, make sure the reader
|
# To avoid leakage of resources, make sure the reader is disconnected
|
||||||
# is disconnected
|
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
# Make card connection and select a suitable communication protocol
|
# Make card connection and select a suitable communication protocol
|
||||||
|
# (Even though pyscard provides an automatic protocol selection, we will make an independent decision
|
||||||
|
# based on the ATR. There are two reasons for that:
|
||||||
|
# 1) In case a card supports T=0 and T=1, we perfer to use T=0.
|
||||||
|
# 2) The automatic protocol selection may be unreliabe on some platforms
|
||||||
|
# see also: https://osmocom.org/issues/6952)
|
||||||
self._con.connect()
|
self._con.connect()
|
||||||
supported_protocols = self._con.getProtocol();
|
atr = ATR(self._con.getATR())
|
||||||
self.disconnect()
|
if atr.isT0Supported():
|
||||||
if (supported_protocols & CardConnection.T0_protocol):
|
self._con.setProtocol(CardConnection.T0_protocol)
|
||||||
protocol = CardConnection.T0_protocol
|
|
||||||
self.set_tpdu_format(0)
|
self.set_tpdu_format(0)
|
||||||
elif (supported_protocols & CardConnection.T1_protocol):
|
elif atr.isT1Supported():
|
||||||
protocol = CardConnection.T1_protocol
|
self._con.setProtocol(CardConnection.T1_protocol)
|
||||||
self.set_tpdu_format(1)
|
self.set_tpdu_format(1)
|
||||||
else:
|
else:
|
||||||
raise ReaderError('Unsupported card protocol')
|
raise ReaderError('Unsupported card protocol')
|
||||||
self._con.connect(protocol)
|
|
||||||
except CardConnectionException as exc:
|
except CardConnectionException as exc:
|
||||||
raise ProtocolError() from exc
|
raise ProtocolError() from exc
|
||||||
except NoCardException as exc:
|
except NoCardException as exc:
|
||||||
|
|||||||
+3
-3
@@ -1058,7 +1058,7 @@ class EF_OCSGL(LinFixedEF):
|
|||||||
# TS 31.102 Section 4.4.11.2 (Rel 15)
|
# TS 31.102 Section 4.4.11.2 (Rel 15)
|
||||||
class EF_5GS3GPPLOCI(TransparentEF):
|
class EF_5GS3GPPLOCI(TransparentEF):
|
||||||
def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20),
|
def __init__(self, fid='4f01', sfid=0x01, name='EF.5GS3GPPLOCI', size=(20, 20),
|
||||||
desc='5S 3GP location information', **kwargs):
|
desc='5GS 3GPP location information', **kwargs):
|
||||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||||
upd_status_constr = Enum(
|
upd_status_constr = Enum(
|
||||||
Byte, updated=0, not_updated=1, roaming_not_allowed=2)
|
Byte, updated=0, not_updated=1, roaming_not_allowed=2)
|
||||||
@@ -1326,7 +1326,7 @@ class EF_5G_PROSE_UIR(TransparentEF):
|
|||||||
pass
|
pass
|
||||||
class FiveGDdnmfCtfAddrForUploading(BER_TLV_IE, tag=0x97):
|
class FiveGDdnmfCtfAddrForUploading(BER_TLV_IE, tag=0x97):
|
||||||
pass
|
pass
|
||||||
class ProSeConfigDataForUeToNetworkRelayUE(BER_TLV_IE, tag=0xa0,
|
class ProSeConfigDataForUsageInfoReporting(BER_TLV_IE, tag=0xa0,
|
||||||
nested=[EF_5G_PROSE_DD.ValidityTimer,
|
nested=[EF_5G_PROSE_DD.ValidityTimer,
|
||||||
CollectionPeriod, ReportingWindow,
|
CollectionPeriod, ReportingWindow,
|
||||||
ReportingIndicators,
|
ReportingIndicators,
|
||||||
@@ -1336,7 +1336,7 @@ class EF_5G_PROSE_UIR(TransparentEF):
|
|||||||
desc='5G ProSe configuration data for usage information reporting', **kwargs):
|
desc='5G ProSe configuration data for usage information reporting', **kwargs):
|
||||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||||
# contains TLV structure despite being TransparentEF, not BER-TLV ?!?
|
# contains TLV structure despite being TransparentEF, not BER-TLV ?!?
|
||||||
self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUeToNetworkRelayUE
|
self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUsageInfoReporting
|
||||||
|
|
||||||
# TS 31.102 Section 4.4.13.8 (Rel 18)
|
# TS 31.102 Section 4.4.13.8 (Rel 18)
|
||||||
class EF_5G_PROSE_U2URU(TransparentEF):
|
class EF_5G_PROSE_U2URU(TransparentEF):
|
||||||
|
|||||||
+98
-23
@@ -251,6 +251,16 @@ 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 },
|
||||||
@@ -261,6 +271,26 @@ class EF_SMSP(LinFixedEF):
|
|||||||
"numbering_plan_id": "reserved_for_extension" },
|
"numbering_plan_id": "reserved_for_extension" },
|
||||||
"call_number": "" },
|
"call_number": "" },
|
||||||
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 1440 } ),
|
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 1440 } ),
|
||||||
|
( 'fffffffffffffffffffffffffffffffffffffffffffffffffdffffffffffffffffffffffff07919403214365f7ffffffffffffff',
|
||||||
|
{ "alpha_id": "", "parameter_indicators": { "tp_dest_addr": False, "tp_sc_addr": True,
|
||||||
|
"tp_pid": False, "tp_dcs": False, "tp_vp": False },
|
||||||
|
"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": 7, "ton_npi": { "ext": True, "type_of_number": "international",
|
||||||
|
"numbering_plan_id": "isdn_e164" },
|
||||||
|
"call_number": "49301234567" },
|
||||||
|
"tp_pid": b"\xff", "tp_dcs": b"\xff", "tp_vp_minutes": 635040 } ),
|
||||||
|
( 'fffffffffffffffffffffffffffffffffffffffffffffffffc0b919403214365f7ffffffff07919403214365f7ffffffffffffff',
|
||||||
|
{ "alpha_id": "", "parameter_indicators": { "tp_dest_addr": True, "tp_sc_addr": True,
|
||||||
|
"tp_pid": False, "tp_dcs": False, "tp_vp": False },
|
||||||
|
"tp_dest_addr": { "length": 11, "ton_npi": { "ext": True, "type_of_number": "international",
|
||||||
|
"numbering_plan_id": "isdn_e164" },
|
||||||
|
"call_number": "49301234567" },
|
||||||
|
"tp_sc_addr": { "length": 7, "ton_npi": { "ext": True, "type_of_number": "international",
|
||||||
|
"numbering_plan_id": "isdn_e164" },
|
||||||
|
"call_number": "49301234567" },
|
||||||
|
"tp_pid": b"\xff", "tp_dcs": b"\xff", "tp_vp_minutes": 635040 } ),
|
||||||
]
|
]
|
||||||
_test_no_pad = True
|
_test_no_pad = True
|
||||||
class ValidityPeriodAdapter(Adapter):
|
class ValidityPeriodAdapter(Adapter):
|
||||||
@@ -289,17 +319,30 @@ class EF_SMSP(LinFixedEF):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sc_addr_len(ctx):
|
def sc_addr_len(ctx):
|
||||||
"""Compute the length field for an address field (like TP-DestAddr or TP-ScAddr)."""
|
"""Compute the length field for an address field (see also: 3GPP TS 24.011, section 8.2.5.2)."""
|
||||||
if not hasattr(ctx, 'call_number') or len(ctx.call_number) == 0:
|
if not hasattr(ctx, 'call_number') or len(ctx.call_number) == 0:
|
||||||
return 0xff
|
return 0xff
|
||||||
else:
|
else:
|
||||||
|
# octets required for the call_number + one octet for ton_npi
|
||||||
return bytes_for_nibbles(len(ctx.call_number)) + 1
|
return bytes_for_nibbles(len(ctx.call_number)) + 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dest_addr_len(ctx):
|
||||||
|
"""Compute the length field for an address field (see also: 3GPP TS 23.040, section 9.1.2.5)."""
|
||||||
|
if not hasattr(ctx, 'call_number') or len(ctx.call_number) == 0:
|
||||||
|
return 0xff
|
||||||
|
else:
|
||||||
|
# number of call_number digits
|
||||||
|
return len(ctx.call_number)
|
||||||
|
|
||||||
def __init__(self, fid='6f42', sfid=None, name='EF.SMSP', desc='Short message service parameters', **kwargs):
|
def __init__(self, fid='6f42', sfid=None, name='EF.SMSP', desc='Short message service parameters', **kwargs):
|
||||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(28, None), **kwargs)
|
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(28, None), **kwargs)
|
||||||
ScAddr = Struct('length'/Rebuild(Int8ub, lambda ctx: EF_SMSP.sc_addr_len(ctx)),
|
ScAddr = Struct('length'/Rebuild(Int8ub, lambda ctx: EF_SMSP.sc_addr_len(ctx)),
|
||||||
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
|
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
|
||||||
self._construct = Struct('alpha_id'/COptional(GsmOrUcs2Adapter(Rpad(Bytes(this._.total_len-28)))),
|
DestAddr = Struct('length'/Rebuild(Int8ub, lambda ctx: EF_SMSP.dest_addr_len(ctx)),
|
||||||
|
'ton_npi'/TonNpi, 'call_number'/PaddedBcdAdapter(Rpad(Bytes(10))))
|
||||||
|
# (see comment below)
|
||||||
|
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,
|
||||||
@@ -307,13 +350,31 @@ class EF_SMSP(LinFixedEF):
|
|||||||
'tp_pid'/Flag,
|
'tp_pid'/Flag,
|
||||||
'tp_sc_addr'/Flag,
|
'tp_sc_addr'/Flag,
|
||||||
'tp_dest_addr'/Flag)),
|
'tp_dest_addr'/Flag)),
|
||||||
'tp_dest_addr'/ScAddr,
|
'tp_dest_addr'/DestAddr,
|
||||||
'tp_sc_addr'/ScAddr,
|
'tp_sc_addr'/ScAddr,
|
||||||
|
|
||||||
'tp_pid'/Bytes(1),
|
'tp_pid'/Bytes(1),
|
||||||
'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):
|
||||||
@@ -389,7 +450,7 @@ class DF_TELECOM(CardDF):
|
|||||||
# TS 51.011 Section 10.3.1
|
# TS 51.011 Section 10.3.1
|
||||||
class EF_LP(TransRecEF):
|
class EF_LP(TransRecEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( "24", "24"),
|
( "24", ["24"] ),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='6f05', sfid=None, name='EF.LP', size=(1, None), rec_len=1,
|
def __init__(self, fid='6f05', sfid=None, name='EF.LP', size=(1, None), rec_len=1,
|
||||||
desc='Language Preference'):
|
desc='Language Preference'):
|
||||||
@@ -446,8 +507,8 @@ class EF_IMSI(TransparentEF):
|
|||||||
# TS 51.011 Section 10.3.4
|
# TS 51.011 Section 10.3.4
|
||||||
class EF_PLMNsel(TransRecEF):
|
class EF_PLMNsel(TransRecEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( "22F860", { "mcc": "228", "mnc": "06" } ),
|
( "22F860", [{ "mcc": "228", "mnc": "06" }] ),
|
||||||
( "330420", { "mcc": "334", "mnc": "020" } ),
|
( "330420", [{ "mcc": "334", "mnc": "020" }] ),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='6f30', sfid=None, name='EF.PLMNsel', desc='PLMN selector',
|
def __init__(self, fid='6f30', sfid=None, name='EF.PLMNsel', desc='PLMN selector',
|
||||||
size=(24, None), rec_len=3, **kwargs):
|
size=(24, None), rec_len=3, **kwargs):
|
||||||
@@ -661,7 +722,7 @@ class EF_AD(TransparentEF):
|
|||||||
# TS 51.011 Section 10.3.20 / 10.3.22
|
# TS 51.011 Section 10.3.20 / 10.3.22
|
||||||
class EF_VGCS(TransRecEF):
|
class EF_VGCS(TransRecEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( "92f9ffff", "299" ),
|
( "92f9ffff", ["299"] ),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='6fb1', sfid=None, name='EF.VGCS', size=(4, 200), rec_len=4,
|
def __init__(self, fid='6fb1', sfid=None, name='EF.VGCS', size=(4, 200), rec_len=4,
|
||||||
desc='Voice Group Call Service', **kwargs):
|
desc='Voice Group Call Service', **kwargs):
|
||||||
@@ -797,9 +858,9 @@ class EF_LOCIGPRS(TransparentEF):
|
|||||||
# TS 51.011 Section 10.3.35..37
|
# TS 51.011 Section 10.3.35..37
|
||||||
class EF_xPLMNwAcT(TransRecEF):
|
class EF_xPLMNwAcT(TransRecEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( '62F2104000', { "mcc": "262", "mnc": "01", "act": [ "E-UTRAN NB-S1", "E-UTRAN WB-S1" ] } ),
|
( '62F2104000', [{ "mcc": "262", "mnc": "01", "act": [ "E-UTRAN NB-S1", "E-UTRAN WB-S1" ] }] ),
|
||||||
( '62F2108000', { "mcc": "262", "mnc": "01", "act": [ "UTRAN" ] } ),
|
( '62F2108000', [{ "mcc": "262", "mnc": "01", "act": [ "UTRAN" ] }] ),
|
||||||
( '62F220488C', { "mcc": "262", "mnc": "02", "act": ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'NG-RAN'] } ),
|
( '62F220488C', [{ "mcc": "262", "mnc": "02", "act": ['E-UTRAN NB-S1', 'E-UTRAN WB-S1', 'EC-GSM-IoT', 'GSM', 'NG-RAN'] }] ),
|
||||||
]
|
]
|
||||||
def __init__(self, fid='1234', sfid=None, name=None, desc=None, size=(40, None), rec_len=5, **kwargs):
|
def __init__(self, fid='1234', sfid=None, name=None, desc=None, size=(40, None), rec_len=5, **kwargs):
|
||||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
|
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, rec_len=rec_len, **kwargs)
|
||||||
@@ -1034,9 +1095,10 @@ class EF_ICCID(TransparentEF):
|
|||||||
# TS 102 221 Section 13.3 / TS 31.101 Section 13 / TS 51.011 Section 10.1.2
|
# TS 102 221 Section 13.3 / TS 31.101 Section 13 / TS 51.011 Section 10.1.2
|
||||||
class EF_PL(TransRecEF):
|
class EF_PL(TransRecEF):
|
||||||
_test_de_encode = [
|
_test_de_encode = [
|
||||||
( '6465', "de" ),
|
( '6465', ["de"] ),
|
||||||
( '656e', "en" ),
|
( '656e', ["en"] ),
|
||||||
( 'ffff', None ),
|
( 'ffff', [None] ),
|
||||||
|
( '656e64657275ffffffff', ["en", "de", "ru", None, None] ),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, fid='2f05', sfid=0x05, name='EF.PL', desc='Preferred Languages'):
|
def __init__(self, fid='2f05', sfid=0x05, name='EF.PL', desc='Preferred Languages'):
|
||||||
@@ -1117,8 +1179,8 @@ class DF_GSM(CardDF):
|
|||||||
EF_MBI(),
|
EF_MBI(),
|
||||||
EF_MWIS(),
|
EF_MWIS(),
|
||||||
EF_CFIS(),
|
EF_CFIS(),
|
||||||
EF_EXT('6fc8', None, 'EF.EXT6', desc='Externsion6 (MBDN)'),
|
EF_EXT('6fc8', None, 'EF.EXT6', desc='Extension6 (MBDN)'),
|
||||||
EF_EXT('6fcc', None, 'EF.EXT7', desc='Externsion7 (CFIS)'),
|
EF_EXT('6fcc', None, 'EF.EXT7', desc='Extension7 (CFIS)'),
|
||||||
EF_SPDI(),
|
EF_SPDI(),
|
||||||
EF_MMSN(),
|
EF_MMSN(),
|
||||||
EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'),
|
EF_EXT('6fcf', None, 'EF.EXT8', desc='Extension8 (MMSN)'),
|
||||||
@@ -1201,9 +1263,11 @@ 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
|
"""
|
||||||
# of TS 102.221 (FcpTemplate), so that higher-level code only has to deal with one
|
Decode the select response to a dict representation, similar to the one of TS 102.221 (see ts_102_221.py,
|
||||||
# format of SELECT response
|
class FcpTemplate), so that higher-level code only has to deal with one respresentation. See also
|
||||||
|
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',
|
||||||
@@ -1241,13 +1305,24 @@ 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:10])
|
ret['access_conditions'] = b2h(resp_bin[8:11])
|
||||||
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 resp_bin[11] & 0x04:
|
elif lcsi & 0xFD == 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'] = 'terminated'
|
ret['life_cycle_status_int'] = lcsi
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
+2
-2
@@ -139,7 +139,6 @@ def enc_plmn(mcc: Hexstr, mnc: Hexstr) -> Hexstr:
|
|||||||
|
|
||||||
def dec_plmn(threehexbytes: Hexstr) -> dict:
|
def dec_plmn(threehexbytes: Hexstr) -> dict:
|
||||||
res = {'mcc': "0", 'mnc': "0"}
|
res = {'mcc': "0", 'mnc': "0"}
|
||||||
dec_mcc_from_plmn_str(threehexbytes)
|
|
||||||
res['mcc'] = dec_mcc_from_plmn_str(threehexbytes)
|
res['mcc'] = dec_mcc_from_plmn_str(threehexbytes)
|
||||||
res['mnc'] = dec_mnc_from_plmn_str(threehexbytes)
|
res['mnc'] = dec_mnc_from_plmn_str(threehexbytes)
|
||||||
return res
|
return res
|
||||||
@@ -911,7 +910,8 @@ class DataObjectCollection:
|
|||||||
def encode(self, decoded) -> bytes:
|
def encode(self, decoded) -> bytes:
|
||||||
res = bytearray()
|
res = bytearray()
|
||||||
for i in decoded:
|
for i in decoded:
|
||||||
obj = self.members_by_name(i[0])
|
name = i[0]
|
||||||
|
obj = self.members_by_name[name]
|
||||||
res.append(obj.to_tlv())
|
res.append(obj.to_tlv())
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ 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]+"]
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
pyscard
|
pyscard
|
||||||
pyserial
|
pyserial
|
||||||
pytlv
|
pytlv
|
||||||
cmd2>=2.6.2,<3.0
|
cmd2>=2.6.2,<4.0
|
||||||
jsonpath-ng
|
jsonpath-ng
|
||||||
construct>=2.10.70
|
construct>=2.10.70
|
||||||
bidict
|
bidict
|
||||||
pyosmocom>=0.0.12
|
pyosmocom>=0.0.12
|
||||||
pyyaml>=5.1
|
pyyaml>=5.4
|
||||||
termcolor
|
termcolor
|
||||||
colorlog
|
colorlog
|
||||||
pycryptodomex
|
pycryptodomex
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ setup(
|
|||||||
"pyscard",
|
"pyscard",
|
||||||
"pyserial",
|
"pyserial",
|
||||||
"pytlv",
|
"pytlv",
|
||||||
"cmd2 >= 1.5.0, < 3.0",
|
"cmd2 >= 2.6.2, < 4.0",
|
||||||
"jsonpath-ng",
|
"jsonpath-ng",
|
||||||
"construct >= 2.10.70",
|
"construct >= 2.10.70",
|
||||||
"bidict",
|
"bidict",
|
||||||
"pyosmocom >= 0.0.12",
|
"pyosmocom >= 0.0.12",
|
||||||
"pyyaml >= 5.1",
|
"pyyaml >= 5.4",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"colorlog",
|
"colorlog",
|
||||||
"pycryptodomex",
|
"pycryptodomex",
|
||||||
|
|||||||
+3
-3
@@ -2200,9 +2200,9 @@ update_record 6 fe0112ffb53e96e5ff99731d51ad7beafd0e23ffffffffffffffffffffffffff
|
|||||||
update_record 7 fe02101da012f436d06824ecdd15050419ff9affffffffffffffffffffffffffffffff
|
update_record 7 fe02101da012f436d06824ecdd15050419ff9affffffffffffffffffffffffffffffff
|
||||||
update_record 8 fe02116929a373388ac904aff57ff57f6b3431ffffffffffffffffffffffffffffffff
|
update_record 8 fe02116929a373388ac904aff57ff57f6b3431ffffffffffffffffffffffffffffffff
|
||||||
update_record 9 fe0212a99245a5dc814e2f4c1aa908e9946e03ffffffffffffffffffffffffffffffff
|
update_record 9 fe0212a99245a5dc814e2f4c1aa908e9946e03ffffffffffffffffffffffffffffffff
|
||||||
update_record 10 fe0310521312c05a9aea93d70d44405172a580ffffffffffffffffffffffffffffffff
|
update_record 10 fe03601111111111111111111111111111111111111111111111111111111111111111
|
||||||
update_record 11 fe0311a9e45c72d45abde7db74261ee0c11b1bffffffffffffffffffffffffffffffff
|
update_record 11 fe03612222222222222222222222222222222222222222222222222222222222222222
|
||||||
update_record 12 fe0312867ba36b5873d60ea8b2cdcf3c0ddddaffffffffffffffffffffffffffffffff
|
update_record 12 fe03623333333333333333333333333333333333333333333333333333333333333333
|
||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
# MF/DF.SYSTEM/EF.SIM_AUTH_COUNTER #
|
# MF/DF.SYSTEM/EF.SIM_AUTH_COUNTER #
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
Using PC/SC reader interface
|
INFO: Using PC/SC reader interface
|
||||||
Reading ...
|
Reading ...
|
||||||
Autodetected card type: Fairwaves-SIM
|
Autodetected card type: Fairwaves-SIM
|
||||||
ICCID: 8988219000000117833
|
ICCID: 8988219000000117833
|
||||||
IMSI: 001010000000111
|
IMSI: 001010000000111
|
||||||
GID1: ffffffffffffffff
|
GID1: ffffffffffffffff
|
||||||
GID2: ffffffffffffffff
|
GID2: ffffffffffffffff
|
||||||
SMSP: e1ffffffffffffffffffffffff0581005155f5ffffffffffff000000ffffffffffffffffffffffffffff
|
SMSP: ffffffffffffffffffffffffffffe1ffffffffffffffffffffffff0581005155f5ffffffffffff000000
|
||||||
SMSC: 0015555
|
SMSC: 0015555
|
||||||
SPN: Fairwaves
|
SPN: Fairwaves
|
||||||
Show in HPLMN: False
|
Show in HPLMN: False
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
Using PC/SC reader interface
|
INFO: Using PC/SC reader interface
|
||||||
Reading ...
|
Reading ...
|
||||||
Autodetected card type: Wavemobile-SIM
|
Autodetected card type: Wavemobile-SIM
|
||||||
ICCID: 89445310150011013678
|
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: e1ffffffffffffffffffffffff0581005155f5ffffffffffff000000ffffffffffffffffffffffffffff
|
SMSP: ffffffffffffffffffffffffffffe1ffffffffffffffffffffffff0581005155f5ffffffffffff000000
|
||||||
SMSC: 0015555
|
SMSC: 0015555
|
||||||
SPN: wavemobile
|
SPN: wavemobile
|
||||||
Show in HPLMN: False
|
Show in HPLMN: False
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Using PC/SC reader interface
|
INFO: Using PC/SC reader interface
|
||||||
Reading ...
|
Reading ...
|
||||||
Autodetected card type: fakemagicsim
|
Autodetected card type: fakemagicsim
|
||||||
ICCID: 1122334455667788990
|
ICCID: 1122334455667788990
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Using PC/SC reader interface
|
INFO: Using PC/SC reader interface
|
||||||
Reading ...
|
Reading ...
|
||||||
Autodetected card type: sysmoISIM-SJA2
|
Autodetected card type: sysmoISIM-SJA2
|
||||||
ICCID: 8988211000000467343
|
ICCID: 8988211000000467343
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Using PC/SC reader interface
|
INFO: Using PC/SC reader interface
|
||||||
Reading ...
|
Reading ...
|
||||||
Autodetected card type: sysmoISIM-SJA5
|
Autodetected card type: sysmoISIM-SJA5
|
||||||
ICCID: 8949440000001155314
|
ICCID: 8949440000001155314
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Using PC/SC reader interface
|
INFO: Using PC/SC reader interface
|
||||||
Reading ...
|
Reading ...
|
||||||
Autodetected card type: sysmoUSIM-SJS1
|
Autodetected card type: sysmoUSIM-SJS1
|
||||||
ICCID: 8988211320300000028
|
ICCID: 8988211320300000028
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Using PC/SC reader interface
|
INFO: Using PC/SC reader interface
|
||||||
Reading ...
|
Reading ...
|
||||||
Autodetected card type: sysmosim-gr1
|
Autodetected card type: sysmosim-gr1
|
||||||
ICCID: 2222334455667788990
|
ICCID: 2222334455667788990
|
||||||
|
|||||||
@@ -7,10 +7,24 @@ 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,9 +0,0 @@
|
|||||||
# Card parameter:
|
|
||||||
ICCID="8949440000001155314"
|
|
||||||
KIC='51D4FC44BCBA7C4589DFADA3297720AF'
|
|
||||||
KID='0449699C472CE71E2FB7B56245EF7684'
|
|
||||||
|
|
||||||
# Testcase: Send OTA-SMS that selects DF.GSM and returns the select response
|
|
||||||
TAR='B00010'
|
|
||||||
APDU='A0A40000027F20A0C0000016'
|
|
||||||
EXPECTED_RESPONSE='0000ffff7f2002000000000009b106350400838a838a 9000'
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Utility to verify the functionality of pySim-trace.py
|
# Utility to verify the functionality of pySim-smpp2sim.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
|
||||||
@@ -20,13 +20,14 @@
|
|||||||
# 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/>.
|
||||||
|
|
||||||
|
PYSIM_SHELL=./pySim-shell.py
|
||||||
|
PYSIM_SHELL_LOG=./pySim-shell.log
|
||||||
PYSIM_SMPP2SIM=./pySim-smpp2sim.py
|
PYSIM_SMPP2SIM=./pySim-smpp2sim.py
|
||||||
PYSIM_SMPP2SIM_LOG=./pySim-smpp2sim.log
|
PYSIM_SMPP2SIM_LOG=./pySim-smpp2sim.log
|
||||||
PYSIM_SMPP2SIM_PORT=2775
|
PYSIM_SMPP2SIM_PORT=2775
|
||||||
PYSIM_SMPP2SIM_TIMEOUT=10
|
PYSIM_SMPP2SIM_TIMEOUT=10
|
||||||
PYSIM_SMPPOTATOOL=./contrib/smpp-ota-tool.py
|
PYSIM_SMPPOTATOOL=./contrib/smpp-ota-tool.py
|
||||||
PYSIM_SMPPOTATOOL_LOG=./smpp-ota-tool.log
|
PYSIM_SMPPOTATOOL_LOG=./smpp-ota-tool.log
|
||||||
PYSIM_SHELL=./pySim-shell.py
|
|
||||||
|
|
||||||
function dump_logs {
|
function dump_logs {
|
||||||
echo ""
|
echo ""
|
||||||
@@ -44,12 +45,11 @@ function dump_logs {
|
|||||||
function send_test_request {
|
function send_test_request {
|
||||||
echo ""
|
echo ""
|
||||||
echo "Sending request to SMPP server:"
|
echo "Sending request to SMPP server:"
|
||||||
TAR=$1
|
C_APDU=$1
|
||||||
C_APDU=$2
|
R_APDU_EXPECTED=$2
|
||||||
R_APDU_EXPECTED=$3
|
|
||||||
|
|
||||||
echo "Sending: $C_APDU"
|
echo "Sending: $C_APDU"
|
||||||
COMMANDLINE="$PYSIM_SMPPOTATOOL --verbose --port $PYSIM_SMPP2SIM_PORT --kic $KIC --kid $KID --tar $TAR --apdu $C_APDU"
|
COMMANDLINE="$PYSIM_SMPPOTATOOL --verbose --port $PYSIM_SMPP2SIM_PORT --kic $KIC --kid $KID --kic-idx $KEY_INDEX --kid-idx $KEY_INDEX --algo-crypt $ALGO_CRYPT --algo-auth $ALGO_AUTH --tar $TAR --apdu $C_APDU"
|
||||||
echo "Commandline: $COMMANDLINE"
|
echo "Commandline: $COMMANDLINE"
|
||||||
R_APDU=`$COMMANDLINE 2> $PYSIM_SMPPOTATOOL_LOG`
|
R_APDU=`$COMMANDLINE 2> $PYSIM_SMPPOTATOOL_LOG`
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@@ -57,7 +57,7 @@ function send_test_request {
|
|||||||
dump_logs
|
dump_logs
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo ""
|
||||||
echo "Got response from SMPP server:"
|
echo "Got response from SMPP server:"
|
||||||
echo "Sent: $C_APDU"
|
echo "Sent: $C_APDU"
|
||||||
echo "Received: $R_APDU"
|
echo "Received: $R_APDU"
|
||||||
@@ -68,16 +68,14 @@ function send_test_request {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Response matches the expected response -- success!"
|
echo "Response matches the expected response -- success!"
|
||||||
echo ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function start_smpp_server {
|
function start_smpp_server {
|
||||||
PCSC_READER=$1
|
PCSC_READER=$1
|
||||||
|
|
||||||
# Start the SMPP server
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Starting SMPP server:"
|
echo "Starting SMPP server:"
|
||||||
|
|
||||||
|
# Start the SMPP server
|
||||||
COMMANDLINE="$PYSIM_SMPP2SIM -p $PCSC_READER --smpp-bind-port $PYSIM_SMPP2SIM_PORT --apdu-trace"
|
COMMANDLINE="$PYSIM_SMPP2SIM -p $PCSC_READER --smpp-bind-port $PYSIM_SMPP2SIM_PORT --apdu-trace"
|
||||||
echo "Commandline: $COMMANDLINE"
|
echo "Commandline: $COMMANDLINE"
|
||||||
$COMMANDLINE > $PYSIM_SMPP2SIM_LOG 2>&1 &
|
$COMMANDLINE > $PYSIM_SMPP2SIM_LOG 2>&1 &
|
||||||
@@ -102,55 +100,117 @@ function start_smpp_server {
|
|||||||
echo "SMPP server reachable (port=$PYSIM_SMPP2SIM_PORT)"
|
echo "SMPP server reachable (port=$PYSIM_SMPP2SIM_PORT)"
|
||||||
}
|
}
|
||||||
|
|
||||||
function find_card_by_iccid {
|
function stop_smpp_server {
|
||||||
# Find reader number of the card
|
echo ""
|
||||||
ICCID=$1
|
echo "Stopping SMPP server:"
|
||||||
|
kill $PYSIM_SMPP2SIM_PID
|
||||||
|
echo "SMPP server stopped (PID=$PYSIM_SMPP2SIM_PID)"
|
||||||
|
trap EXIT
|
||||||
|
}
|
||||||
|
|
||||||
|
function find_card_by_iccid_or_eid {
|
||||||
|
ICCID=$1
|
||||||
|
EID=$2
|
||||||
echo ""
|
echo ""
|
||||||
echo "Searching for card:"
|
echo "Searching for card:"
|
||||||
echo "ICCID: \"$ICCID\""
|
echo "ICCID: \"$ICCID\""
|
||||||
|
if [ -n "$EID" ]; then
|
||||||
|
echo "EID: \"$EID\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine number of available PCSC readers
|
||||||
|
PCSC_READER_COUNT=`pcsc_scan -rn | wc -l`
|
||||||
|
|
||||||
|
# In case an EID is set, search for a card with that EID first
|
||||||
|
if [ -n "$EID" ]; then
|
||||||
|
for PCSC_READER in $(seq 0 $(($PCSC_READER_COUNT-1))); do
|
||||||
|
echo "probing card (eID) in reader $PCSC_READER ..."
|
||||||
|
RESULT_JSON=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e "select ADF.ISD-R" -e "get_eid" 2> /dev/null | tail -3`
|
||||||
|
echo $RESULT_JSON | grep $EID > /dev/null
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Found card (eID) in reader $PCSC_READER"
|
||||||
|
return $PCSC_READER
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Search for card with the given ICCID
|
||||||
if [ -z "$ICCID" ]; then
|
if [ -z "$ICCID" ]; then
|
||||||
echo "invalid ICCID, zero length ICCID is not allowed! -- abort"
|
echo "invalid ICCID, zero length ICCID is not allowed! -- abort"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PCSC_READER_COUNT=`pcsc_scan -rn | wc -l`
|
|
||||||
for PCSC_READER in $(seq 0 $(($PCSC_READER_COUNT-1))); do
|
for PCSC_READER in $(seq 0 $(($PCSC_READER_COUNT-1))); do
|
||||||
echo "probing card in reader $PCSC_READER ..."
|
echo "probing card (ICCID) in reader $PCSC_READER ..."
|
||||||
EF_ICCID_DECODED=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e 'select EF.ICCID' -e 'read_binary_decoded --oneline' 2> /dev/null | tail -1`
|
RESULT_JSON=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e "select EF.ICCID" -e "read_binary_decoded" 2> /dev/null | tail -3`
|
||||||
echo $EF_ICCID_DECODED | grep $ICCID > /dev/null
|
echo $RESULT_JSON | grep $ICCID > /dev/null
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "Found card in reader $PCSC_READER"
|
echo "Found card (by ICCID) in reader $PCSC_READER"
|
||||||
return $PCSC_READER
|
return $PCSC_READER
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Card with ICCID \"$ICCID\" not found -- abort"
|
echo "Card not found -- abort"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enable_profile {
|
||||||
|
PCSC_READER=$1
|
||||||
|
ICCID=$2
|
||||||
|
EID=$3
|
||||||
|
if [ -z "$EID" ]; then
|
||||||
|
# This is no eUICC, nothing to enable
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the profile is already enabled
|
||||||
|
RESULT_JSON=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e "select EF.ICCID" -e "read_binary_decoded" 2> /dev/null | tail -3`
|
||||||
|
ICCID_ENABLED=`echo $RESULT_JSON | jq -r '.iccid'`
|
||||||
|
if [ $ICCID != $ICCID_ENABLED ]; then
|
||||||
|
# Disable the currentle enabled profile
|
||||||
|
echo ""
|
||||||
|
echo "Disabeling currently enabled profile:"
|
||||||
|
echo "ICCID: \"$ICCID\""
|
||||||
|
RESULT_JSON=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e "select ADF.ISD-R" -e "disable_profile --iccid $ICCID_ENABLED" 2> /dev/null | tail -3`
|
||||||
|
echo $RESULT_JSON | grep "ok" > /dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "unable to disable profile with \"$ICCID_ENABLED\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "profile disabled"
|
||||||
|
|
||||||
|
# Enable the profile we intend to test with
|
||||||
|
echo ""
|
||||||
|
echo "Enabeling profile:"
|
||||||
|
echo "ICCID: \"$ICCID\""
|
||||||
|
RESULT_JSON=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e "select ADF.ISD-R" -e "enable_profile --iccid $ICCID" 2> /dev/null | tail -3`
|
||||||
|
echo $RESULT_JSON | grep "ok\|profileNotInDisabledState" > /dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "unable to enable profile with \"$ICCID\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "profile enabled"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
export PYTHONPATH=./
|
export PYTHONPATH=./
|
||||||
|
|
||||||
echo "pySim-smpp2sim_test - a test program to test pySim-smpp2sim.py"
|
echo "pySim-smpp2sim_test - a test program to test pySim-smpp2sim.py"
|
||||||
echo "=============================================================="
|
echo "=============================================================="
|
||||||
|
|
||||||
# TODO: At the moment we can only have one card and one testcase. This is
|
TESTCASE_DIR=`dirname $0`
|
||||||
# sufficient for now. We can extend this later as needed.
|
for TEST_CONFIG_FILE in $TESTCASE_DIR/testcase_*.cfg ; do
|
||||||
|
echo ""
|
||||||
# Read test parameters from config from file
|
echo "running testcase: $TEST_CONFIG_FILE"
|
||||||
TEST_CONFIG_FILE=${0%.*}.cfg
|
. $TEST_CONFIG_FILE
|
||||||
echo "using config file: $TEST_CONFIG_FILE"
|
find_card_by_iccid_or_eid $ICCID $EID
|
||||||
if ! [ -e "$TEST_CONFIG_FILE" ]; then
|
PCSC_READER=$?
|
||||||
echo "test configuration file does not exist! -- abort"
|
enable_profile $PCSC_READER $ICCID $EID
|
||||||
exit 1
|
start_smpp_server $PCSC_READER
|
||||||
fi
|
send_test_request $APDU "$EXPECTED_RESPONSE"
|
||||||
. $TEST_CONFIG_FILE
|
stop_smpp_server
|
||||||
|
echo ""
|
||||||
# Execute testcase
|
echo "testcase ok"
|
||||||
find_card_by_iccid $ICCID
|
echo "--------------------------------------------------------------"
|
||||||
start_smpp_server $?
|
done
|
||||||
send_test_request $TAR $APDU "$EXPECTED_RESPONSE"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "done."
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Preparation:
|
||||||
|
# This testcase executes against a sysmoISIM-SJA5 card. For the testcase, the
|
||||||
|
# key configuration on the card may be used as it is.
|
||||||
|
|
||||||
|
# Card parameter:
|
||||||
|
ICCID="8949440000001155314" # <-- change to the ICCID of your card!
|
||||||
|
EID=""
|
||||||
|
KIC='51D4FC44BCBA7C4589DFADA3297720AF' # <-- change to the KIC1 of your card!
|
||||||
|
KID='0449699C472CE71E2FB7B56245EF7684' # <-- change to the KID1 of your card!
|
||||||
|
KEY_INDEX=1
|
||||||
|
ALGO_CRYPT=triple_des_cbc2
|
||||||
|
ALGO_AUTH=triple_des_cbc2
|
||||||
|
TAR='B00010'
|
||||||
|
|
||||||
|
# Testcase: Send OTA-SMS that selects DF.GSM and returns the select response
|
||||||
|
APDU='A0A40000027F20A0C0000016'
|
||||||
|
EXPECTED_RESPONSE='0000ffff7f2002000000000009b106350400838a838a 9000'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Preparation:
|
||||||
|
# This testcase executes against a sysmoEUICC1-C2T, which is equipped with the
|
||||||
|
# TS48V1-B-UNIQUE test profile from https://test.rsp.sysmocom.de/ (Activation
|
||||||
|
# code: 1$smdpp.test.rsp.sysmocom.de$TS48V1-B-UNIQUE). This testprofile must be
|
||||||
|
# present on the eUICC before this testcase can be executed.
|
||||||
|
|
||||||
|
# Card parameter:
|
||||||
|
ICCID="8949449999999990031"
|
||||||
|
EID="89049044900000000000000000102355" # <-- change to the EID of your card!
|
||||||
|
KIC='66778899aabbccdd1122334455eeff10'
|
||||||
|
KID='112233445566778899aabbccddeeff10'
|
||||||
|
KEY_INDEX=2
|
||||||
|
ALGO_CRYPT=aes_cbc
|
||||||
|
ALGO_AUTH=aes_cmac
|
||||||
|
TAR='b00120'
|
||||||
|
|
||||||
|
# Testcase: Send OTA-SMS that selects DF.ICCID and returns the select response
|
||||||
|
APDU='00a40004022fe200C000001d'
|
||||||
|
EXPECTED_RESPONSE='621b8202412183022fe2a503d001408a01058b032f06038002000a8800 9000'
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Preparation:
|
||||||
|
# This testcase executes against a sysmoISIM-SJA5 card. Since this card model is
|
||||||
|
# shipped with a classic DES key configuration, it is necessary to provision
|
||||||
|
# AES128 test keys before this testcase may be executed. The the following
|
||||||
|
# pySim-shell command sequence may be used:
|
||||||
|
#
|
||||||
|
# verify_adm 34173960 # <-- change to the ADM key of your card!
|
||||||
|
# select /DF.SYSTEM/EF.0348_KEY
|
||||||
|
# update_record 10 fe03601111111111111111111111111111111111111111111111111111111111111111
|
||||||
|
# update_record 11 fe03612222222222222222222222222222222222222222222222222222222222222222
|
||||||
|
# update_record 12 fe03623333333333333333333333333333333333333333333333333333333333333333
|
||||||
|
#
|
||||||
|
# This overwrites one of the already existing 3DES SCP02 key (KVN 47) and replaces it
|
||||||
|
# with an AES256 SCP80 key (KVN 3).
|
||||||
|
|
||||||
|
# Card parameter:
|
||||||
|
ICCID="8949440000001155314" # <-- change to the ICCID of your card!
|
||||||
|
EID=""
|
||||||
|
KIC='1111111111111111111111111111111111111111111111111111111111111111'
|
||||||
|
KID='2222222222222222222222222222222222222222222222222222222222222222'
|
||||||
|
KEY_INDEX=3
|
||||||
|
ALGO_CRYPT=aes_cbc
|
||||||
|
ALGO_AUTH=aes_cmac
|
||||||
|
TAR='B00010'
|
||||||
|
|
||||||
|
# Testcase: Send OTA-SMS that selects DF.GSM and returns the select response
|
||||||
|
APDU='A0A40000027F20A0C0000016'
|
||||||
|
EXPECTED_RESPONSE='0000ffff7f2002000000000009b106350400838a838a 9000'
|
||||||
@@ -20,7 +20,8 @@ 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_provider_register(CardKeyProviderCsv(csv_file_path, column_keys))
|
card_key_field_cryptor = CardKeyFieldCryptor(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):
|
||||||
|
|||||||
@@ -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,11 @@ class ConfigurableParameterTest(unittest.TestCase):
|
|||||||
p13n.SdKeyScp80Kvn03DesDek,
|
p13n.SdKeyScp80Kvn03DesDek,
|
||||||
#p13n.SdKeyScp80Kvn03DesEnc,
|
#p13n.SdKeyScp80Kvn03DesEnc,
|
||||||
#p13n.SdKeyScp80Kvn03DesMac,
|
#p13n.SdKeyScp80Kvn03DesMac,
|
||||||
p13n.SdKeyScp81Kvn40Dek ,
|
p13n.SdKeyScp81Kvn40AesDek,
|
||||||
#p13n.SdKeyScp81Kvn40Tlspsk,
|
#p13n.SdKeyScp81Kvn40Tlspsk,
|
||||||
#p13n.SdKeyScp81Kvn41Dek ,
|
#p13n.SdKeyScp81Kvn41AesDek,
|
||||||
p13n.SdKeyScp81Kvn41Tlspsk,
|
p13n.SdKeyScp81Kvn41Tlspsk,
|
||||||
#p13n.SdKeyScp81Kvn42Dek ,
|
#p13n.SdKeyScp81Kvn42AesDek,
|
||||||
#p13n.SdKeyScp81Kvn42Tlspsk,
|
#p13n.SdKeyScp81Kvn42Tlspsk,
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -441,6 +441,191 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/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()
|
||||||
@@ -176,12 +176,11 @@ class TransRecEF_Test(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
def test_de_encode_record(self):
|
def test_de_encode_record(self):
|
||||||
"""Test the decoder and encoder for a transparent record-oriented EF. Performs first a decoder
|
"""Test the decoder and encoder for a transparent record-oriented EF at the whole-file
|
||||||
test, and then re-encodes the decoded data, comparing the re-encoded data with the
|
level. Performs first a decode test, then re-encodes and compares with the input.
|
||||||
initial input data.
|
|
||||||
|
|
||||||
Requires the given TransRecEF subclass to have a '_test_de_encode' attribute,
|
Requires the given TransRecEF subclass to have a '_test_de_encode' attribute,
|
||||||
containing a list of tuples. Each tuple has to be a 2-tuple (hexstring, decoded_dict).
|
containing a list of 2-tuples (hexstring, decoded_list).
|
||||||
"""
|
"""
|
||||||
for c in self.classes:
|
for c in self.classes:
|
||||||
name = get_qualified_name(c)
|
name = get_qualified_name(c)
|
||||||
@@ -192,14 +191,12 @@ class TransRecEF_Test(unittest.TestCase):
|
|||||||
encoded = t[0]
|
encoded = t[0]
|
||||||
decoded = t[1]
|
decoded = t[1]
|
||||||
logging.debug("Testing decode of %s", name)
|
logging.debug("Testing decode of %s", name)
|
||||||
re_dec = inst.decode_record_hex(encoded)
|
re_dec = inst.decode_hex(encoded)
|
||||||
self.assertEqual(decoded, re_dec)
|
self.assertEqual(decoded, re_dec)
|
||||||
# re-encode the decoded data
|
# re-encode the decoded data
|
||||||
logging.debug("Testing re-encode of %s", name)
|
logging.debug("Testing re-encode of %s", name)
|
||||||
re_enc = inst.encode_record_hex(re_dec, len(encoded)//2)
|
re_enc = inst.encode_hex(re_dec, len(encoded)//2)
|
||||||
self.assertEqual(encoded.upper(), re_enc.upper())
|
self.assertEqual(encoded.upper(), re_enc.upper())
|
||||||
# there's no point in testing padded input, as TransRecEF have a fixed record
|
|
||||||
# size and we cannot ever receive more input data than that size.
|
|
||||||
|
|
||||||
|
|
||||||
class TransparentEF_Test(unittest.TestCase):
|
class TransparentEF_Test(unittest.TestCase):
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# (C) 2026 by sysmocom - s.f.m.c. GmbH <info@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/>.
|
||||||
|
|
||||||
|
"""Verify that every CardProfile / CardApplication subclass with EF/DF content,
|
||||||
|
and every standalone CardDF subclass (one not reachable as a child of any profile
|
||||||
|
or application), is either listed in docs/pysim_fs_sphinx.py::SECTIONS or
|
||||||
|
explicitly EXCLUDED."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Make docs/pysim_fs_sphinx.py importable without a full Sphinx build.
|
||||||
|
_DOCS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'docs')
|
||||||
|
sys.path.insert(0, os.path.abspath(_DOCS_DIR))
|
||||||
|
|
||||||
|
import pySim # noqa: E402
|
||||||
|
from pySim.filesystem import CardApplication, CardDF, CardMF, CardADF # noqa: E402
|
||||||
|
from pySim.profile import CardProfile # noqa: E402
|
||||||
|
from pysim_fs_sphinx import EXCLUDED, SECTIONS # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class TestFsCoverage(unittest.TestCase):
|
||||||
|
"""Ensure SECTIONS + EXCLUDED together account for all classes with content."""
|
||||||
|
|
||||||
|
# Base CardDF types that are not concrete filesystem objects on their own.
|
||||||
|
_DF_BASE_TYPES = frozenset([CardDF, CardMF, CardADF])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _collect_reachable_df_types(obj) -> set:
|
||||||
|
"""Return the set of all CardDF *types* reachable as children of *obj*."""
|
||||||
|
result = set()
|
||||||
|
if isinstance(obj, CardProfile):
|
||||||
|
children = obj.files_in_mf
|
||||||
|
elif isinstance(obj, CardApplication):
|
||||||
|
result.add(type(obj.adf))
|
||||||
|
children = list(obj.adf.children.values())
|
||||||
|
elif isinstance(obj, CardDF):
|
||||||
|
children = list(obj.children.values())
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
queue = list(children)
|
||||||
|
while queue:
|
||||||
|
child = queue.pop()
|
||||||
|
if isinstance(child, CardDF):
|
||||||
|
result.add(type(child))
|
||||||
|
queue.extend(child.children.values())
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_content(obj) -> bool:
|
||||||
|
"""Return True if *obj* owns any EFs/DFs."""
|
||||||
|
if isinstance(obj, CardProfile):
|
||||||
|
return bool(obj.files_in_mf)
|
||||||
|
if isinstance(obj, CardApplication):
|
||||||
|
return bool(obj.adf.children)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_all_profiles_and_apps_covered(self):
|
||||||
|
# build a set of (module, class-name) pairs that are already accounted for
|
||||||
|
covered = {(mod, cls) for (_, mod, cls) in SECTIONS}
|
||||||
|
accounted_for = covered | EXCLUDED
|
||||||
|
|
||||||
|
uncovered = []
|
||||||
|
reachable_df_types = set()
|
||||||
|
loaded_modules = {}
|
||||||
|
|
||||||
|
for modinfo in pkgutil.walk_packages(pySim.__path__, prefix='pySim.'):
|
||||||
|
modname = modinfo.name
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(modname)
|
||||||
|
except Exception: # skip inport errors, if any
|
||||||
|
continue
|
||||||
|
loaded_modules[modname] = module
|
||||||
|
|
||||||
|
for name, cls in inspect.getmembers(module, inspect.isclass):
|
||||||
|
# skip classes that are merely imported by this module
|
||||||
|
if cls.__module__ != modname:
|
||||||
|
continue
|
||||||
|
# examine only subclasses of CardProfile and CardApplication
|
||||||
|
if not issubclass(cls, (CardProfile, CardApplication)):
|
||||||
|
continue
|
||||||
|
# skip the abstract base classes themselves
|
||||||
|
if cls in (CardProfile, CardApplication):
|
||||||
|
continue
|
||||||
|
# classes that require constructor arguments cannot be probed
|
||||||
|
try:
|
||||||
|
obj = cls()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# collect all CardDF types reachable from this profile/application
|
||||||
|
# (used below to identify standalone DFs)
|
||||||
|
reachable_df_types |= self._collect_reachable_df_types(obj)
|
||||||
|
|
||||||
|
if self._has_content(obj) and (modname, name) not in accounted_for:
|
||||||
|
uncovered.append((modname, name))
|
||||||
|
|
||||||
|
# check standalone CardDFs (such as DF.EIRENE or DF.SYSTEM)
|
||||||
|
for modname, module in loaded_modules.items():
|
||||||
|
for name, cls in inspect.getmembers(module, inspect.isclass):
|
||||||
|
if cls.__module__ != modname:
|
||||||
|
continue
|
||||||
|
if not issubclass(cls, CardDF):
|
||||||
|
continue
|
||||||
|
if cls in self._DF_BASE_TYPES:
|
||||||
|
continue
|
||||||
|
if cls in reachable_df_types:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
obj = cls()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if obj.children and (modname, name) not in accounted_for:
|
||||||
|
uncovered.append((modname, name))
|
||||||
|
|
||||||
|
if uncovered:
|
||||||
|
lines = [
|
||||||
|
'The following classes have EFs/DFs, but not listed in SECTIONS or EXCLUDED:',
|
||||||
|
*(f' {modname}.{name}' for modname, name in sorted(uncovered)),
|
||||||
|
'Please modify docs/pysim_fs_sphinx.py accordingly',
|
||||||
|
]
|
||||||
|
self.fail('\n'.join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -295,7 +295,7 @@ class Install_param_Test(unittest.TestCase):
|
|||||||
load_parameters = gen_install_parameters(256, 256, '010001001505000000000000000000000000')
|
load_parameters = gen_install_parameters(256, 256, '010001001505000000000000000000000000')
|
||||||
self.assertEqual(load_parameters, 'c900ef1cc8020100c7020100ca12010001001505000000000000000000000000')
|
self.assertEqual(load_parameters, 'c900ef1cc8020100c7020100ca12010001001505000000000000000000000000')
|
||||||
|
|
||||||
load_parameters = gen_install_parameters(None, None, '')
|
load_parameters = gen_install_parameters()
|
||||||
self.assertEqual(load_parameters, 'c900')
|
self.assertEqual(load_parameters, 'c900')
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class ParamSourceTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_param_source(self):
|
def test_param_source(self):
|
||||||
|
|
||||||
class ParamSourceTest(D):
|
class Paramtest(D):
|
||||||
mandatory = (
|
mandatory = (
|
||||||
'param_source',
|
'param_source',
|
||||||
'n',
|
'n',
|
||||||
@@ -78,6 +78,11 @@ 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)
|
||||||
@@ -100,74 +105,59 @@ class ParamSourceTest(unittest.TestCase):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
param_source_tests = [
|
param_source_tests = [
|
||||||
ParamSourceTest(param_source=param_source.ConstantSource.from_str('123'),
|
Paramtest(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'),
|
||||||
},
|
n=10,
|
||||||
),
|
expect=expect_random,
|
||||||
ParamSourceTest(param_source=param_source.RandomDigitSource.from_str('1..999'),
|
expect_arg={'digits': decimals,
|
||||||
n=10,
|
'val_minlen': 1,
|
||||||
expect=expect_random,
|
'val_maxlen': 3}),
|
||||||
expect_arg={'digits': decimals,
|
Paramtest(param_source=param_source.RandomDigitSource.from_str('001..999'),
|
||||||
'val_minlen': 1,
|
n=10,
|
||||||
'val_maxlen': 3,
|
expect=expect_random,
|
||||||
},
|
expect_arg={'digits': decimals,
|
||||||
),
|
'val_minlen': 3,
|
||||||
ParamSourceTest(param_source=param_source.RandomDigitSource.from_str('001..999'),
|
'val_maxlen': 3}),
|
||||||
n=10,
|
Paramtest(param_source=param_source.RandomHexDigitSource.from_str('12345678'),
|
||||||
expect=expect_random,
|
n=3,
|
||||||
expect_arg={'digits': decimals,
|
expect=expect_random,
|
||||||
'val_minlen': 3,
|
expect_arg={'digits': hexadecimals,
|
||||||
'val_maxlen': 3,
|
'val_minlen': 8,
|
||||||
},
|
'val_maxlen': 8}),
|
||||||
),
|
Paramtest(param_source=param_source.RandomHexDigitSource.from_str('0*8'),
|
||||||
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('00*4'),
|
||||||
},
|
n=3,
|
||||||
),
|
expect=expect_random,
|
||||||
ParamSourceTest(param_source=param_source.RandomHexDigitSource.from_str('0*8'),
|
expect_arg={'digits': hexadecimals,
|
||||||
n=3,
|
'val_minlen': 8,
|
||||||
expect=expect_random,
|
'val_maxlen': 8}),
|
||||||
expect_arg={'digits': hexadecimals,
|
Paramtest(param_source=param_source.IncDigitSource.from_str('10001'),
|
||||||
'val_minlen': 8,
|
n=3,
|
||||||
'val_maxlen': 8,
|
expect=expect_const,
|
||||||
},
|
expect_arg=('10001', '10002', '10003')),
|
||||||
),
|
Paramtest(param_source=param_source.CsvSource('column_name'),
|
||||||
ParamSourceTest(param_source=param_source.RandomHexDigitSource.from_str('00*4'),
|
n=3,
|
||||||
n=3,
|
expect=expect_const,
|
||||||
expect=expect_random,
|
expect_arg=('first val', 'second val', 'third val'),
|
||||||
expect_arg={'digits': hexadecimals,
|
csv_rows=(
|
||||||
'val_minlen': 8,
|
{'column_name': 'first val'},
|
||||||
'val_maxlen': 8,
|
{'column_name': 'second val'},
|
||||||
},
|
{'column_name': 'third val'},
|
||||||
),
|
)),
|
||||||
ParamSourceTest(param_source=param_source.IncDigitSource.from_str('10001'),
|
|
||||||
n=3,
|
|
||||||
expect=expect_const,
|
|
||||||
expect_arg=('10001', '10002', '10003')
|
|
||||||
),
|
|
||||||
ParamSourceTest(param_source=param_source.CsvSource('column_name'),
|
|
||||||
n=3,
|
|
||||||
expect=expect_const,
|
|
||||||
expect_arg=('first val', 'second val', 'third val'),
|
|
||||||
csv_rows=(
|
|
||||||
{'column_name': 'first val',},
|
|
||||||
{'column_name': 'second val',},
|
|
||||||
{'column_name': 'third val',},
|
|
||||||
)
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = []
|
outputs = []
|
||||||
|
|||||||
@@ -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,6 +127,42 @@ 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}
|
||||||
@@ -551,25 +587,57 @@ 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 SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
|
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
|
ok: TS48v5_SAIP2.1A_NoBERTLV.der SdKeyScp81Kvn40AesDek(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-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-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
|
||||||
@@ -701,7 +769,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
|
||||||
@@ -713,7 +781,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
|
||||||
@@ -751,6 +819,42 @@ 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}
|
||||||
@@ -1175,25 +1279,57 @@ 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 SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
|
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
|
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der SdKeyScp81Kvn40AesDek(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-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-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
|
||||||
@@ -1325,7 +1461,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
|
||||||
@@ -1337,7 +1473,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
|
||||||
@@ -1375,6 +1511,42 @@ 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}
|
||||||
@@ -1799,25 +1971,57 @@ 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 SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
|
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
|
ok: TS48v5_SAIP2.1B_NoBERTLV.der SdKeyScp81Kvn40AesDek(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-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-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
|
||||||
@@ -1949,7 +2153,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
|
||||||
@@ -1961,7 +2165,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
|
||||||
@@ -1999,6 +2203,42 @@ 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}
|
||||||
@@ -2423,25 +2663,57 @@ 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 SdKeyScp81Kvn40Dek(val= '01020304050607080910111213141516':str)
|
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
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-DEK': '01020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-DEK': '01020304050607080910111213141516'}:{hexstr}
|
||||||
|
|
||||||
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40Dek(val= 11020304050607080910111213141516:int)
|
ok: TS48v5_SAIP2.3_NoBERTLV.der SdKeyScp81Kvn40AesDek(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-DEK': '11020304050607080910111213141516'}:{hexstr}
|
read_back_val= {'SCP81-KVN40-AES-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
|
||||||
|
|||||||
Reference in New Issue
Block a user