mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-21 21:08:36 +03:00
Compare commits
1 Commits
master
...
pmaier/ota
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d5df4329f |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
.*.sw?
|
.*.swp
|
||||||
|
|
||||||
/docs/_*
|
/docs/_*
|
||||||
/docs/generated
|
/docs/generated
|
||||||
|
|||||||
@@ -30,48 +30,6 @@ 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
|
||||||
|
|
||||||
@@ -183,7 +141,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)
|
||||||
@@ -209,28 +167,65 @@ 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.kid_idx,
|
kid_idx=opts.kic_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' : not opts.no_ciphering,
|
'ciphering' : opts.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': not opts.por_no_ciphering,
|
'por_shall_be_ciphered':opts.por_shall_be_ciphered,
|
||||||
'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))
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ 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,179 +0,0 @@
|
|||||||
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,5 +55,3 @@ 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.
|
|
||||||
|
|||||||
@@ -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
|
||||||
raise ValueError('Invalid 1-byte generic APDU access rule')
|
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||||
else:
|
else:
|
||||||
if len(do) % 8:
|
if len(do) % 8:
|
||||||
raise ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
|
return 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'
|
||||||
raise ValueError('Invalid 1-byte generic APDU access rule')
|
return ValueError('Invalid 1-byte generic APDU access rule')
|
||||||
else:
|
else:
|
||||||
if not 'apdu_filter' in self.decoded:
|
if not 'apdu_filter' in self.decoded:
|
||||||
raise ValueError('Invalid APDU AR DO')
|
return 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:
|
||||||
raise ValueError('APDU filter must contain header and mask')
|
return 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:
|
||||||
raise ValueError('APDU filter header and mask must each be 4 bytes')
|
return 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:
|
||||||
raise ValueError('DO > 255 bytes not supported yet')
|
return 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
|
||||||
@@ -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:
|
||||||
raise ValueError(f'Invalid non-modulo-16 length of APDU filter: {len(opts.apdu_filter)}')
|
return ValueError('Invalid non-modulo-16 length of APDU filter: %d' % len(do))
|
||||||
offset = 0
|
offset = 0
|
||||||
apdu_filter = []
|
apdu_filter = []
|
||||||
while offset < len(opts.apdu_filter):
|
while offset < len(opts.apdu_filter):
|
||||||
|
|||||||
@@ -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='Administrative Data', size=(3, None), **kwargs):
|
desc='Service Provider Name', 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: MS operation mode
|
# Byte 1: Display Condition
|
||||||
'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),
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ def compile_asn1_subdir(subdir_name:str, codec='der'):
|
|||||||
__ver = sys.version_info
|
__ver = sys.version_info
|
||||||
if (__ver.major, __ver.minor) >= (3, 9):
|
if (__ver.major, __ver.minor) >= (3, 9):
|
||||||
for i in resources.files('pySim.esim').joinpath('asn1').joinpath(subdir_name).iterdir():
|
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 += i.read_text()
|
||||||
asn_txt += "\n"
|
asn_txt += "\n"
|
||||||
#else:
|
#else:
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import abc
|
|||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Optional, Tuple
|
from re import match
|
||||||
|
from typing import Optional
|
||||||
import base64
|
import base64
|
||||||
from twisted.web.server import Request
|
from twisted.web.server import Request
|
||||||
|
|
||||||
@@ -180,7 +181,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', see also method rewrite_url).
|
# path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder')
|
||||||
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
|
||||||
@@ -210,7 +211,7 @@ class JsonHttpApiFunction(abc.ABC):
|
|||||||
# additional custom HTTP headers (server responses)
|
# additional custom HTTP headers (server responses)
|
||||||
extra_http_res_headers = {}
|
extra_http_res_headers = {}
|
||||||
|
|
||||||
def __new__(cls, *args, role = 'legacy_client', **kwargs):
|
def __new__(cls, *args, role = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
args: (see JsonHttpApiClient and JsonHttpApiServer)
|
args: (see JsonHttpApiClient and JsonHttpApiServer)
|
||||||
@@ -220,13 +221,14 @@ class JsonHttpApiFunction(abc.ABC):
|
|||||||
|
|
||||||
# Create a dictionary with the class attributes of this class (the properties listed above and the encode_
|
# 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
|
# 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 attr_name.startswith('__')}
|
cls_attr = { attr_name: getattr(cls, attr_name) for attr_name in dir(cls) if not match("__.*__", attr_name) }
|
||||||
|
|
||||||
# Normal instantiation as JsonHttpApiFunction:
|
# Normal instantiation as JsonHttpApiFunction:
|
||||||
if len(args) == 0 and len(kwargs) == 0:
|
if len(args) == 0:
|
||||||
return type(cls.__name__, (abc.ABC,), cls_attr)()
|
return type(cls.__name__, (abc.ABC,), cls_attr)()
|
||||||
|
|
||||||
# Instantiation as as JsonHttpApiFunction with a JsonHttpApiClient or JsonHttpApiServer base
|
# Instantiation as as JsonHttpApiFunction with a JsonHttpApiClient or JsonHttpApiServer base
|
||||||
|
role = kwargs.get('role', 'legacy_client')
|
||||||
if role == 'legacy_client':
|
if role == 'legacy_client':
|
||||||
# Deprecated: With the advent of the server role (JsonHttpApiServer) the API had to be changed. To maintain
|
# 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
|
# compatibility with existing code (out-of-tree) the original behaviour and API interface and behaviour had
|
||||||
@@ -336,22 +338,6 @@ 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):
|
||||||
@@ -368,16 +354,8 @@ 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
|
||||||
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."""
|
||||||
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
|
||||||
@@ -386,11 +364,6 @@ 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))
|
||||||
|
|
||||||
@@ -402,6 +375,7 @@ 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))
|
||||||
|
|||||||
@@ -151,8 +151,6 @@ 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)
|
||||||
@@ -280,8 +278,6 @@ 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):
|
||||||
@@ -308,8 +304,6 @@ 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
|
||||||
|
|
||||||
@@ -329,8 +323,6 @@ 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:
|
||||||
@@ -441,7 +433,7 @@ class File:
|
|||||||
elif k == 'fillFileContent':
|
elif k == 'fillFileContent':
|
||||||
stream.write(v)
|
stream.write(v)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown key '%s' in tuple list" % k)
|
return 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]:
|
||||||
|
|||||||
@@ -352,7 +352,6 @@ class SmspTpScAddr(ConfigurableParameter):
|
|||||||
strip_chars = ' \t\r\n'
|
strip_chars = ' \t\r\n'
|
||||||
max_len = 21 # '+' and 20 digits
|
max_len = 21 # '+' and 20 digits
|
||||||
min_len = 1
|
min_len = 1
|
||||||
example_input = '+49301234567'
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val):
|
||||||
@@ -628,7 +627,7 @@ class MilenageRotationConstants(BinaryParam, AlgoConfig):
|
|||||||
name = 'MilenageRotation'
|
name = 'MilenageRotation'
|
||||||
algo_config_key = 'rotationConstants'
|
algo_config_key = 'rotationConstants'
|
||||||
allow_len = 5 # length in bytes (from BinaryParam)
|
allow_len = 5 # length in bytes (from BinaryParam)
|
||||||
example_input = '40 00 20 40 60'
|
example_input = '0a 0b 0c 0d 0e'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_val(cls, val):
|
def validate_val(cls, val):
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class SeqNumber(BER_TLV_IE, tag=0x80):
|
|||||||
class NotificationAddress(BER_TLV_IE, tag=0x0c):
|
class NotificationAddress(BER_TLV_IE, tag=0x0c):
|
||||||
_construct = Utf8Adapter(GreedyBytes)
|
_construct = Utf8Adapter(GreedyBytes)
|
||||||
class Iccid(BER_TLV_IE, tag=0x5a):
|
class Iccid(BER_TLV_IE, tag=0x5a):
|
||||||
_construct = PaddedBcdAdapter(GreedyBytes)
|
_construct = BcdAdapter(GreedyBytes)
|
||||||
class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation,
|
class NotificationMetadata(BER_TLV_IE, tag=0xbf2f, nested=[SeqNumber, ProfileMgmtOperation,
|
||||||
NotificationAddress, Iccid]):
|
NotificationAddress, Iccid]):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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):
|
||||||
_construct = GreedyRange(Int16ub)
|
_consuruct = 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, nested=[ApplicationAID]):
|
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[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 STORE DATA command in order to store some card-specific data.
|
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
|
||||||
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
See GlobalPlatform CardSpecification v2.3Section 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 STORE DATA command in order to store some card-specific data.
|
"""Perform the GlobalPlatform GET DATA command in order to store some card-specific data.
|
||||||
See GlobalPlatform CardSpecification v2.3 Section 11.11 for details."""
|
See GlobalPlatform CardSpecification v2.3Section 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 h2b(response)
|
return data
|
||||||
|
|
||||||
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')
|
||||||
|
|||||||
@@ -266,13 +266,11 @@ 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:
|
||||||
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB)
|
||||||
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:
|
||||||
# See also GPC section B.1.1.2, E.4.7, and E.4.1
|
cipher = DES.new(self.card_keys.dek[:8], DES.MODE_ECB)
|
||||||
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):
|
||||||
@@ -438,7 +436,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=True for computing the ICV for R-DEC."""
|
* is_response=False 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.
|
||||||
|
|||||||
@@ -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_crypt)
|
raise ValueError('No implementation for crypt algorithm %s' % otak.algo_auth)
|
||||||
|
|
||||||
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_auth))
|
raise ValueError('Cannot use algorithm %s with key for %s' % (self.enum_name, otak.algo_crypt))
|
||||||
super().__init__(otak)
|
super().__init__(otak)
|
||||||
|
|
||||||
def sign(self, data:bytes) -> bytes:
|
def sign(self, data:bytes) -> bytes:
|
||||||
|
|||||||
10
pySim/sms.py
10
pySim/sms.py
@@ -169,14 +169,8 @@ 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,
|
flags_construct = BitStruct('tp_rp'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag, 'tp_sri'/Flag,
|
||||||
'tp_udhi'/Flag,
|
Padding(1), 'tp_mms'/Flag, 'tp_mti'/BitsInteger(2))
|
||||||
'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)
|
||||||
|
|||||||
@@ -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='5GS 3GPP location information', **kwargs):
|
desc='5S 3GP 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 ProSeConfigDataForUsageInfoReporting(BER_TLV_IE, tag=0xa0,
|
class ProSeConfigDataForUeToNetworkRelayUE(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.ProSeConfigDataForUsageInfoReporting
|
self._tlv = EF_5G_PROSE_UIR.ProSeConfigDataForUeToNetworkRelayUE
|
||||||
|
|
||||||
# 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):
|
||||||
|
|||||||
@@ -1117,8 +1117,8 @@ class DF_GSM(CardDF):
|
|||||||
EF_MBI(),
|
EF_MBI(),
|
||||||
EF_MWIS(),
|
EF_MWIS(),
|
||||||
EF_CFIS(),
|
EF_CFIS(),
|
||||||
EF_EXT('6fc8', None, 'EF.EXT6', desc='Extension6 (MBDN)'),
|
EF_EXT('6fc8', None, 'EF.EXT6', desc='Externsion6 (MBDN)'),
|
||||||
EF_EXT('6fcc', None, 'EF.EXT7', desc='Extension7 (CFIS)'),
|
EF_EXT('6fcc', None, 'EF.EXT7', desc='Externsion7 (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)'),
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ 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
|
||||||
@@ -910,8 +911,7 @@ class DataObjectCollection:
|
|||||||
def encode(self, decoded) -> bytes:
|
def encode(self, decoded) -> bytes:
|
||||||
res = bytearray()
|
res = bytearray()
|
||||||
for i in decoded:
|
for i in decoded:
|
||||||
name = i[0]
|
obj = self.members_by_name(i[0])
|
||||||
obj = self.members_by_name[name]
|
|
||||||
res.append(obj.to_tlv())
|
res.append(obj.to_tlv())
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|||||||
@@ -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 fe03601111111111111111111111111111111111111111111111111111111111111111
|
update_record 10 fe0310521312c05a9aea93d70d44405172a580ffffffffffffffffffffffffffffffff
|
||||||
update_record 11 fe03612222222222222222222222222222222222222222222222222222222222222222
|
update_record 11 fe0311a9e45c72d45abde7db74261ee0c11b1bffffffffffffffffffffffffffffffff
|
||||||
update_record 12 fe03623333333333333333333333333333333333333333333333333333333333333333
|
update_record 12 fe0312867ba36b5873d60ea8b2cdcf3c0ddddaffffffffffffffffffffffffffffffff
|
||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
# MF/DF.SYSTEM/EF.SIM_AUTH_COUNTER #
|
# MF/DF.SYSTEM/EF.SIM_AUTH_COUNTER #
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"profile_info": {
|
"profile_info": {
|
||||||
"iccid": "8949449999999990031",
|
"iccid": "8949449999999990031f",
|
||||||
"isdp_aid": "a0000005591010ffffffff8900001200",
|
"isdp_aid": "a0000005591010ffffffff8900001200",
|
||||||
"profile_state": "disabled",
|
"profile_state": "disabled",
|
||||||
"service_provider_name": "OsmocomSPN",
|
"service_provider_name": "OsmocomSPN",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
from utils import *
|
from utils import *
|
||||||
|
|
||||||
# This testcase requires a sysmoEUICC1-C2T with the test prfile TS48V1-B-UNIQUE (ICCID 8949449999999990031)
|
# This testcase requires a sysmoEUICC1-C2T with the test prfile TS48V1-B-UNIQUE (ICCID 8949449999999990031f)
|
||||||
# installed, and in disabled state. Also the profile must be installed in such a way that notifications are
|
# 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)
|
# generated when the profile is disabled or enabled (ProfileMetadata)
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ select ADF.ISD-R
|
|||||||
enable_profile --iccid 89000123456789012341
|
enable_profile --iccid 89000123456789012341
|
||||||
|
|
||||||
# Generate two (additional) notifications by quickly enabeling the test profile
|
# Generate two (additional) notifications by quickly enabeling the test profile
|
||||||
enable_profile --iccid 8949449999999990031
|
enable_profile --iccid 8949449999999990031f
|
||||||
enable_profile --iccid 89000123456789012341
|
enable_profile --iccid 89000123456789012341
|
||||||
|
|||||||
9
tests/pySim-smpp2sim_test/pySim-smpp2sim_test.cfg
Normal file
9
tests/pySim-smpp2sim_test/pySim-smpp2sim_test.cfg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 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'
|
||||||
@@ -20,14 +20,13 @@
|
|||||||
# 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 ""
|
||||||
@@ -45,11 +44,12 @@ 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:"
|
||||||
C_APDU=$1
|
TAR=$1
|
||||||
R_APDU_EXPECTED=$2
|
C_APDU=$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 --kic-idx $KEY_INDEX --kid-idx $KEY_INDEX --algo-crypt $ALGO_CRYPT --algo-auth $ALGO_AUTH --tar $TAR --apdu $C_APDU"
|
COMMANDLINE="$PYSIM_SMPPOTATOOL --verbose --port $PYSIM_SMPP2SIM_PORT --kic $KIC --kid $KID --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,14 +68,16 @@ 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 &
|
||||||
@@ -100,117 +102,55 @@ function start_smpp_server {
|
|||||||
echo "SMPP server reachable (port=$PYSIM_SMPP2SIM_PORT)"
|
echo "SMPP server reachable (port=$PYSIM_SMPP2SIM_PORT)"
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop_smpp_server {
|
function find_card_by_iccid {
|
||||||
echo ""
|
# Find reader number of the card
|
||||||
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
|
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 (ICCID) in reader $PCSC_READER ..."
|
echo "probing card 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`
|
EF_ICCID_DECODED=`$PYSIM_SHELL -p $PCSC_READER --noprompt -e 'select EF.ICCID' -e 'read_binary_decoded --oneline' 2> /dev/null | tail -1`
|
||||||
echo $RESULT_JSON | grep $ICCID > /dev/null
|
echo $EF_ICCID_DECODED | grep $ICCID > /dev/null
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "Found card (by ICCID) in reader $PCSC_READER"
|
echo "Found card in reader $PCSC_READER"
|
||||||
return $PCSC_READER
|
return $PCSC_READER
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Card not found -- abort"
|
echo "Card with ICCID \"$ICCID\" 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 "=============================================================="
|
||||||
|
|
||||||
TESTCASE_DIR=`dirname $0`
|
# TODO: At the moment we can only have one card and one testcase. This is
|
||||||
for TEST_CONFIG_FILE in $TESTCASE_DIR/testcase_*.cfg ; do
|
# sufficient for now. We can extend this later as needed.
|
||||||
echo ""
|
|
||||||
echo "running testcase: $TEST_CONFIG_FILE"
|
# Read test parameters from config from file
|
||||||
. $TEST_CONFIG_FILE
|
TEST_CONFIG_FILE=${0%.*}.cfg
|
||||||
find_card_by_iccid_or_eid $ICCID $EID
|
echo "using config file: $TEST_CONFIG_FILE"
|
||||||
PCSC_READER=$?
|
if ! [ -e "$TEST_CONFIG_FILE" ]; then
|
||||||
enable_profile $PCSC_READER $ICCID $EID
|
echo "test configuration file does not exist! -- abort"
|
||||||
start_smpp_server $PCSC_READER
|
exit 1
|
||||||
send_test_request $APDU "$EXPECTED_RESPONSE"
|
fi
|
||||||
stop_smpp_server
|
. $TEST_CONFIG_FILE
|
||||||
echo ""
|
|
||||||
echo "testcase ok"
|
# Execute testcase
|
||||||
echo "--------------------------------------------------------------"
|
find_card_by_iccid $ICCID
|
||||||
done
|
start_smpp_server $?
|
||||||
|
send_test_request $TAR $APDU "$EXPECTED_RESPONSE"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo "done."
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# 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'
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# 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'
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# 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'
|
|
||||||
Reference in New Issue
Block a user