24 Commits

Author SHA1 Message Date
Philipp Maier
e5f56dd35f pySim/transport: fix GET RESPONSE behaviour
The current behavior we implement in the method __send_apdu_T0 is
incomplete. Some details discussed in ETSI TS 102 221,
section 7.3.1.1.4, clause 4 seem to be not fully implemented. We
may also end up sending a GET RESPONSE in other APDU cases than
case 4 (the only case that uses the GET RESPONSE command).

Related: OS#6970
Change-Id: I26f0566af0cdd61dcc97f5f502479dc76adc37cc
2026-03-10 16:58:27 +01:00
Philipp Maier
c3edcf7294 pySim-prog/pySim-read: add pySimLogger and verbose cmdline argument
pySim-prog and pySim-read do not integrate the pySimLogger yet. As we
may add more debug output that should not be visible on normal use, we
should ensure that the pySimLogger is correctly set up.

Change-Id: Ia2fa535fd9ce4ffa301c3f5d6f98c1f7a4716c74
2026-03-10 16:58:27 +01:00
Philipp Maier
858c9eb421 pySim-shell/cosmetic: remove semicolon
Change-Id: I629bacd432491211b939fcd2bed554b44ef441bc
2026-03-10 16:58:27 +01:00
Philipp Maier
a48b9e565a PySimLogger: add parameter to set initial log-level/verbosity
When we initialize a new PySimLogger, we always call the setup method
first and then use the set_verbose and set_level method to configure
the initial log level and the initial log verbosity. However, we
initialize the PySimLogger in all our programs the same way and we
end up with the same boilerplate code every time. Let's add a keyword
parameter to the setup method where we can pass our opts.verbose (bool)
parameter so that the setup method can do the work for the main program.

In case the caller wants a different default configuration he still can
call set_verbose and set_level methods as needed.

Change-Id: I4b8ef1e203186878910c9614a1d900d5759236a8
2026-03-10 16:58:27 +01:00
Philipp Maier
914abe3309 docs/smpp-ota-tool: Add documentation/tutorial
We already have documentation that explains how to run pySim-smpp2sim.
With smpp-ota-tool we now have a counterpart for pySim-smpp2sim, so
let's add documentation for this tool as well.

Related: SYS#7881
Change-Id: If0d18a263f5a6dc035b90f5c5c6a942d46bbba49
2026-03-10 09:23:03 +00:00
Philipp Maier
84754b6ebb contrib/smpp-ota-tool: define commandline arguments in global scope
The commandline arguments are currently defined under __main__ in a
private scope. From there they are not reachable to the sphinx
argparse module. We have to define the arguments globally at the
top. (like in the other applications)

Related: SYS#7881
Change-Id: I2d9782e3f5b1cac78c22d206fdcac4118c7d5e7c
2026-03-10 09:23:03 +00:00
Philipp Maier
c47005d408 contrib/smpp-ota-tool: use '-' instead of '_' in command line args
Some commandline arguments have an underscore in their name. Let's
replace those with dashes.

Change-Id: Icbe9d753d59263997e9ca34d46ed0daca36ca16c
Related: SYS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
2dfaac6e4f contrib/smpp-ota-tool: fix description string (copy+paste error)
Change-Id: I559844bfa1ac372370ef9d148f2f8a6bf4ab4ef5
Related: SYS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
a615ba5138 tests/pySim-smpp2sim_test: add testcases for AES128 and AES256
Extend the existing test script so that it can handle multiple
testcases. Also add support for switching eUICC profiles.
Finally, add a testcases to test OTA-SMS (RFM) with AES128 and
AES256 encryption.

Change-Id: I1f10504f3a29a8c74a17991632d932819fecfa5a
Related: OS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
8ee10ab1a5 tests/pySim-smpp2sim_test/card_sanitizer: update card backup with new test keyset
In our test setup we run the card_sanitizer.py script regualary to ensure that
we have consistent start conditions when running our tests. In case a testcase
crashes for some reason and leaves messed up files on a test card. The
card_sanitizer.py script will ensure that any problem like that is cleaned up
over night.

For the testcases we are about to add in the patch following this one, we need
to provision a new test keyset to one of our test cards. This has been already
done manually. However since the card_sanitizer still has the old keys in its
backup we will have to update that as well.

Change-Id: I5aa8a413b19b3e43a79d03e904daab50b4b1e767
Related: OS#6868
2026-03-10 09:23:03 +00:00
Philipp Maier
f10af30aed global_platform/scp: fix dek_encrypt/dek_decrypt for SCP02
The methods dek_encrypt/dek_decrypt use the wrong algorithm and the
wrong key material. The algorithm should be 3DES rather then single
DES and the key must be the DEK session key instead of the static
DEK key from which the DEK session key is derived.

Related: SYS#7902
Change-Id: I3d0cc7378680b346fa39152c8b7074446d2c869d
2026-03-06 15:51:19 +01:00
Neels Hofmeyr
d8f3c78135 SmspTpScAddr: set example_input
Change-Id: Ie2c367788215d746807be24051478f0032a19448
2026-03-04 00:24:02 +01:00
Neels Hofmeyr
6b9b46a5a4 MilenageRotationConstants: set example_input to 3GPP default
Change-Id: I36a9434b2f96d26d710f489d5afce1f0ef05bba1
2026-03-04 00:23:41 +01:00
Philipp Maier
b6b4501e37 contrib/smpp-ota-tool: fix boolean commandline parameters
Boolean parameters should be false by default and use store_true when
set.

Change-Id: I0652b48d2ea5efbaaf5bc147aa8cef7ab8b0861d
Related: OS#6868
2026-02-24 09:52:48 +01:00
Philipp Maier
54658fa3a9 contrib/smpp-ota-tool: add missing usage helpstrings
Change-Id: Ic1521ba11b405f311a30fdb3585ad518375669ae
Related: OS#6868
2026-02-24 09:52:48 +01:00
Philipp Maier
eb04bb1082 contrib/smpp-ota-tool: warn about mixed up KIC/KIC indexes
Cards usually have multiple sets of KIC, KID (and KIK). The keys
are selected through an index. However, mixing keys from different
sets is concidered as a security violation and cards should reject
such configurations.

Let's print a warning to make users aware that something is off.

Change-Id: Ieb4e14145baba1c2cb4a237b612b04694940f402
Related: OS#6868
2026-02-24 09:52:48 +01:00
Philipp Maier
453fde5a3a contrib/smpp-ota-tool: use correct kid index
(normally KID index and KIC index should be the same since mixing keys
is a concidered as a security violation. However, in this tool we
want to allow users to specify different indexes for KIC and KIC so that
they can make tests to make sure their cards correctly reject mixed up
key indexes)

Change-Id: I8847ccc39e4779971187e7877b8902fca7f8bfc1
Related: OS#6868
2026-02-17 15:24:25 +01:00
Philipp Maier
57237b650e contrib/smpp-ota-tool/cosmetic: use lazy formatting for logging
Change-Id: I2540472a50b7a49b5a67d088cbdd4a2228eef8f4
Related: OS#6868
2026-02-17 15:24:25 +01:00
Philipp Maier
1f94791240 contrib/smpp-ota-tool/cosmetic: fix sourcecode formatting
Change-Id: Icbce41ffac097d2ef8714bc8963536ba54a77db2
Related: OS#6868
2026-02-17 15:24:25 +01:00
Philipp Maier
1a28575327 pySim-shell_test/euicc: ensure test-profile is enabled
When testing commands like get_profile_info, enable_profile,
disable_profile or the commands to manage notifications, we
should ensure that the correct profile is enabled before
executing the actual testcase.

Change-Id: Ie57b0305876bc5001ab3a9c3a3b5711408161b74
2026-02-17 09:22:42 +00:00
Neels Hofmeyr
e7016b5b57 compile_asn1_subdir: filter compiled files by .asn suffix
When I open the .asn file in vim, pySim should not attempt to read the
vim .swp file as asn.1.

	  File "/home/moi/osmo-dev/src/pysim/pySim/esim/saip/__init__.py", line 45, in <module>
	    asn1 = compile_asn1_subdir('saip')
	[...]
	  File "<frozen codecs>", line 325, in decode
	UnicodeDecodeError: 'utf-8' codec can't decode byte 0xad in position 21: invalid start byte

Related: OS#6937
Change-Id: I37df3fc081e51e2ed2198876c63f6e68ecc8fcd8
2026-02-10 16:14:14 +00:00
Philipp Maier
e80f3160a9 pySim/euicc: fix encoding/decoding of Iccid
The class Iccid uses a BcdAdapter to encoded/decode the ICCID. This
works fine for ICCIDs that have an even (20) number of digits. In case
the digit count is odd (19), the ICCID the last digit requires padding.

Let's switch to PaddedBcdAdapter for encoding/decoding, to ensure that
odd-length ICCIDs are padded automatically.

Change-Id: I527a44ba454656a0d682ceb590eec6d9d0ac883a
Related: OS#6868
2026-02-10 13:26:45 +00:00
Neels Hofmeyr
917ad7f9f5 gitignore: fix vim swp file pattern
Change-Id: I5a8351dc09f6ca7c8e9032ff8352e5cf1a4833a3
2026-02-10 13:10:17 +00:00
Philipp Maier
8b2a49aa8e esim/http_json_api: add alternative API interface (follow up)
This is a follow up patch to change:
I2a5d4b59b12e08d5eae7a1215814d3a69c8921f6

- do not ignore length of kwargs
- fix role parameter (roles other than 'legacy_client' can be used now)
- use startswith instead of match

Related: SYS#7866
Change-Id: Ifae13e82d671ff09bddf771f063a388d2ab283eb
2026-02-10 13:42:44 +01:00
34 changed files with 509 additions and 143 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
*.pyc
.*.swp
.*.sw?
/docs/_*
/docs/generated

View File

@@ -285,10 +285,7 @@ if __name__ == '__main__':
option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False)
opts = option_parser.parse_args()
PySimLogger.setup(print, {logging.WARN: "\033[33m"})
if (opts.verbose):
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
# Open CSV file
cr = open_csv(opts)

View File

@@ -30,6 +30,48 @@ from pathlib import Path
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:
client = None
@@ -141,7 +183,7 @@ class SmppHandler:
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
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)
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()
logging.basicConfig(level=logging.DEBUG if opts.verbose else logging.INFO,
format='%(asctime)s %(levelname)s %(message)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,
kic_idx=opts.kic_idx,
kic=h2b(opts.kic),
algo_auth=opts.algo_auth,
kid_idx=opts.kic_idx,
kid_idx=opts.kid_idx,
kid=h2b(opts.kid),
cntr=opts.cntr)
spi = {'counter' : opts.cntr_req,
'ciphering' : opts.ciphering,
'ciphering' : not opts.no_ciphering,
'rc_cc_ds': opts.rc_cc_ds,
'por_in_submit':opts.por_in_submit,
'por_shall_be_ciphered':opts.por_shall_be_ciphered,
'por_in_submit': opts.por_in_submit,
'por_shall_be_ciphered': not opts.por_no_ciphering,
'por_rc_cc_ds': opts.por_rc_cc_ds,
'por': opts.por_req}
apdu = h2b("".join(opts.apdu))

View File

@@ -48,6 +48,7 @@ pySim consists of several parts:
sim-rest
suci-keytool
saip-tool
smpp-ota-tool
Indices and tables

179
docs/smpp-ota-tool.rst Normal file
View File

@@ -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

View File

@@ -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'}))
INFO root: ENVELOPE: d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
INFO root: SW 9000: 027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c
.. note:: for sending OTA SMS messages :ref:`smpp-ota-tool` may be used.

View File

@@ -44,6 +44,11 @@ from pySim.legacy.ts_51_011 import EF
from pySim.card_handler 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():
@@ -185,6 +190,7 @@ def parse_options():
default=False, action="store_true")
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine")
parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
options = parser.parse_args()
@@ -770,6 +776,9 @@ if __name__ == '__main__':
# Parse options
opts = parse_options()
# Setup logger
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
# Init card reader driver
sl = init_reader(opts)

View File

@@ -46,11 +46,17 @@ from pySim.utils import dec_imsi, dec_iccid
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
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',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
argparse_add_reader_args(option_parser)
def select_app(adf: str, card: SimCard):
"""Select application by its AID"""
sw = 0
@@ -75,6 +81,9 @@ if __name__ == '__main__':
# Parse options
opts = option_parser.parse_args()
# Setup logger
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
# Init card reader driver
sl = init_reader(opts)

View File

@@ -107,12 +107,12 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
kwargs = {'include_ipy': True}
self.verbose = verbose
self._onchange_verbose('verbose', False, self.verbose);
PySimLogger.setup(self.poutput, {logging.WARN: YELLOW})
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})
self.intro = style(self.BANNER, fg=RED)
self.default_category = 'pySim-shell built-in commands'
self.card = None
@@ -1175,13 +1175,7 @@ if __name__ == '__main__':
opts = option_parser.parse_args()
# Ensure that we are able to print formatted warnings from the beginning.
PySimLogger.setup(print, {logging.WARN: YELLOW})
if opts.verbose:
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
else:
PySimLogger.set_verbose(False)
PySimLogger.set_level(logging.INFO)
PySimLogger.setup(print, {logging.WARN: YELLOW}, opts.verbose)
# Register csv-file as card data provider, either from specified CSV
# or from CSV file in home directory

View File

@@ -54,6 +54,8 @@ def compile_asn1_subdir(subdir_name:str, codec='der'):
__ver = sys.version_info
if (__ver.major, __ver.minor) >= (3, 9):
for i in resources.files('pySim.esim').joinpath('asn1').joinpath(subdir_name).iterdir():
if not i.name.endswith('.asn'):
continue
asn_txt += i.read_text()
asn_txt += "\n"
#else:

View File

@@ -19,7 +19,6 @@ import abc
import requests
import logging
import json
from re import match
from typing import Optional
import base64
from twisted.web.server import Request
@@ -211,7 +210,7 @@ class JsonHttpApiFunction(abc.ABC):
# additional custom HTTP headers (server responses)
extra_http_res_headers = {}
def __new__(cls, *args, role = None, **kwargs):
def __new__(cls, *args, role = 'legacy_client', **kwargs):
"""
Args:
args: (see JsonHttpApiClient and JsonHttpApiServer)
@@ -221,14 +220,13 @@ class JsonHttpApiFunction(abc.ABC):
# Create a dictionary with the class attributes of this class (the properties listed above and the encode_
# decode_ methods below). The dictionary will not include any dunder/magic methods
cls_attr = { attr_name: getattr(cls, attr_name) for attr_name in dir(cls) if not match("__.*__", attr_name) }
cls_attr = {attr_name: getattr(cls, attr_name) for attr_name in dir(cls) if not attr_name.startswith('__')}
# Normal instantiation as JsonHttpApiFunction:
if len(args) == 0:
if len(args) == 0 and len(kwargs) == 0:
return type(cls.__name__, (abc.ABC,), cls_attr)()
# Instantiation as as JsonHttpApiFunction with a JsonHttpApiClient or JsonHttpApiServer base
role = kwargs.get('role', 'legacy_client')
if role == 'legacy_client':
# Deprecated: With the advent of the server role (JsonHttpApiServer) the API had to be changed. To maintain
# compatibility with existing code (out-of-tree) the original behaviour and API interface and behaviour had

View File

@@ -352,6 +352,7 @@ class SmspTpScAddr(ConfigurableParameter):
strip_chars = ' \t\r\n'
max_len = 21 # '+' and 20 digits
min_len = 1
example_input = '+49301234567'
@classmethod
def validate_val(cls, val):
@@ -627,7 +628,7 @@ class MilenageRotationConstants(BinaryParam, AlgoConfig):
name = 'MilenageRotation'
algo_config_key = 'rotationConstants'
allow_len = 5 # length in bytes (from BinaryParam)
example_input = '0a 0b 0c 0d 0e'
example_input = '40 00 20 40 60'
@classmethod
def validate_val(cls, val):

View File

@@ -181,7 +181,7 @@ class SeqNumber(BER_TLV_IE, tag=0x80):
class NotificationAddress(BER_TLV_IE, tag=0x0c):
_construct = Utf8Adapter(GreedyBytes)
class Iccid(BER_TLV_IE, tag=0x5a):
_construct = BcdAdapter(GreedyBytes)
_construct = PaddedBcdAdapter(GreedyBytes)
class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation,
NotificationAddress, Iccid]):
pass

View File

@@ -266,11 +266,13 @@ class SCP02(SCP):
super().__init__(*args, **kwargs)
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)
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)
def _compute_cryptograms(self, card_challenge: bytes, host_challenge: bytes):

View File

@@ -63,7 +63,7 @@ class PySimLogger:
raise RuntimeError('static class, do not instantiate')
@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
called, default settings apply.
@@ -72,10 +72,20 @@ class PySimLogger:
have the following format: print_callback(message:str)
colors : An optional dict through which certain log levels can be assigned a color.
(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.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
def set_verbose(verbose:bool = False):
"""

View File

@@ -301,24 +301,53 @@ class LinkBaseTpdu(LinkBase):
prev_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)")
# When we have sent the first APDU, the SW may indicate that there are response bytes
# available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
# After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further
# TPDUs have to be sent in order to complete the task.
if sw is not None:
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
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
prev_tpdu = tpdu_gr
d, sw = self.send_tpdu(tpdu_gr)
data += d
if case == 4 or self.apdu_strict == False:
# In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data
# available which has to be retrieved using a GET RESPONSE command TPDU.
#
# ETSI TS 102 221, section 7.3.1.1.4 is very cleare about the fact that the GET RESPONSE mechanism
# shall only apply on case #4 APDUs but unfortunately it is impossible to distinguish between case #3
# and case #4 when the APDU format is not strictly followed. In order to be able to detect case #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
data_gr, sw = self.send_tpdu(tpdu_gr)
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
data += data_gr
if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
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

View File

@@ -2200,9 +2200,9 @@ update_record 6 fe0112ffb53e96e5ff99731d51ad7beafd0e23ffffffffffffffffffffffffff
update_record 7 fe02101da012f436d06824ecdd15050419ff9affffffffffffffffffffffffffffffff
update_record 8 fe02116929a373388ac904aff57ff57f6b3431ffffffffffffffffffffffffffffffff
update_record 9 fe0212a99245a5dc814e2f4c1aa908e9946e03ffffffffffffffffffffffffffffffff
update_record 10 fe0310521312c05a9aea93d70d44405172a580ffffffffffffffffffffffffffffffff
update_record 11 fe0311a9e45c72d45abde7db74261ee0c11b1bffffffffffffffffffffffffffffffff
update_record 12 fe0312867ba36b5873d60ea8b2cdcf3c0ddddaffffffffffffffffffffffffffffffff
update_record 10 fe03601111111111111111111111111111111111111111111111111111111111111111
update_record 11 fe03612222222222222222222222222222222222222222222222222222222222222222
update_record 12 fe03623333333333333333333333333333333333333333333333333333333333333333
#
################################################################################
# MF/DF.SYSTEM/EF.SIM_AUTH_COUNTER #

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface
INFO: Using PC/SC reader interface
Reading ...
Autodetected card type: Fairwaves-SIM
ICCID: 8988219000000117833

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface
INFO: Using PC/SC reader interface
Reading ...
Autodetected card type: Wavemobile-SIM
ICCID: 89445310150011013678

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface
INFO: Using PC/SC reader interface
Reading ...
Autodetected card type: fakemagicsim
ICCID: 1122334455667788990

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface
INFO: Using PC/SC reader interface
Reading ...
Autodetected card type: sysmoISIM-SJA2
ICCID: 8988211000000467343

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface
INFO: Using PC/SC reader interface
Reading ...
Autodetected card type: sysmoISIM-SJA5
ICCID: 8949440000001155314

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface
INFO: Using PC/SC reader interface
Reading ...
Autodetected card type: sysmoUSIM-SJS1
ICCID: 8988211320300000028

View File

@@ -1,4 +1,4 @@
Using PC/SC reader interface
INFO: Using PC/SC reader interface
Reading ...
Autodetected card type: sysmosim-gr1
ICCID: 2222334455667788990

View File

@@ -15,7 +15,7 @@
},
{
"profile_info": {
"iccid": "8949449999999990031f",
"iccid": "8949449999999990031",
"isdp_aid": "a0000005591010ffffffff8900001200",
"profile_state": "disabled",
"service_provider_name": "OsmocomSPN",

View File

@@ -23,7 +23,7 @@ import os
import json
from utils import *
# This testcase requires a sysmoEUICC1-C2T with the test prfile TS48V1-B-UNIQUE (ICCID 8949449999999990031f)
# This testcase requires a sysmoEUICC1-C2T with the test prfile TS48V1-B-UNIQUE (ICCID 8949449999999990031)
# installed, and in disabled state. Also the profile must be installed in such a way that notifications are
# generated when the profile is disabled or enabled (ProfileMetadata)

View File

@@ -3,6 +3,9 @@ set echo true
select ADF.ISD-R
# Ensure that the test-profile we intend to test with is actually enabled
enable_profile --iccid 89000123456789012341
# by ICCID (pre-installed test profile on sysmoEUICC1-C2T)
disable_profile --iccid 89000123456789012341 > enable_disable_profile.tmp
enable_profile --iccid 89000123456789012341 >> enable_disable_profile.tmp

View File

@@ -3,6 +3,11 @@ set echo true
select ADF.ISD-R
# Generate two (additional) notifications by quickly enabeling the test profile
enable_profile --iccid 8949449999999990031f
# Ensure that the test-profile is actually enabled. (In case te test-profile
# was disabled, a notification may be generated. The testcase should tolerate
# that)
enable_profile --iccid 89000123456789012341
# Generate two (additional) notifications by quickly enabeling the test profile
enable_profile --iccid 8949449999999990031
enable_profile --iccid 89000123456789012341

View File

@@ -1,5 +1,10 @@
set debug true
set echo true
# The output of get_profiles_info will also include the "profile_state", which
# can be either "enabled" or "disabled". Ensure that the correct profile is
# enabled.
enable_profile --iccid 89000123456789012341
select ADF.ISD-R
get_profiles_info > get_profiles_info.tmp

View File

@@ -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'

View File

@@ -20,13 +20,14 @@
# You should have received a copy of the GNU General Public License
# 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_LOG=./pySim-smpp2sim.log
PYSIM_SMPP2SIM_PORT=2775
PYSIM_SMPP2SIM_TIMEOUT=10
PYSIM_SMPPOTATOOL=./contrib/smpp-ota-tool.py
PYSIM_SMPPOTATOOL_LOG=./smpp-ota-tool.log
PYSIM_SHELL=./pySim-shell.py
function dump_logs {
echo ""
@@ -44,12 +45,11 @@ function dump_logs {
function send_test_request {
echo ""
echo "Sending request to SMPP server:"
TAR=$1
C_APDU=$2
R_APDU_EXPECTED=$3
C_APDU=$1
R_APDU_EXPECTED=$2
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"
R_APDU=`$COMMANDLINE 2> $PYSIM_SMPPOTATOOL_LOG`
if [ $? -ne 0 ]; then
@@ -57,7 +57,7 @@ function send_test_request {
dump_logs
exit 1
fi
echo ""
echo "Got response from SMPP server:"
echo "Sent: $C_APDU"
echo "Received: $R_APDU"
@@ -68,16 +68,14 @@ function send_test_request {
exit 1
fi
echo "Response matches the expected response -- success!"
echo ""
}
function start_smpp_server {
PCSC_READER=$1
# Start the SMPP server
echo ""
echo "Starting SMPP server:"
# Start the SMPP server
COMMANDLINE="$PYSIM_SMPP2SIM -p $PCSC_READER --smpp-bind-port $PYSIM_SMPP2SIM_PORT --apdu-trace"
echo "Commandline: $COMMANDLINE"
$COMMANDLINE > $PYSIM_SMPP2SIM_LOG 2>&1 &
@@ -102,55 +100,117 @@ function start_smpp_server {
echo "SMPP server reachable (port=$PYSIM_SMPP2SIM_PORT)"
}
function find_card_by_iccid {
# Find reader number of the card
ICCID=$1
function stop_smpp_server {
echo ""
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 "Searching for card:"
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
echo "invalid ICCID, zero length ICCID is not allowed! -- abort"
exit 1
fi
PCSC_READER_COUNT=`pcsc_scan -rn | wc -l`
for PCSC_READER in $(seq 0 $(($PCSC_READER_COUNT-1))); do
echo "probing card 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`
echo $EF_ICCID_DECODED | grep $ICCID > /dev/null
echo "probing card (ICCID) in reader $PCSC_READER ..."
RESULT_JSON=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e "select EF.ICCID" -e "read_binary_decoded" 2> /dev/null | tail -3`
echo $RESULT_JSON | grep $ICCID > /dev/null
if [ $? -eq 0 ]; then
echo "Found card in reader $PCSC_READER"
echo "Found card (by ICCID) in reader $PCSC_READER"
return $PCSC_READER
fi
done
echo "Card with ICCID \"$ICCID\" not found -- abort"
echo "Card not found -- abort"
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=./
echo "pySim-smpp2sim_test - a test program to test pySim-smpp2sim.py"
echo "=============================================================="
# TODO: At the moment we can only have one card and one testcase. This is
# sufficient for now. We can extend this later as needed.
# Read test parameters from config from file
TEST_CONFIG_FILE=${0%.*}.cfg
echo "using config file: $TEST_CONFIG_FILE"
if ! [ -e "$TEST_CONFIG_FILE" ]; then
echo "test configuration file does not exist! -- abort"
exit 1
fi
. $TEST_CONFIG_FILE
# Execute testcase
find_card_by_iccid $ICCID
start_smpp_server $?
send_test_request $TAR $APDU "$EXPECTED_RESPONSE"
TESTCASE_DIR=`dirname $0`
for TEST_CONFIG_FILE in $TESTCASE_DIR/testcase_*.cfg ; do
echo ""
echo "running testcase: $TEST_CONFIG_FILE"
. $TEST_CONFIG_FILE
find_card_by_iccid_or_eid $ICCID $EID
PCSC_READER=$?
enable_profile $PCSC_READER $ICCID $EID
start_smpp_server $PCSC_READER
send_test_request $APDU "$EXPECTED_RESPONSE"
stop_smpp_server
echo ""
echo "testcase ok"
echo "--------------------------------------------------------------"
done
echo "done."

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'